Specialized struct generics rather than interface (#1640)
Changed the IHttpParser interface to be generic. This lets use a struct to get better code generation and also should allow us to inline calls back into the handler from the parser.
This commit is contained in:
parent
1faaefef30
commit
d29e4d4cf0
|
|
@ -83,7 +83,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Adapter.Internal
|
|||
return WriteAsync(default(ArraySegment<byte>), chunk: false, cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
public void Write<T>(Action<WritableBuffer, T> callback, T state)
|
||||
public void Write<T>(Action<WritableBuffer, T> callback, T state) where T : struct
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ using Microsoft.Extensions.Primitives;
|
|||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||
{
|
||||
public abstract partial class Frame : IFrameControl, IHttpRequestLineHandler, IHttpHeadersHandler
|
||||
public abstract partial class Frame : IFrameControl
|
||||
{
|
||||
private const byte ByteAsterisk = (byte)'*';
|
||||
private const byte ByteForwardSlash = (byte)'/';
|
||||
|
|
@ -34,7 +34,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
|||
|
||||
private static readonly ArraySegment<byte> _endChunkedResponseBytes = CreateAsciiByteArraySegment("0\r\n\r\n");
|
||||
private static readonly ArraySegment<byte> _continueBytes = CreateAsciiByteArraySegment("HTTP/1.1 100 Continue\r\n\r\n");
|
||||
private static readonly Action<WritableBuffer, Frame> _writeHeaders = WriteResponseHeaders;
|
||||
private static readonly Action<WritableBuffer, FrameAdapter> _writeHeaders = WriteResponseHeaders;
|
||||
|
||||
private static readonly byte[] _bytesConnectionClose = Encoding.ASCII.GetBytes("\r\nConnection: close");
|
||||
private static readonly byte[] _bytesConnectionKeepAlive = Encoding.ASCII.GetBytes("\r\nConnection: keep-alive");
|
||||
|
|
@ -80,7 +80,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
|||
protected long _responseBytesWritten;
|
||||
|
||||
private readonly FrameContext _frameContext;
|
||||
private readonly IHttpParser _parser;
|
||||
private readonly IHttpParser<FrameAdapter> _parser;
|
||||
|
||||
public Frame(FrameContext frameContext)
|
||||
{
|
||||
|
|
@ -88,7 +88,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
|||
|
||||
ServerOptions = ServiceContext.ServerOptions;
|
||||
|
||||
_parser = ServiceContext.HttpParserFactory(this);
|
||||
_parser = ServiceContext.HttpParserFactory(new FrameAdapter(this));
|
||||
|
||||
FrameControl = this;
|
||||
_keepAliveMilliseconds = (long)ServerOptions.Limits.KeepAliveTimeout.TotalMilliseconds;
|
||||
|
|
@ -988,11 +988,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
|||
responseHeaders.SetRawDate(dateHeaderValues.String, dateHeaderValues.Bytes);
|
||||
}
|
||||
|
||||
Output.Write(_writeHeaders, this);
|
||||
Output.Write(_writeHeaders, new FrameAdapter(this));
|
||||
}
|
||||
|
||||
private static void WriteResponseHeaders(WritableBuffer writableBuffer, Frame frame)
|
||||
private static void WriteResponseHeaders(WritableBuffer writableBuffer, FrameAdapter frameAdapter)
|
||||
{
|
||||
var frame = frameAdapter.Frame;
|
||||
var writer = new WritableBufferWriter(writableBuffer);
|
||||
|
||||
var responseHeaders = frame.FrameResponseHeaders;
|
||||
|
|
@ -1050,7 +1051,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
|||
overLength = true;
|
||||
}
|
||||
|
||||
var result = _parser.ParseRequestLine(this, buffer, out consumed, out examined);
|
||||
var result = _parser.ParseRequestLine(new FrameAdapter(this), buffer, out consumed, out examined);
|
||||
if (!result && overLength)
|
||||
{
|
||||
RejectRequest(RequestRejectionReason.RequestLineTooLong);
|
||||
|
|
@ -1072,7 +1073,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
|||
overLength = true;
|
||||
}
|
||||
|
||||
var result = _parser.ParseHeaders(this, buffer, out consumed, out examined, out var consumedBytes);
|
||||
var result = _parser.ParseHeaders(new FrameAdapter(this), buffer, out consumed, out examined, out var consumedBytes);
|
||||
_remainingRequestHeadersBytesAllowed -= consumedBytes;
|
||||
|
||||
if (!result && overLength)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Internal.System;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||
{
|
||||
public struct FrameAdapter : IHttpRequestLineHandler, IHttpHeadersHandler
|
||||
{
|
||||
public Frame Frame;
|
||||
|
||||
public FrameAdapter(Frame frame)
|
||||
{
|
||||
Frame = frame;
|
||||
}
|
||||
|
||||
public void OnHeader(Span<byte> name, Span<byte> value)
|
||||
=> Frame.OnHeader(name, value);
|
||||
|
||||
public void OnStartLine(HttpMethod method, HttpVersion version, Span<byte> target, Span<byte> path, Span<byte> query, Span<byte> customMethod, bool pathEncoded)
|
||||
=> Frame.OnStartLine(method, version, target, path, query, customMethod, pathEncoded);
|
||||
}
|
||||
}
|
||||
|
|
@ -9,7 +9,7 @@ using Microsoft.Extensions.Logging;
|
|||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||
{
|
||||
public class HttpParser : IHttpParser
|
||||
public class HttpParser<TRequestHandler> : IHttpParser<TRequestHandler> where TRequestHandler : IHttpHeadersHandler, IHttpRequestLineHandler
|
||||
{
|
||||
public HttpParser(IKestrelTrace log)
|
||||
{
|
||||
|
|
@ -27,7 +27,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
|||
private const byte ByteQuestionMark = (byte)'?';
|
||||
private const byte BytePercentage = (byte)'%';
|
||||
|
||||
public unsafe bool ParseRequestLine(IHttpRequestLineHandler handler, ReadableBuffer buffer, out ReadCursor consumed, out ReadCursor examined)
|
||||
public unsafe bool ParseRequestLine(TRequestHandler handler, ReadableBuffer buffer, out ReadCursor consumed, out ReadCursor examined)
|
||||
{
|
||||
consumed = buffer.Start;
|
||||
examined = buffer.End;
|
||||
|
|
@ -66,7 +66,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
|||
return true;
|
||||
}
|
||||
|
||||
private unsafe void ParseRequestLine(IHttpRequestLineHandler handler, byte* data, int length)
|
||||
private unsafe void ParseRequestLine(TRequestHandler handler, byte* data, int length)
|
||||
{
|
||||
int offset;
|
||||
Span<byte> customMethod = default(Span<byte>);
|
||||
|
|
@ -183,7 +183,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
|||
handler.OnStartLine(method, httpVersion, targetBuffer, pathBuffer, query, customMethod, pathEncoded);
|
||||
}
|
||||
|
||||
public unsafe bool ParseHeaders(IHttpHeadersHandler handler, ReadableBuffer buffer, out ReadCursor consumed, out ReadCursor examined, out int consumedBytes)
|
||||
public unsafe bool ParseHeaders(TRequestHandler handler, ReadableBuffer buffer, out ReadCursor consumed, out ReadCursor examined, out int consumedBytes)
|
||||
{
|
||||
consumed = buffer.Start;
|
||||
examined = buffer.End;
|
||||
|
|
@ -346,7 +346,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
|||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private unsafe void TakeSingleHeader(byte* headerLine, int length, IHttpHeadersHandler handler)
|
||||
private unsafe void TakeSingleHeader(byte* headerLine, int length, TRequestHandler handler)
|
||||
{
|
||||
// Skip CR, LF from end position
|
||||
var valueEnd = length - 3;
|
||||
|
|
|
|||
|
|
@ -5,11 +5,11 @@ using Microsoft.AspNetCore.Server.Kestrel.Internal.System.IO.Pipelines;
|
|||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||
{
|
||||
public interface IHttpParser
|
||||
public interface IHttpParser<TRequestHandler> where TRequestHandler : IHttpHeadersHandler, IHttpRequestLineHandler
|
||||
{
|
||||
bool ParseRequestLine(IHttpRequestLineHandler handler, ReadableBuffer buffer, out ReadCursor consumed, out ReadCursor examined);
|
||||
bool ParseRequestLine(TRequestHandler handler, ReadableBuffer buffer, out ReadCursor consumed, out ReadCursor examined);
|
||||
|
||||
bool ParseHeaders(IHttpHeadersHandler handler, ReadableBuffer buffer, out ReadCursor consumed, out ReadCursor examined, out int consumedBytes);
|
||||
bool ParseHeaders(TRequestHandler handler, ReadableBuffer buffer, out ReadCursor consumed, out ReadCursor examined, out int consumedBytes);
|
||||
|
||||
void Reset();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
|||
Task WriteAsync(ArraySegment<byte> buffer, bool chunk = false, CancellationToken cancellationToken = default(CancellationToken));
|
||||
void Flush();
|
||||
Task FlushAsync(CancellationToken cancellationToken = default(CancellationToken));
|
||||
void Write<T>(Action<WritableBuffer, T> write, T state);
|
||||
void Write<T>(Action<WritableBuffer, T> write, T state) where T : struct;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
|
|||
|
||||
public IThreadPool ThreadPool { get; set; }
|
||||
|
||||
public Func<Frame, IHttpParser> HttpParserFactory { get; set; }
|
||||
public Func<FrameAdapter, IHttpParser<FrameAdapter>> HttpParserFactory { get; set; }
|
||||
|
||||
public DateHeaderValueManager DateHeaderValueManager { get; set; }
|
||||
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
|
|||
var serviceContext = new ServiceContext
|
||||
{
|
||||
Log = trace,
|
||||
HttpParserFactory = frame => new HttpParser(frame.ServiceContext.Log),
|
||||
HttpParserFactory = frameParser => new HttpParser<FrameAdapter>(frameParser.Frame.ServiceContext.Log),
|
||||
ThreadPool = threadPool,
|
||||
DateHeaderValueManager = _dateHeaderValueManager,
|
||||
ServerOptions = Options
|
||||
|
|
|
|||
|
|
@ -256,28 +256,5 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
"42,000",
|
||||
"42.000",
|
||||
};
|
||||
|
||||
private class NoopHttpParser : IHttpParser
|
||||
{
|
||||
public bool ParseHeaders(IHttpHeadersHandler handler, ReadableBuffer buffer, out ReadCursor consumed, out ReadCursor examined, out int consumedBytes)
|
||||
{
|
||||
consumed = buffer.Start;
|
||||
examined = buffer.End;
|
||||
consumedBytes = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool ParseRequestLine(IHttpRequestLineHandler handler, ReadableBuffer buffer, out ReadCursor consumed, out ReadCursor examined)
|
||||
{
|
||||
consumed = buffer.Start;
|
||||
examined = buffer.End;
|
||||
return false;
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,6 @@ using System.Collections.Generic;
|
|||
using System.Linq;
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Internal.System;
|
||||
|
|
@ -418,7 +417,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
Assert.Equal(buffer.End, examined);
|
||||
}
|
||||
|
||||
private IHttpParser CreateParser(IKestrelTrace log) => new HttpParser(log);
|
||||
private IHttpParser<RequestHandler> CreateParser(IKestrelTrace log) => new HttpParser<RequestHandler>(log);
|
||||
|
||||
public static IEnumerable<string[]> RequestLineValidData => HttpParsingData.RequestLineValidData;
|
||||
|
||||
|
|
|
|||
|
|
@ -38,11 +38,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
|
||||
var called = false;
|
||||
|
||||
((ISocketOutput)socketOutput).Write<object>((buffer, state) =>
|
||||
((ISocketOutput)socketOutput).Write((buffer, state) =>
|
||||
{
|
||||
called = true;
|
||||
},
|
||||
null);
|
||||
0);
|
||||
|
||||
Assert.False(called);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance
|
|||
{
|
||||
var serviceContext = new ServiceContext
|
||||
{
|
||||
HttpParserFactory = _ => NullParser.Instance,
|
||||
HttpParserFactory = _ => NullParser<FrameAdapter>.Instance,
|
||||
ServerOptions = new KestrelServerOptions()
|
||||
};
|
||||
var frameContext = new FrameContext
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance
|
|||
{
|
||||
var serviceContext = new ServiceContext
|
||||
{
|
||||
HttpParserFactory = _ => NullParser.Instance,
|
||||
HttpParserFactory = _ => NullParser<FrameAdapter>.Instance,
|
||||
ServerOptions = new KestrelServerOptions()
|
||||
};
|
||||
var frameContext = new FrameContext
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance
|
|||
DateHeaderValueManager = new DateHeaderValueManager(),
|
||||
ServerOptions = new KestrelServerOptions(),
|
||||
Log = new MockTrace(),
|
||||
HttpParserFactory = f => new HttpParser(log: null)
|
||||
HttpParserFactory = f => new HttpParser<FrameAdapter>(log: null)
|
||||
};
|
||||
var frameContext = new FrameContext
|
||||
{
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance
|
|||
|
||||
public class KestrelHttpParserBenchmark : IHttpRequestLineHandler, IHttpHeadersHandler
|
||||
{
|
||||
private readonly HttpParser _parser = new HttpParser(log: null);
|
||||
private readonly HttpParser<Adapter> _parser = new HttpParser<Adapter>(log: null);
|
||||
|
||||
private ReadableBuffer _buffer;
|
||||
|
||||
|
|
@ -53,14 +53,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance
|
|||
|
||||
private void ParseData()
|
||||
{
|
||||
if (!_parser.ParseRequestLine(this, _buffer, out var consumed, out var examined))
|
||||
if (!_parser.ParseRequestLine(new Adapter(this), _buffer, out var consumed, out var examined))
|
||||
{
|
||||
ErrorUtilities.ThrowInvalidRequestHeaders();
|
||||
}
|
||||
|
||||
_buffer = _buffer.Slice(consumed, _buffer.End);
|
||||
|
||||
if (!_parser.ParseHeaders(this, _buffer, out consumed, out examined, out var consumedBytes))
|
||||
if (!_parser.ParseHeaders(new Adapter(this), _buffer, out consumed, out examined, out var consumedBytes))
|
||||
{
|
||||
ErrorUtilities.ThrowInvalidRequestHeaders();
|
||||
}
|
||||
|
|
@ -73,5 +73,21 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance
|
|||
public void OnHeader(Span<byte> name, Span<byte> value)
|
||||
{
|
||||
}
|
||||
|
||||
private struct Adapter : IHttpRequestLineHandler, IHttpHeadersHandler
|
||||
{
|
||||
public KestrelHttpParserBenchmark RequestHandler;
|
||||
|
||||
public Adapter(KestrelHttpParserBenchmark requestHandler)
|
||||
{
|
||||
RequestHandler = requestHandler;
|
||||
}
|
||||
|
||||
public void OnHeader(Span<byte> name, Span<byte> value)
|
||||
=> RequestHandler.OnHeader(name, value);
|
||||
|
||||
public void OnStartLine(HttpMethod method, HttpVersion version, Span<byte> target, Span<byte> path, Span<byte> query, Span<byte> customMethod, bool pathEncoded)
|
||||
=> RequestHandler.OnStartLine(method, version, target, path, query, customMethod, pathEncoded);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ using Microsoft.AspNetCore.Server.Kestrel.Internal.System.IO.Pipelines;
|
|||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Performance
|
||||
{
|
||||
public class NullParser : IHttpParser
|
||||
public class NullParser<TRequestHandler> : IHttpParser<TRequestHandler> where TRequestHandler : struct, IHttpHeadersHandler, IHttpRequestLineHandler
|
||||
{
|
||||
private readonly byte[] _startLine = Encoding.ASCII.GetBytes("GET /plaintext HTTP/1.1\r\n");
|
||||
private readonly byte[] _target = Encoding.ASCII.GetBytes("/plaintext");
|
||||
|
|
@ -19,9 +19,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance
|
|||
private readonly byte[] _connectionHeaderName = Encoding.ASCII.GetBytes("Connection");
|
||||
private readonly byte[] _connectionHeaderValue = Encoding.ASCII.GetBytes("keep-alive");
|
||||
|
||||
public static readonly NullParser Instance = new NullParser();
|
||||
public static readonly NullParser<FrameAdapter> Instance = new NullParser<FrameAdapter>();
|
||||
|
||||
public bool ParseHeaders(IHttpHeadersHandler handler, ReadableBuffer buffer, out ReadCursor consumed, out ReadCursor examined, out int consumedBytes)
|
||||
public bool ParseHeaders(TRequestHandler handler, ReadableBuffer buffer, out ReadCursor consumed, out ReadCursor examined, out int consumedBytes)
|
||||
{
|
||||
handler.OnHeader(new Span<byte>(_hostHeaderName), new Span<byte>(_hostHeaderValue));
|
||||
handler.OnHeader(new Span<byte>(_acceptHeaderName), new Span<byte>(_acceptHeaderValue));
|
||||
|
|
@ -34,7 +34,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance
|
|||
return true;
|
||||
}
|
||||
|
||||
public bool ParseRequestLine(IHttpRequestLineHandler handler, ReadableBuffer buffer, out ReadCursor consumed, out ReadCursor examined)
|
||||
public bool ParseRequestLine(TRequestHandler handler, ReadableBuffer buffer, out ReadCursor consumed, out ReadCursor examined)
|
||||
{
|
||||
handler.OnStartLine(HttpMethod.Get,
|
||||
HttpVersion.Http11,
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance
|
|||
{
|
||||
var serviceContext = new ServiceContext
|
||||
{
|
||||
HttpParserFactory = f => new HttpParser(f.ServiceContext.Log),
|
||||
HttpParserFactory = f => new HttpParser<FrameAdapter>(f.Frame.ServiceContext.Log),
|
||||
ServerOptions = new KestrelServerOptions()
|
||||
};
|
||||
var frameContext = new FrameContext
|
||||
|
|
|
|||
|
|
@ -170,7 +170,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance
|
|||
{
|
||||
var serviceContext = new ServiceContext
|
||||
{
|
||||
HttpParserFactory = f => new HttpParser(f.ServiceContext.Log),
|
||||
HttpParserFactory = f => new HttpParser<FrameAdapter>(f.Frame.ServiceContext.Log),
|
||||
ServerOptions = new KestrelServerOptions()
|
||||
};
|
||||
var frameContext = new FrameContext
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance
|
|||
DateHeaderValueManager = new DateHeaderValueManager(),
|
||||
ServerOptions = new KestrelServerOptions(),
|
||||
Log = new MockTrace(),
|
||||
HttpParserFactory = f => new HttpParser(log: null)
|
||||
HttpParserFactory = f => new HttpParser<FrameAdapter>(log: null)
|
||||
};
|
||||
|
||||
var frameContext = new FrameContext
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ namespace Microsoft.AspNetCore.Testing
|
|||
return TaskCache.CompletedTask;
|
||||
}
|
||||
|
||||
public void Write<T>(Action<WritableBuffer, T> write, T state)
|
||||
public void Write<T>(Action<WritableBuffer, T> write, T state) where T : struct
|
||||
{
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ namespace Microsoft.AspNetCore.Testing
|
|||
ThreadPool = new LoggingThreadPool(Log);
|
||||
DateHeaderValueManager = new DateHeaderValueManager(systemClock: new MockSystemClock());
|
||||
DateHeaderValue = DateHeaderValueManager.GetDateHeaderValues().String;
|
||||
HttpParserFactory = frame => new HttpParser(frame.ServiceContext.Log);
|
||||
HttpParserFactory = frameAdapter => new HttpParser<FrameAdapter>(frameAdapter.Frame.ServiceContext.Log);
|
||||
ServerOptions = new KestrelServerOptions
|
||||
{
|
||||
AddServerHeader = false
|
||||
|
|
|
|||
Loading…
Reference in New Issue