Commit 5bbb50e0 authored by Shani Elharrar's avatar Shani Elharrar

Added sessions and cookies handling.

parent a5204e2d
......@@ -17,11 +17,17 @@
*/
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.Net;
using System.Net.Sockets;
using uhttpsharp;
using uhttpsharp.Handlers;
using uhttpsharp.Headers;
using uhttpsharp.Listeners;
using uhttpsharp.RequestProviders;
using uhttpsharpdemo.Handlers;
namespace uhttpsharpdemo
......@@ -39,6 +45,7 @@ namespace uhttpsharpdemo
httpServer.Use(new TcpListenerAdapter(new TcpListener(IPAddress.Loopback, 82)));
//httpServer.Use(new ListenerSslDecorator(new TcpListenerAdapter(new TcpListener(IPAddress.Loopback, 443)), serverCertificate));
httpServer.Use(new SessionHandler<DateTime>(() => DateTime.Now));
httpServer.Use(new ExceptionHandler());
httpServer.Use(new TimingHandler());
......@@ -60,4 +67,68 @@ namespace uhttpsharpdemo
}
}
internal class SessionHandler<TSession> : IHttpRequestHandler
{
private readonly Func<TSession> _sessionFactory;
private static readonly Random RandomGenerator = new Random();
private class SessionHolder
{
private readonly TSession _session;
private DateTime _lastAccessTime = DateTime.Now;
public TSession Session
{
get
{
_lastAccessTime = DateTime.Now;
return _session;
}
}
public DateTime LastAccessTime
{
get
{
return _lastAccessTime;
}
}
public SessionHolder(TSession session)
{
_session = session;
}
}
private readonly ConcurrentDictionary<string, TSession> _sessions = new ConcurrentDictionary<string, TSession>();
public SessionHandler(Func<TSession> sessionFactory)
{
_sessionFactory = sessionFactory;
}
public System.Threading.Tasks.Task Handle(IHttpContext context, Func<System.Threading.Tasks.Task> next)
{
string sessId;
if (!context.Cookies.TryGetByName("SESSID", out sessId))
{
sessId = RandomGenerator.Next().ToString(CultureInfo.InvariantCulture);
context.Cookies.Upsert("SESSID", sessId);
}
var session = _sessions.GetOrAdd(sessId, CreateSession);
context.State.Session = session;
return next();
}
private TSession CreateSession(string sessionId)
{
return _sessionFactory();
}
}
}
\ No newline at end of file
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
......@@ -47,6 +48,7 @@ namespace uhttpsharp.Headers
return new QueryStringHttpHeaders(body);
}
internal int Count
{
get
......
......@@ -33,6 +33,17 @@ namespace uhttpsharp.Headers
return value;
}
public static T GetByNameOrDefault<T>(this IHttpHeaders headers, string name, T defaultValue)
{
T value;
if (headers.TryGetByName(name, out value))
{
return value;
}
return defaultValue;
}
public static string ToUriData(this IHttpHeaders headers)
{
var builder = new StringBuilder();
......
......@@ -16,10 +16,10 @@
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
using System.Text;
using log4net;
using System.Net;
using System.Reflection;
using System.Security.Cryptography.X509Certificates;
using log4net;
using System;
using System.Collections.Generic;
using System.IO;
......@@ -27,12 +27,15 @@ using System.Net.Sockets;
using System.Threading.Tasks;
using uhttpsharp.Clients;
using uhttpsharp.Headers;
using uhttpsharp.RequestProviders;
namespace uhttpsharp
{
internal sealed class HttpClient
{
private const string CrLf = "\r\n";
private static readonly byte[] CrLfBuffer = Encoding.UTF8.GetBytes(CrLf);
private static readonly ILog Logger = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
private readonly IClient _client;
......@@ -82,7 +85,23 @@ namespace uhttpsharp
if (response != null)
{
// Headers
await response.WriteHeaders(_outputStream).ConfigureAwait(false);
// Cookies
if (context.Cookies.Touched)
{
await _outputStream.WriteAsync(context.Cookies.ToCookieData())
.ConfigureAwait(false);
await _outputStream.FlushAsync().ConfigureAwait(false);
}
// Empty Line
await _outputStream.BaseStream.WriteAsync(CrLfBuffer, 0, CrLfBuffer.Length).ConfigureAwait(false);
// Body
await response.WriteResponse(_outputStream).ConfigureAwait(false);
if (!request.Headers.KeepAliveConnection() || response.CloseConnection)
{
_client.Close();
......@@ -123,25 +142,4 @@ namespace uhttpsharp
return () => _requestHandlers[index].Handle(context, BuildHandlers(context, index + 1));
}
}
internal class HttpContext : IHttpContext
{
private readonly IHttpRequest _request;
private IHttpResponse _response;
public HttpContext(IHttpRequest request)
{
_request = request;
}
public IHttpRequest Request
{
get { return _request; }
}
public IHttpResponse Response
{
get { return _response; }
set { _response = value; }
}
}
}
\ No newline at end of file
using System.Dynamic;
using uhttpsharp.Headers;
namespace uhttpsharp
{
internal class HttpContext : IHttpContext
{
private readonly IHttpRequest _request;
private readonly ICookiesStorage _cookies;
private readonly ExpandoObject _state = new ExpandoObject();
public HttpContext(IHttpRequest request)
{
_request = request;
_cookies = new CookiesStorage(_request.Headers.GetByNameOrDefault("cookie", string.Empty));
}
public IHttpRequest Request
{
get { return _request; }
}
public IHttpResponse Response { get; set; }
public ICookiesStorage Cookies
{
get { return _cookies; }
}
public dynamic State
{
get { return _state; }
}
}
}
\ No newline at end of file
......@@ -20,8 +20,6 @@ using System;
using System.CodeDom;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;
using uhttpsharp.Headers;
namespace uhttpsharp
......@@ -117,99 +115,6 @@ namespace uhttpsharp
}
public interface IHttpRequestProvider
{
/// <summary>
/// Provides an <see cref="IHttpRequest"/> based on the context of the stream,
/// May return null / throw exceptions on invalid requests.
/// </summary>
/// <param name="streamReader"></param>
/// <returns></returns>
Task<IHttpRequest> Provide(StreamReader streamReader);
}
public class HttpRequestProvider : IHttpRequestProvider
{
private static readonly char[] Separators = { '/' };
public async Task<IHttpRequest> Provide(StreamReader streamReader)
{
// parse the http request
var request = await streamReader.ReadLineAsync().ConfigureAwait(false);
if (request == null)
return null;
var tokens = request.Split(' ');
if (tokens.Length != 3)
{
return null;
}
var httpMethod = HttpMethodProvider.Default.Provide(tokens[0]);
var httpProtocol = tokens[2];
var url = tokens[1];
var queryString = GetQueryStringData(ref url);
var uri = new Uri(url, UriKind.Relative);
var headersRaw = new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase);
// get the headers
string line;
while (!string.IsNullOrEmpty((line = await streamReader.ReadLineAsync().ConfigureAwait(false))))
{
var headerKvp = SplitHeader(line);
headersRaw.Add(headerKvp.Key, headerKvp.Value);
}
IHttpHeaders headers = new HttpHeaders(headersRaw);
IHttpHeaders post = await GetPostData(streamReader, headers);
return new HttpRequest(headers, httpMethod, httpProtocol, uri,
uri.OriginalString.Split(Separators, StringSplitOptions.RemoveEmptyEntries), queryString, post);
}
private static IHttpHeaders GetQueryStringData(ref string url)
{
var queryStringIndex = url.IndexOf('?');
IHttpHeaders queryString;
if (queryStringIndex != -1)
{
queryString = new QueryStringHttpHeaders(url.Substring(queryStringIndex + 1));
url = url.Substring(0, queryStringIndex);
}
else
{
queryString = EmptyHttpHeaders.Empty;
}
return queryString;
}
private static async Task<IHttpHeaders> GetPostData(StreamReader streamReader, IHttpHeaders headers)
{
int postContentLength;
IHttpHeaders post;
if (headers.TryGetByName("content-length", out postContentLength))
{
post = await HttpHeaders.FromPost(streamReader, postContentLength);
}
else
{
post = EmptyHttpHeaders.Empty;
}
return post;
}
private KeyValuePair<string, string> SplitHeader(string header)
{
var index = header.IndexOf(": ", StringComparison.InvariantCulture);
return new KeyValuePair<string, string>(header.Substring(0, index), header.Substring(index + 2).TrimStart(' '));
}
}
public sealed class HttpRequestParameters
{
private readonly string[] _params;
......
......@@ -29,6 +29,8 @@ namespace uhttpsharp
{
Task WriteResponse(StreamWriter writer);
Task WriteHeaders(StreamWriter writer);
bool CloseConnection { get; }
}
......@@ -71,7 +73,7 @@ namespace uhttpsharp
ContentStream = contentStream;
_closeConnection = !keepAliveConnection;
WriteHeaders(new StreamWriter(_headerStream));
CacheHeaders(new StreamWriter(_headerStream));
}
public HttpResponse(HttpResponseCode code, byte[] contentStream, bool closeConnection)
: this (code, "text/html; charset=utf-8", new MemoryStream(contentStream), closeConnection)
......@@ -96,10 +98,6 @@ namespace uhttpsharp
}
public async Task WriteResponse(StreamWriter writer)
{
_headerStream.Position = 0;
await _headerStream.CopyToAsync(writer.BaseStream).ConfigureAwait(false);
ContentStream.Position = 0;
await ContentStream.CopyToAsync(writer.BaseStream).ConfigureAwait(false);
await writer.BaseStream.FlushAsync();
......@@ -110,15 +108,23 @@ namespace uhttpsharp
get { return _closeConnection; }
}
private void WriteHeaders(StreamWriter tempWriter)
private void CacheHeaders(StreamWriter tempWriter)
{
tempWriter.WriteLine("{0} {1} {2}", Protocol, (int)Code, ResponseTexts[(int)Code]);
tempWriter.WriteLine("Date: {0}", DateTime.UtcNow.ToString("R"));
tempWriter.WriteLine("Connection: {0}", _closeConnection ? "Close" : "Keep-Alive");
tempWriter.WriteLine("Content-Type: {0}", ContentType);
tempWriter.WriteLine("Content-Length: {0}", ContentStream.Length);
tempWriter.WriteLine();
//tempWriter.WriteLine();
tempWriter.Flush();
}
public async Task WriteHeaders(StreamWriter writer)
{
_headerStream.Position = 0;
await _headerStream.CopyToAsync(writer.BaseStream).ConfigureAwait(false);
}
}
}
\ No newline at end of file
......@@ -22,6 +22,7 @@ using System.Collections.Generic;
using System.Reflection;
using System.Threading.Tasks;
using uhttpsharp.Listeners;
using uhttpsharp.RequestProviders;
namespace uhttpsharp
{
......

\ No newline at end of file
using System;
using System.Collections.Generic;
using System.Dynamic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using log4net.Core;
using uhttpsharp.Headers;
namespace uhttpsharp
{
......@@ -13,7 +16,98 @@ namespace uhttpsharp
IHttpResponse Response { get; set; }
ICookiesStorage Cookies { get; }
dynamic State { get; }
}
public interface ICookiesStorage : IHttpHeaders
{
void Upsert(string key, string value);
void Remove(string key);
bool Touched { get; }
string ToCookieData();
}
public class CookiesStorage : ICookiesStorage
{
private static readonly string[] CookieSeparators = { "; ", "=" };
private readonly Dictionary<string, string> _values;
private bool _touched;
public bool Touched
{
get { return _touched; }
}
public string ToCookieData()
{
StringBuilder builder = new StringBuilder();
foreach (var kvp in _values)
{
builder.AppendFormat("Set-Cookie: {0}={1}{2}", kvp.Key, kvp.Value, Environment.NewLine);
}
return builder.ToString();
}
public CookiesStorage(string cookie)
{
var keyValues = cookie.Split(CookieSeparators, StringSplitOptions.RemoveEmptyEntries);
_values = new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase);
for (int i = 0; i < keyValues.Length; i += 2)
{
var key = keyValues[i];
var value = keyValues[i + 1];
_values[key] = value;
}
}
public void Upsert(string key, string value)
{
_values[key] = value;
_touched = true;
}
public void Remove(string key)
{
if (_values.Remove(key))
{
_touched = true;
}
}
public IEnumerator<KeyValuePair<string, string>> GetEnumerator()
{
return _values.GetEnumerator();
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
public string GetByName(string name)
{
return _values[name];
}
public bool TryGetByName(string name, out string value)
{
return _values.TryGetValue(name, out value);
}
}
}
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using uhttpsharp.Headers;
namespace uhttpsharp.RequestProviders
{
public class HttpRequestProvider : IHttpRequestProvider
{
private static readonly char[] Separators = { '/' };
public async Task<IHttpRequest> Provide(StreamReader streamReader)
{
// parse the http request
var request = await streamReader.ReadLineAsync().ConfigureAwait(false);
if (request == null)
return null;
var tokens = request.Split(' ');
if (tokens.Length != 3)
{
return null;
}
var httpMethod = HttpMethodProvider.Default.Provide(tokens[0]);
var httpProtocol = tokens[2];
var url = tokens[1];
var queryString = GetQueryStringData(ref url);
var uri = new Uri(url, UriKind.Relative);
var headersRaw = new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase);
// get the headers
string line;
while (!string.IsNullOrEmpty((line = await streamReader.ReadLineAsync().ConfigureAwait(false))))
{
var headerKvp = SplitHeader(line);
headersRaw.Add(headerKvp.Key, headerKvp.Value);
}
IHttpHeaders headers = new HttpHeaders(headersRaw);
IHttpHeaders post = await GetPostData(streamReader, headers);
return new HttpRequest(headers, httpMethod, httpProtocol, uri,
uri.OriginalString.Split(Separators, StringSplitOptions.RemoveEmptyEntries), queryString, post);
}
private static IHttpHeaders GetQueryStringData(ref string url)
{
var queryStringIndex = url.IndexOf('?');
IHttpHeaders queryString;
if (queryStringIndex != -1)
{
queryString = new QueryStringHttpHeaders(url.Substring(queryStringIndex + 1));
url = url.Substring(0, queryStringIndex);
}
else
{
queryString = EmptyHttpHeaders.Empty;
}
return queryString;
}
private static async Task<IHttpHeaders> GetPostData(StreamReader streamReader, IHttpHeaders headers)
{
int postContentLength;
IHttpHeaders post;
if (headers.TryGetByName("content-length", out postContentLength))
{
post = await HttpHeaders.FromPost(streamReader, postContentLength);
}
else
{
post = EmptyHttpHeaders.Empty;
}
return post;
}
private KeyValuePair<string, string> SplitHeader(string header)
{
var index = header.IndexOf(": ", StringComparison.InvariantCultureIgnoreCase);
return new KeyValuePair<string, string>(header.Substring(0, index), header.Substring(index + 2));
}
}
}
\ No newline at end of file
using System.IO;
using System.Threading.Tasks;
namespace uhttpsharp.RequestProviders
{
public interface IHttpRequestProvider
{
/// <summary>
/// Provides an <see cref="IHttpRequest"/> based on the context of the stream,
/// May return null / throw exceptions on invalid requests.
/// </summary>
/// <param name="streamReader"></param>
/// <returns></returns>
Task<IHttpRequest> Provide(StreamReader streamReader);
}
}
\ No newline at end of file
......@@ -68,6 +68,7 @@
<Compile Include="Headers\IHttpHeaders.cs" />
<Compile Include="Headers\QueryStringHttpHeaders.cs" />
<Compile Include="HttpClient.cs" />
<Compile Include="HttpContext.cs" />
<Compile Include="HttpMethodProvider.cs" />
<Compile Include="HttpMethodProviderCache.cs" />
<Compile Include="HttpMethods.cs" />
......@@ -77,6 +78,7 @@
<Compile Include="HttpResponse.cs" />
<Compile Include="HttpServer.cs" />
<Compile Include="HttpServerExtensions.cs" />
<Compile Include="IEnumerable.cs" />
<Compile Include="IHttpContext.cs" />
<Compile Include="IHttpMethodProvider.cs" />
<Compile Include="Listeners\IHttpListener.cs" />
......@@ -84,6 +86,8 @@
<Compile Include="Listeners\TcpListenerAdapter.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="HttpResponseCode.cs" />
<Compile Include="RequestProviders\HttpRequestProvider.cs" />
<Compile Include="RequestProviders\IHttpRequestProvider.cs" />
<Compile Include="TaskFactoryExtensions.cs" />
<Compile Include="Clients\TcpClientAdapter.cs" />
</ItemGroup>
......
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