Commit 1494cb4f authored by justcoding121's avatar justcoding121 Committed by justcoding121

Merge pull request #220 from honfika/develop

Allow to change the root certificate names (issuer, name) in Certific…
parents 07b66dbd de0f7a7e
using System;
using System.Collections.Generic;
using System.Net;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;
using Titanium.Web.Proxy.EventArguments;
using Titanium.Web.Proxy.Models;
......@@ -18,6 +17,7 @@ namespace Titanium.Web.Proxy.Examples.Basic
public ProxyTestController()
{
proxyServer = new ProxyServer();
proxyServer.ExceptionFunc = exception => Console.WriteLine(exception.Message);
proxyServer.TrustRootCertificate = true;
//optionally set the Certificate Engine
......@@ -99,7 +99,7 @@ namespace Titanium.Web.Proxy.Examples.Basic
{
Console.WriteLine(e.WebSession.Request.Url);
////read request headers
//read request headers
var requestHeaders = e.WebSession.Request.RequestHeaders;
var method = e.WebSession.Request.Method.ToUpper();
......@@ -128,6 +128,7 @@ namespace Titanium.Web.Proxy.Examples.Basic
"</body>" +
"</html>");
}
//Redirect example
if (e.WebSession.Request.RequestUri.AbsoluteUri.Contains("wikipedia.org"))
{
......@@ -143,6 +144,7 @@ namespace Titanium.Web.Proxy.Examples.Basic
//access request body by looking up the shared dictionary using requestId
var requestBody = requestBodyHistory[e.Id];
}
//read response headers
var responseHeaders = e.WebSession.Response.ResponseHeaders;
......
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Net;
using System.Collections.Generic;
using System.Threading.Tasks;
using Titanium.Web.Proxy.EventArguments;
using Titanium.Web.Proxy.Models;
......@@ -28,14 +27,13 @@ namespace Titanium.Web.Proxy.IntegrationTests
{
var response = client.GetAsync(new Uri(testUrl)).Result;
}
}
private HttpClient CreateHttpClient(string url, int localProxyPort)
{
var handler = new HttpClientHandler
{
Proxy = new WebProxy(string.Format("http://localhost:{0}", localProxyPort), false),
Proxy = new WebProxy($"http://localhost:{localProxyPort}", false),
UseProxy = true,
};
......@@ -47,7 +45,7 @@ namespace Titanium.Web.Proxy.IntegrationTests
public class ProxyTestController
{
private ProxyServer proxyServer;
private readonly ProxyServer proxyServer;
public ProxyTestController()
{
......@@ -63,7 +61,6 @@ namespace Titanium.Web.Proxy.IntegrationTests
proxyServer.ClientCertificateSelectionCallback += OnCertificateSelection;
var explicitEndPoint = new ExplicitProxyEndPoint(IPAddress.Any, proxyPort, true);
//An explicit endpoint is where the client knows about the existance of a proxy
//So client sends request in a proxy friendly manner
......@@ -73,7 +70,6 @@ namespace Titanium.Web.Proxy.IntegrationTests
foreach (var endPoint in proxyServer.ProxyEndPoints)
Console.WriteLine("Listening on '{0}' endpoint at Ip {1} and port: {2} ",
endPoint.GetType().Name, endPoint.IpAddress, endPoint.Port);
}
public void Stop()
......
......@@ -20,8 +20,7 @@ namespace Titanium.Web.Proxy.UnitTests
{
var tasks = new List<Task>();
var mgr = new CertificateManager(CertificateEngine.DefaultWindows, "Titanium", "Titanium Root Certificate Authority",
new Lazy<Action<Exception>>(() => (e => { })).Value);
var mgr = new CertificateManager(new Lazy<Action<Exception>>(() => (e => { })).Value);
mgr.ClearIdleCertificates(1);
......
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=BC/@EntryIndexedValue">BC</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateConstants/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateInstanceFields/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateStaticFields/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateStaticReadonly/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/UserRules/=dda2ffa1_002D435c_002D4111_002D88eb_002D1a7c93c382f0/@EntryIndexedValue">&lt;Policy&gt;&lt;Descriptor Staticness="Static, Instance" AccessRightKinds="Private" Description="Property (private)"&gt;&lt;ElementKinds&gt;&lt;Kind Name="PROPERTY" /&gt;&lt;/ElementKinds&gt;&lt;/Descriptor&gt;&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /&gt;&lt;/Policy&gt;</s:String></wpf:ResourceDictionary>
\ No newline at end of file
......@@ -21,7 +21,7 @@ namespace Titanium.Web.Proxy.Decompression
int read;
while ((read = await decompressor.ReadAsync(buffer, 0, buffer.Length)) > 0)
{
await output.WriteAsync(buffer, 0, read);
output.Write(buffer, 0, read);
}
return output.ToArray();
......
......@@ -19,8 +19,9 @@ namespace Titanium.Web.Proxy.Decompression
int read;
while ((read = await decompressor.ReadAsync(buffer, 0, buffer.Length)) > 0)
{
await output.WriteAsync(buffer, 0, read);
output.Write(buffer, 0, read);
}
return output.ToArray();
}
}
......
......@@ -46,7 +46,7 @@ namespace Titanium.Web.Proxy.Extensions
while (totalbytesRead < totalBytesToRead)
{
var buffer = await streamReader.ReadBytesAsync(bufferSize, bytesToRead);
var buffer = await streamReader.ReadBytesAsync(bytesToRead);
if (buffer.Length == 0)
{
......@@ -81,7 +81,7 @@ namespace Titanium.Web.Proxy.Extensions
if (chunkSize != 0)
{
var buffer = await clientStreamReader.ReadBytesAsync(bufferSize, chunkSize);
var buffer = await clientStreamReader.ReadBytesAsync(chunkSize);
await stream.WriteAsync(buffer, 0, buffer.Length);
//chunk trail
await clientStreamReader.ReadLineAsync();
......@@ -179,7 +179,7 @@ namespace Titanium.Web.Proxy.Extensions
if (chunkSize != 0)
{
var buffer = await inStreamReader.ReadBytesAsync(bufferSize, chunkSize);
var buffer = await inStreamReader.ReadBytesAsync(chunkSize);
var chunkHeadBytes = Encoding.ASCII.GetBytes(chunkSize.ToString("x2"));
......
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Globalization;
namespace Titanium.Web.Proxy.Extensions
{
......
......@@ -19,18 +19,18 @@ namespace Titanium.Web.Proxy.Helpers
private readonly Encoding encoding;
[ThreadStatic]
private static byte[] staticBuffer;
private static byte[] staticBufferField;
private byte[] buffer
private byte[] staticBuffer
{
get
{
if (staticBuffer == null || staticBuffer.Length != bufferSize)
if (staticBufferField == null || staticBufferField.Length != bufferSize)
{
staticBuffer = new byte[bufferSize];
staticBufferField = new byte[bufferSize];
}
return staticBuffer;
return staticBufferField;
}
}
......@@ -56,7 +56,7 @@ namespace Titanium.Web.Proxy.Helpers
int bufferDataLength = 0;
// try to use the thread static buffer, usually it is enough
var buffer = this.buffer;
var buffer = staticBuffer;
while (stream.DataAvailable || await stream.FillBufferAsync())
{
......@@ -117,22 +117,16 @@ namespace Titanium.Web.Proxy.Helpers
/// <summary>
/// Read the specified number of raw bytes from the base stream
/// </summary>
/// <param name="bufferSize"></param>
/// <param name="totalBytesToRead"></param>
/// <returns></returns>
internal async Task<byte[]> ReadBytesAsync(int bufferSize, long totalBytesToRead)
internal async Task<byte[]> ReadBytesAsync(long totalBytesToRead)
{
int bytesToRead = bufferSize;
if (totalBytesToRead < bufferSize)
bytesToRead = (int)totalBytesToRead;
var buffer = this.buffer;
if (bytesToRead > buffer.Length)
{
buffer = new byte[bytesToRead];
}
var buffer = staticBuffer;
int bytesRead;
var totalBytesRead = 0;
......
......@@ -16,7 +16,7 @@ namespace Titanium.Web.Proxy.Helpers
{
private readonly Stream baseStream;
private readonly byte[] buffer;
private readonly byte[] streamBuffer;
private int bufferLength;
......@@ -30,7 +30,7 @@ namespace Titanium.Web.Proxy.Helpers
public CustomBufferedStream(Stream baseStream, int bufferSize)
{
this.baseStream = baseStream;
buffer = new byte[bufferSize];
streamBuffer = new byte[bufferSize];
}
/// <summary>
......@@ -84,7 +84,7 @@ namespace Titanium.Web.Proxy.Helpers
int available = Math.Min(bufferLength, count);
if (available > 0)
{
Buffer.BlockCopy(this.buffer, bufferPos, buffer, offset, available);
Buffer.BlockCopy(streamBuffer, bufferPos, buffer, offset, available);
bufferPos += available;
bufferLength -= available;
}
......@@ -120,7 +120,7 @@ namespace Titanium.Web.Proxy.Helpers
if (bufferLength > 0)
{
int available = Math.Min(bufferLength, count);
Buffer.BlockCopy(this.buffer, bufferPos, buffer, offset, available);
Buffer.BlockCopy(streamBuffer, bufferPos, buffer, offset, available);
bufferPos += available;
bufferLength -= available;
return new ReadAsyncResult(available);
......@@ -167,7 +167,7 @@ namespace Titanium.Web.Proxy.Helpers
{
if (bufferLength > 0)
{
await destination.WriteAsync(buffer, bufferPos, bufferLength, cancellationToken);
await destination.WriteAsync(streamBuffer, bufferPos, bufferLength, cancellationToken);
}
await baseStream.CopyToAsync(destination, bufferSize, cancellationToken);
......@@ -256,7 +256,7 @@ namespace Titanium.Web.Proxy.Helpers
int available = Math.Min(bufferLength, count);
if (available > 0)
{
Buffer.BlockCopy(this.buffer, bufferPos, buffer, offset, available);
Buffer.BlockCopy(streamBuffer, bufferPos, buffer, offset, available);
bufferPos += available;
bufferLength -= available;
}
......@@ -283,7 +283,7 @@ namespace Titanium.Web.Proxy.Helpers
}
bufferLength--;
return buffer[bufferPos++];
return streamBuffer[bufferPos++];
}
public byte ReadByteFromBuffer()
......@@ -294,7 +294,7 @@ namespace Titanium.Web.Proxy.Helpers
}
bufferLength--;
return buffer[bufferPos++];
return streamBuffer[bufferPos++];
}
/// <summary>
......@@ -390,7 +390,7 @@ namespace Titanium.Web.Proxy.Helpers
/// </summary>
public bool FillBuffer()
{
bufferLength = baseStream.Read(buffer, 0, buffer.Length);
bufferLength = baseStream.Read(streamBuffer, 0, streamBuffer.Length);
bufferPos = 0;
return bufferLength > 0;
}
......@@ -411,7 +411,7 @@ namespace Titanium.Web.Proxy.Helpers
/// <returns></returns>
public async Task<bool> FillBufferAsync(CancellationToken cancellationToken)
{
bufferLength = await baseStream.ReadAsync(buffer, 0, buffer.Length, cancellationToken);
bufferLength = await baseStream.ReadAsync(streamBuffer, 0, streamBuffer.Length, cancellationToken);
bufferPos = 0;
return bufferLength > 0;
}
......
......@@ -55,7 +55,7 @@ namespace Titanium.Web.Proxy.Helpers
{
localhost = Dns.GetHostEntry(Dns.GetHostName());
IPAddress ipAddress = null;
IPAddress ipAddress;
if (IPAddress.TryParse(hostName, out ipAddress))
isLocalhost = localhost.AddressList.Any(x => x.Equals(ipAddress));
......
......@@ -21,9 +21,9 @@ namespace Titanium.Web.Proxy.Models
/// <param name="enableSsl"></param>
protected ProxyEndPoint(IPAddress ipAddress, int port, bool enableSsl)
{
this.IpAddress = ipAddress;
this.Port = port;
this.EnableSsl = enableSsl;
IpAddress = ipAddress;
Port = port;
EnableSsl = enableSsl;
}
/// <summary>
......@@ -55,8 +55,8 @@ namespace Titanium.Web.Proxy.Models
/// </summary>
public class ExplicitProxyEndPoint : ProxyEndPoint
{
internal List<Regex> excludedHttpsHostNameRegex;
internal List<Regex> includedHttpsHostNameRegex;
internal List<Regex> ExcludedHttpsHostNameRegexList;
internal List<Regex> IncludedHttpsHostNameRegexList;
internal bool IsSystemHttpProxy { get; set; }
......@@ -67,7 +67,7 @@ namespace Titanium.Web.Proxy.Models
/// </summary>
public IEnumerable<string> ExcludedHttpsHostNameRegex
{
get { return excludedHttpsHostNameRegex?.Select(x => x.ToString()).ToList(); }
get { return ExcludedHttpsHostNameRegexList?.Select(x => x.ToString()).ToList(); }
set
{
if (IncludedHttpsHostNameRegex != null)
......@@ -75,7 +75,7 @@ namespace Titanium.Web.Proxy.Models
throw new ArgumentException("Cannot set excluded when included is set");
}
excludedHttpsHostNameRegex = value?.Select(x=>new Regex(x, RegexOptions.Compiled)).ToList();
ExcludedHttpsHostNameRegexList = value?.Select(x=>new Regex(x, RegexOptions.Compiled)).ToList();
}
}
......@@ -84,7 +84,7 @@ namespace Titanium.Web.Proxy.Models
/// </summary>
public IEnumerable<string> IncludedHttpsHostNameRegex
{
get { return includedHttpsHostNameRegex?.Select(x => x.ToString()).ToList(); }
get { return IncludedHttpsHostNameRegexList?.Select(x => x.ToString()).ToList(); }
set
{
if (ExcludedHttpsHostNameRegex != null)
......@@ -92,7 +92,7 @@ namespace Titanium.Web.Proxy.Models
throw new ArgumentException("Cannot set included when excluded is set");
}
includedHttpsHostNameRegex = value?.Select(x => new Regex(x, RegexOptions.Compiled)).ToList();
IncludedHttpsHostNameRegexList = value?.Select(x => new Regex(x, RegexOptions.Compiled)).ToList();
}
}
......@@ -136,7 +136,7 @@ namespace Titanium.Web.Proxy.Models
public TransparentProxyEndPoint(IPAddress ipAddress, int port, bool enableSsl)
: base(ipAddress, port, enableSsl)
{
this.GenericCertificateName = "localhost";
GenericCertificateName = "localhost";
}
}
......
......@@ -181,7 +181,7 @@ namespace Titanium.Web.Proxy.Network.Certificate
}
});
manualResetEvent.Wait(TimeSpan.FromMinutes(1), cancellationToken: cancellationToken);
manualResetEvent.Wait(TimeSpan.FromMinutes(1), cancellationToken);
}
return certificate;
......
......@@ -31,14 +31,48 @@ namespace Titanium.Web.Proxy.Network
/// </summary>
internal class CertificateManager : IDisposable
{
public CertificateEngine Engine
{
get { return engine; }
set
{
//For Mono only Bouncy Castle is supported
if (RunTime.IsRunningOnMono())
{
value = CertificateEngine.BouncyCastle;
}
if (value != engine)
{
certEngine = null;
engine = value;
}
if (certEngine == null)
{
certEngine = engine == CertificateEngine.BouncyCastle ?
(ICertificateMaker) new BCCertificateMaker()
: new WinCertificateMaker();
}
}
}
private const string defaultRootCertificateIssuer = "Titanium";
private const string defaultRootRootCertificateName = "Titanium Root Certificate Authority";
private readonly ICertificateMaker certEngine;
private CertificateEngine engine;
private ICertificateMaker certEngine;
private string issuer;
private string rootCertificateName;
private bool clearCertificates { get; set; }
private X509Certificate2 rootCertificate;
/// <summary>
/// Cache dictionary
/// </summary>
......@@ -46,36 +80,56 @@ namespace Titanium.Web.Proxy.Network
private readonly Action<Exception> exceptionFunc;
internal string Issuer { get; }
internal string RootCertificateName { get; }
internal X509Certificate2 RootCertificate { get; set; }
internal CertificateManager(CertificateEngine engine,
string issuer,
string rootCertificateName,
Action<Exception> exceptionFunc)
internal string Issuer
{
this.exceptionFunc = exceptionFunc;
get { return issuer ?? defaultRootCertificateIssuer; }
set
{
issuer = value;
ClearRootCertificate();
}
}
//For Mono only Bouncy Castle is supported
if (RunTime.IsRunningOnMono()
|| engine == CertificateEngine.BouncyCastle)
internal string RootCertificateName
{
get { return rootCertificateName ?? defaultRootRootCertificateName; }
set
{
certEngine = new BCCertificateMaker();
rootCertificateName = value;
ClearRootCertificate();
}
else
}
internal X509Certificate2 RootCertificate
{
get { return rootCertificate; }
set
{
certEngine = new WinCertificateMaker();
ClearRootCertificate();
rootCertificate = value;
}
}
/// <summary>
/// Is the root certificate used by this proxy is valid?
/// </summary>
internal bool CertValidated => RootCertificate != null;
Issuer = issuer ?? defaultRootCertificateIssuer;
RootCertificateName = rootCertificateName ?? defaultRootRootCertificateName;
internal CertificateManager(Action<Exception> exceptionFunc)
{
this.exceptionFunc = exceptionFunc;
Engine = CertificateEngine.DefaultWindows;
certificateCache = new ConcurrentDictionary<string, CachedCertificate>();
}
private void ClearRootCertificate()
{
certificateCache.Clear();
rootCertificate = null;
}
private string GetRootCertificatePath()
{
var assemblyLocation = System.Reflection.Assembly.GetExecutingAssembly().Location;
......@@ -150,14 +204,13 @@ namespace Titanium.Web.Proxy.Network
/// <summary>
/// Trusts the root certificate.
/// </summary>
/// <param name="exceptionFunc"></param>
internal void TrustRootCertificate(Action<Exception> exceptionFunc)
internal void TrustRootCertificate()
{
//current user
TrustRootCertificate(StoreLocation.CurrentUser, exceptionFunc);
TrustRootCertificate(StoreLocation.CurrentUser);
//current system
TrustRootCertificate(StoreLocation.LocalMachine, exceptionFunc);
TrustRootCertificate(StoreLocation.LocalMachine);
}
/// <summary>
......@@ -246,10 +299,8 @@ namespace Titanium.Web.Proxy.Network
/// Make current machine trust the Root Certificate used by this proxy
/// </summary>
/// <param name="storeLocation"></param>
/// <param name="exceptionFunc"></param>
/// <returns></returns>
internal void TrustRootCertificate(StoreLocation storeLocation,
Action<Exception> exceptionFunc)
internal void TrustRootCertificate(StoreLocation storeLocation)
{
if (RootCertificate == null)
{
......
......@@ -16,6 +16,7 @@ namespace Titanium.Web.Proxy.Network.Tcp
internal ExternalProxy UpStreamHttpsProxy { get; set; }
internal string HostName { get; set; }
internal int Port { get; set; }
internal bool IsHttps { get; set; }
......@@ -58,7 +59,6 @@ namespace Titanium.Web.Proxy.Network.Tcp
TcpClient.LingerState = new LingerOption(true, 0);
TcpClient.Close();
}
}
}
......@@ -19,12 +19,7 @@ namespace Titanium.Web.Proxy
/// </summary>
public partial class ProxyServer : IDisposable
{
/// <summary>
/// Is the root certificate used by this proxy is valid?
/// </summary>
private bool? certValidated { get; set; }
/// <summary>
/// <summary>
/// Is the proxy currently running
/// </summary>
private bool proxyRunning { get; set; }
......@@ -85,11 +80,7 @@ namespace Titanium.Web.Proxy
public string RootCertificateIssuerName
{
get { return certificateManager.Issuer; }
set
{
CreateCertificateManager(certificateManager.RootCertificateName, value);
certValidated = null;
}
set { certificateManager.RootCertificateName = value; }
}
/// <summary>
......@@ -102,11 +93,7 @@ namespace Titanium.Web.Proxy
public string RootCertificateName
{
get { return certificateManager.RootCertificateName; }
set
{
CreateCertificateManager(value, certificateManager.Issuer);
certValidated = null;
}
set { certificateManager.Issuer = value; }
}
/// <summary>
......@@ -132,7 +119,11 @@ namespace Titanium.Web.Proxy
/// Optionally set to BouncyCastle
/// Mono only support BouncyCastle and it is the default
/// </summary>
public CertificateEngine CertificateEngine { get; set; }
public CertificateEngine CertificateEngine
{
get { return certificateManager.Engine; }
set { certificateManager.Engine = value; }
}
/// <summary>
/// Does this proxy uses the HTTP protocol 100 continue behaviour strictly?
......@@ -272,7 +263,16 @@ namespace Titanium.Web.Proxy
new FireFoxProxySettingsManager();
#endif
CreateCertificateManager(rootCertificateName, rootCertificateIssuerName);
certificateManager = new CertificateManager(ExceptionFunc);
if (rootCertificateName != null)
{
RootCertificateName = rootCertificateName;
}
if (rootCertificateIssuerName != null)
{
RootCertificateIssuerName = rootCertificateIssuerName;
}
}
/// <summary>
......@@ -369,7 +369,7 @@ namespace Titanium.Web.Proxy
EnsureRootCertificate();
//If certificate was trusted by the machine
if (certValidated == true)
if (certificateManager.CertValidated)
{
systemProxySettingsManager.SetHttpsProxy(
Equals(endPoint.IpAddress, IPAddress.Any) |
......@@ -557,21 +557,15 @@ namespace Titanium.Web.Proxy
}
private void CreateCertificateManager(string rootCertificateName, string rootCertificateIssuerName)
{
certificateManager = new CertificateManager(CertificateEngine,
rootCertificateIssuerName, rootCertificateName, ExceptionFunc);
}
private void EnsureRootCertificate()
{
if (!certValidated.HasValue)
if (!certificateManager.CertValidated)
{
certValidated = certificateManager.CreateTrustedRootCertificate();
certificateManager.CreateTrustedRootCertificate();
if (TrustRootCertificate)
{
certificateManager.TrustRootCertificate(ExceptionFunc);
certificateManager.TrustRootCertificate();
}
}
}
......
......@@ -75,12 +75,12 @@ namespace Titanium.Web.Proxy
if (endPoint.ExcludedHttpsHostNameRegex != null)
{
excluded = endPoint.excludedHttpsHostNameRegex.Any(x => x.IsMatch(httpRemoteUri.Host));
excluded = endPoint.ExcludedHttpsHostNameRegexList.Any(x => x.IsMatch(httpRemoteUri.Host));
}
if (endPoint.IncludedHttpsHostNameRegex != null)
{
excluded = !endPoint.includedHttpsHostNameRegex.Any(x => x.IsMatch(httpRemoteUri.Host));
excluded = !endPoint.IncludedHttpsHostNameRegexList.Any(x => x.IsMatch(httpRemoteUri.Host));
}
List<HttpHeader> connectRequestHeaders = null;
......
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