Skip to content

Commit

Permalink
proxy authentication, ip bans, and connection timeouts
Browse files Browse the repository at this point in the history
  • Loading branch information
william-stacken committed Jul 29, 2021
1 parent 6654dbd commit d60cf9b
Show file tree
Hide file tree
Showing 9 changed files with 243 additions and 63 deletions.
44 changes: 23 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,24 +19,26 @@ An HTTPS man-in-the-middle proxy framework written in C#.
- OCSP query support (No OCSP stapling).
- Certificate Revocation List (CRL) support.
- Logging of incoming HTTP requests and responses using different verbosity levels.
- Optional proxy authentication
- Web interfaces with HTTP basic authentication support.
- Customized web interfaces for remote plugin configuration.
- Web interface to view and delete logs remotely.
- IP bans for clients sending too many incorrect login attempts.

## 2. Files
The program creates the following folders and files:
- **logs**: Folder that contains one log file for each host, named `<hostname>.log`, containing
- `logs`: Folder that contains one log file for each host, named `<hostname>.log`, containing
each request sent and response received from that host.
- **-RootCA-.pfx**: PKCS12 file containing the Root CA certificate that will be used to sign
certificates generated by Tiriryarai. The Root CA certificate needs to be
installed in your client, refer to [6. How To Use](#6-how-to-use) for details.
- **-OcspCA-.pfx**: PKCS12 file containing the OCSP CA certificate that will be used to sign
OCSP responses generated by Tiriryarai. The password to this file is `secret`.
- **tiriryarai.pfx**: PKCS12 file containing the certificate that will be used to authenticate the
Tiriryarai host itself.
- **(plugin-hostname).pfx**: PKCS12 file containing the certificate that will be used to authenticate the
Man-in-the-middle plugin. It will use the hostname that was supplied via the
`-d` flag or the local IP address.
- `-RootCA-.pfx`: PKCS12 file containing the Root CA certificate that will be used to sign
certificates generated by Tiriryarai. The Root CA certificate needs to be
installed in your client, refer to [6. How To Use](#6-how-to-use) for details.
- `-OcspCA-.pfx`: PKCS12 file containing the OCSP CA certificate that will be used to sign
OCSP responses generated by Tiriryarai.
- `tiriryarai.pfx`: PKCS12 file containing the certificate that will be used to authenticate the
Tiriryarai host itself.
- `<plugin-hostname>.pfx`: PKCS12 file containing the certificate that will be used to authenticate the
Man-in-the-middle plugin. It will use the hostname that was supplied via the
`-d` flag or the local IP address.

The password to all `.pfx` files is `secret`. By default, they can be found in the application data
folder, which is `$HOME/.config/Tiriryarai`
Expand All @@ -45,16 +47,16 @@ on Unix systems.
## 3. Web Interface
The following Uri endpoints are used by Tiriryarai and will be invoked if it receives an HTTP request
to `http[s]://tiriryarai` as the destination host.
- **/**: Contains a welcome page with instructions and links.
- **/favicon.ico**: Contains the favicon used by the interface.
- **/cert** and **/Tiriryarai.crt**: Contains the Root CA certificate used to sign certificates
generated by Tiriryarai.
- **/ocsp**: Contains an OCSP responder that will send an OCSP response indicating that the certificate in the
request was valid.
- **/revoked.crl**: Contains an empty certificate revocation list, meaning that no certificates have been revoked.
- **/logs/***: Contains a log management interface that lists links to all logs. This endpoint can only be accessed
securely using HTTPS. It is disabled by default and can be enabled using the `-l` flag. If Tiriryarai
was configured to use a username and password, it will be protected using HTTP basic authentication.
- `/`: Contains a welcome page with instructions and links.
- `/favicon.ico`: Contains the favicon used by the interface.
- `/cert` and `/Tiriryarai.crt`: Contains the Root CA certificate used to sign certificates
generated by Tiriryarai.
- `/ocsp`: Contains an OCSP responder that will send an OCSP response indicating that the certificate in the
request was valid.
- `/revoked.crl`: Contains an empty certificate revocation list, meaning that no certificates have been revoked.
- `/logs/*`: Contains a log management interface that lists links to all logs. This endpoint can only be accessed
securely using HTTPS. It is disabled by default and can be enabled using the `-l` flag. If Tiriryarai
was configured to use a username and password, it will be protected using HTTP basic authentication.

Tiriryarai supports custom web interfaces for each plugin using the `HomePage()` method in the `IManInTheMiddle`
interface, see [5. Adding Plugins](#5-adding-plugins) for details about how to add plugins. The custom web page is
Expand Down
9 changes: 5 additions & 4 deletions Tiriryarai/Http/HttpMessage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -397,16 +397,17 @@ public void SetDecodedBodyAndLength(byte[] body)
}

/// <summary>
/// Checks if the message has HTTP basic authentication matching the
/// given username and password. Assumes valid username and password.
/// Checks if the message has HTTP basic authentication in the given header
/// matching the given username and password. Assumes valid username and password.
/// </summary>
/// <returns><c>true</c>, if authenticated, <c>false</c> otherwise.</returns>
/// <param name="header">The header that should contain the basic authentication.</param>
/// <param name="user">The given username.</param>
/// <param name="pass">The given password.</param>
public bool BasicAuthenticated(string user, string pass)
public bool BasicAuthenticated(string header, string user, string pass)
{
string[] auth;
string[] authArr = GetHeader("Authorization");
string[] authArr = GetHeader(header);
if (authArr != null && authArr.Length > 0 && authArr[0] != null)
{
auth = authArr[0].Split(' ');
Expand Down
21 changes: 16 additions & 5 deletions Tiriryarai/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,11 @@ class Program
private static uint? MaxLogSize = null;
private static string Username = null;
private static string Password = null;
private static string ProxyPass = null;
private static string ConfigDir = null;
private static bool Logs = false;
private static bool IgnoreCerts = false;
private static int ReadTimeout = -1;
private static bool Help = false;
private static bool Version = false;

Expand All @@ -55,15 +57,21 @@ static void Main(string[] args)
List<string> extraOpts = new List<string>();
OptionSet opts = new OptionSet
{
{ "d|hostname=", "The hostname of the server, if it has one.", (host) => Hostname = host },
{ "d|hostname=", "The hostname of the server, if it has one. If not given, it will default " +
"to the system IP.", (host) => Hostname = host },
{ "p|port=", "The port the server will listen on, 8081 by default.", (ushort port) => Port = port },
{ "v|verbosity=", "The higher this value is, the more information will be logged.", (uint v) => Verbosity = v },
{ "s|logsize=", "The maximum allowed size of a log in MiB before it is deleted.", (uint s) => MaxLogSize = s },
{ "u|username=", "The username required for basic HTTP authentication if one should be required.", (user) => Username = user },
{ "w|password=", "The password required for basic HTTP authentication if one should be required.", (pass) => Password = pass },
{ "u|username=", "The username required for basic HTTP authentication if one should be required. " +
"Used for both proxy authentication and accessing the admin pages.", (user) => Username = user },
{ "w|password=", "The password required for accessing the admin pages if one should be required. " +
"It will be sent securely using HTTPS only.", (pass) => Password = pass },
{ "x|proxypass=", "The password required for using the proxy if one should be required. " +
"It will be sent insecurely using HTTP and should not be the same as the admin password.", (pass) => ProxyPass = pass },
{ "c|configdir=", "The directory where certificates, server configuration, and log files will be stored.", (dir) => ConfigDir = dir },
{ "l|logs", "Activate remote log management via the web interface. Usage of authentication recommended.", _ => Logs = true },
{ "l|logs", "Activate admin remote log management via the web interface. Usage of authentication recommended.", _ => Logs = true },
{ "i|ignorecerts", "Ignore invalid certificates when sending HTTPS requests.", _ => IgnoreCerts = true },
{ "t|timeout=", "The time in milliseconds to wait on a client request before terminating the connection.", (int t) => ReadTimeout = t },
{ "h|help", "Show help", _ => Help = true },
{ "version", "Show version and about info", _ => Version = true }
};
Expand Down Expand Up @@ -116,12 +124,15 @@ where t.GetInterfaces().Contains(typeof(IManInTheMiddle))
);
HttpsMitmProxyParams prms = new HttpsMitmProxyParams(mitms.ElementAt(0), Port, Username, Password)
{
ProxyPassword = ProxyPass,
ConfigDirectory = ConfigDir,
LogVerbosity = Verbosity,
MaxLogSize = MaxLogSize,
Hostname = Hostname,
LogManagement = Logs,
IgnoreCertificates = IgnoreCerts
IgnoreCertificates = IgnoreCerts,
AllowedLoginAttempts = 5,
ReadTimeout = ReadTimeout
};
proxy = new HttpsMitmProxy(prms);
}
Expand Down
96 changes: 70 additions & 26 deletions Tiriryarai/Server/HttpsMitmProxy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -255,33 +255,58 @@ private void ProcessClient(TcpClient client)
HttpRequest req;
HttpResponse resp;
string host;
X509Certificate2 cert;
Stream stream = client.GetStream();
X509Certificate2 cert = null;
NetworkStream stream = client.GetStream();
IPAddress clientIp = (client.Client.RemoteEndPoint as IPEndPoint)?.Address;
try
{
try
{
req = HttpRequest.FromStream(stream);
}
catch (Exception e)
if (clientIp == null)
throw new NullReferenceException(nameof(clientIp));

if (cache.GetIPStatistics(clientIp).IsBanned(prms.AllowedLoginAttempts))
{
resp = DefaultHttpResponse(400);
resp = DefaultHttpResponse(403);
resp.ToStream(stream);
throw e;
client.Close();
return;
}

if (prms.ReadTimeout > 0)
stream.ReadTimeout = prms.ReadTimeout;
try
{
host = req.Uri.Split(':')[0];
cert = cache.GetCertificate(host);
req = HttpRequest.FromStream(stream);
}
catch (Exception e)
{
resp = DefaultHttpResponse(500, req);
resp = DefaultHttpResponse(400);
resp.ToStream(stream);
throw e;
}

if (req.Method == Method.CONNECT)
{
host = req.Uri.Split(':')[0];
if (prms.ProxyAuthenticate &&
!IsTiriryarai(host) &&
!req.BasicAuthenticated("Proxy-Authorization", prms.Username, prms.ProxyPassword))
{
// Don't count login attempts here as it would be really easy to get banned by mistake otherwise
resp = DefaultHttpResponse(407, req);
resp.ToStream(stream);
client.Close();
return;
}
try
{
cert = cache.GetCertificate(host);
}
catch (Exception e)
{
resp = DefaultHttpResponse(500, req);
resp.ToStream(stream);
throw e;
}
resp = new HttpResponse(200, null, null, "Connection Established");
resp.ToStream(stream);
SslStream sslStream = new SslStream(stream);
Expand All @@ -291,7 +316,7 @@ private void ProcessClient(TcpClient client)
try
{
req = HttpRequest.FromStream(sslStream);
resp = HandleRequest(req, tls: true);
resp = HandleRequest(req, host, clientIp, tls: true);
}
catch (Exception e)
{
Expand All @@ -313,7 +338,18 @@ private void ProcessClient(TcpClient client)
{
// If a non-CONNECT request is received, it will be proxied
// directly using the host header.
resp = HandleRequest(req, tls: false);
host = req.Host.Split(':')[0];
if (prms.ProxyAuthenticate &&
!IsTiriryarai(host) &&
!req.BasicAuthenticated("Proxy-Authorization", prms.Username, prms.ProxyPassword))
{
// Don't count login attempts here as it would be really easy to get banned by mistake otherwise
resp = DefaultHttpResponse(407, req);
}
else
{
resp = HandleRequest(req, host, clientIp, tls: false);
}
resp.ToStream(stream);
}
}
Expand All @@ -327,18 +363,16 @@ private void ProcessClient(TcpClient client)
}
}

private HttpResponse HandleRequest(HttpRequest req, bool tls)
private HttpResponse HandleRequest(HttpRequest req, string host, IPAddress client, bool tls)
{
HttpResponse resp;
HttpMessage http;
try
{
string host = req.Host;

Console.WriteLine("\n--------------------\n" +
req.Method + (tls ? " https://" : " http://") + host + req.Path);

if (!IsDestinedToMitm(req))
if (!IsTiriryarai(host))
{
if (!prms.MitM.Block(host))
{
Expand All @@ -355,7 +389,7 @@ private HttpResponse HandleRequest(HttpRequest req, bool tls)
}
catch (Exception e)
{
// TODO Examine exception and return a more descriptive message
// TODO Examine exception and return a more descriptive response
logger.LogException(e);
return DefaultHttpResponse(502);
}
Expand All @@ -382,7 +416,7 @@ private HttpResponse HandleRequest(HttpRequest req, bool tls)
}
else
{
resp = HomePage(req, tls);
resp = HomePage(req, client, tls);
}
}
catch (Exception e)
Expand All @@ -393,17 +427,16 @@ private HttpResponse HandleRequest(HttpRequest req, bool tls)
return resp;
}

private bool IsDestinedToMitm(HttpRequest req)
private bool IsTiriryarai(string host)
{
// TODO: This may not be an exhaustive list, if there is another
// loopback IP, there is a risk of an infinite loop where the proxy
// sends requests to itself
string host = req.Host.Split(':')[0];
return host.Equals(Resources.HOSTNAME) ||
pluginHosts.Contains(host);
}

private HttpResponse HomePage(HttpRequest req, bool tls)
private HttpResponse HomePage(HttpRequest req, IPAddress client, bool tls)
{
HttpResponse resp;
string host = req.Host.Split(':')[0];
Expand All @@ -415,8 +448,9 @@ private HttpResponse HomePage(HttpRequest req, bool tls)
// tiriryarai welcome page with info.
resp = DefaultHttpResponse(301, req);
}
else if (prms.Authenticate && !req.BasicAuthenticated(prms.Username, prms.Password))
else if (prms.Authenticate && !req.BasicAuthenticated("Authorization", prms.Username, prms.Password))
{
cache.GetIPStatistics(client).LoginAttempt();
resp = DefaultHttpResponse(401, req);
}
// From here on, the client is authenticated to access the plugin page
Expand All @@ -439,8 +473,9 @@ private HttpResponse HomePage(HttpRequest req, bool tls)
// default welcome page with info.
resp = DefaultHttpResponse(301, req);
}
else if (prms.Authenticate && !req.BasicAuthenticated(prms.Username, prms.Password))
else if (prms.Authenticate && !req.BasicAuthenticated("Authorization", prms.Username, prms.Password))
{
cache.GetIPStatistics(client).LoginAttempt();
resp = DefaultHttpResponse(401, req);
}
// From here on, the client is authenticated to access configuration pages
Expand Down Expand Up @@ -480,12 +515,21 @@ private HttpResponse DefaultHttpResponse(int status, HttpRequest req)
body = Resources.BAD_PAGE;
break;
case 401:
resp.SetHeader("WWW-Authenticate", "Basic realm=\"Access to admin pages\"");
resp.SetHeader("WWW-Authenticate",
"Basic realm=\"Access to admin pages. This is sent securely over HTTPS.\"");
body = Resources.AUTH_PAGE;
break;
case 403:
body = Resources.FORBIDDEN_PAGE;
break;
case 404:
body = Resources.NON_PAGE;
break;
case 407:
resp.SetHeader("Proxy-Authenticate",
"Basic realm=\"Use of the proxy server. This is sent insecurely over HTTP.\"");
body = Resources.PROXY_PAGE;
break;
case 500:
body = Resources.ERR_PAGE;
break;
Expand Down
1 change: 1 addition & 0 deletions Tiriryarai/Tiriryarai.projitems
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
<Compile Include="$(MSBuildThisFileDirectory)Util\HttpsMitmProxyParams.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Http\HttpChunkStream.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Program.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Util\IpClientStats.cs" />
</ItemGroup>
<ItemGroup>
<Folder Include="$(MSBuildThisFileDirectory)Security\" />
Expand Down
15 changes: 12 additions & 3 deletions Tiriryarai/Util/HttpsMitmProxyCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
namespace Tiriryarai.Util
{
/// <summary>
/// A class used for caching certificates, OCSP responses, and CRLs used by
/// A class used for caching certificates, OCSP responses, and other data used by
/// man-in-the-middle HTTPS proxies. The content will be cached and removed automatically
/// when expired. The cache is self-mantaining in that it shrinks itself if it
/// gets too large. The root CA and OCSP CA certificates will be stored on disk instead
Expand Down Expand Up @@ -221,8 +221,6 @@ public X509OCSPResponse GetOCSPResponse(HttpRequest req)
req.Body : Convert.FromBase64String(HttpUtility.UrlDecode(req.SubPath(1)));

X509OCSPRequest ocspReq = new X509OCSPRequest(rawOcspReq);
// TODO X509OCSPCertID has no equals method, so is there any way for the cache to tell if an OCSP response
// is already present?
return AddOrGetExisting(ocspReq.CertificateID, CreateOCSPResponse, val => (
val as X509OCSPResponse).ExpiryDate
) as X509OCSPResponse;
Expand All @@ -236,6 +234,17 @@ public X509OCSPResponse GetOCSPResponse(HttpRequest req)
);
}

/// <summary>
/// Gets the IP client statistics of the given IP.
/// </summary>
/// <returns>The IP client statistics.</returns>
/// <param name="req">The IP whose client statistics to obtain.</param>
public IpClientStats GetIPStatistics(IPAddress ip)
{
return AddOrGetExisting("$" + ip, val => new IpClientStats(), val => DateTime.Now.AddDays(14)
) as IpClientStats;
}

private static PKCS12 SaveToPKCS12(string path, X509CertificateBuilder cb, AsymmetricAlgorithm key)
{
PKCS12 p12 = new PKCS12();
Expand Down
Loading

0 comments on commit d60cf9b

Please sign in to comment.