diff --git a/IotWeb NET45/Helper/HybridSessionStorageHandler.cs b/IotWeb NET45/Helper/HybridSessionStorageHandler.cs new file mode 100644 index 0000000..c8c72bb --- /dev/null +++ b/IotWeb NET45/Helper/HybridSessionStorageHandler.cs @@ -0,0 +1,228 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using IotWeb.Common.Interfaces; +using IotWeb.Common.Util; +using System.Net.Mime; +using System.Security.AccessControl; +using IotWeb.Common.Http; +using Newtonsoft.Json; + +namespace IotWeb.Server.Helper +{ + public class HybridSessionStorageHandler : ISessionStorageHandler + { + private readonly SessionConfiguration _sessionConfiguration; + private Dictionary _sessionDataCache; + private readonly string _sessionFileExtension; + + public HybridSessionStorageHandler(SessionConfiguration sessionConfiguration) + { + _sessionConfiguration = sessionConfiguration; + _sessionDataCache = new Dictionary(); + _sessionFileExtension = "sess"; + LoadSessionFiles(); + } + + private void LoadSessionFiles() + { + string[] files = Directory.GetFiles(GetStoragePath()); + + foreach (string file in files) + { + var sessionData = File.ReadAllText(file); + IDictionary sessionDictionary = new Dictionary(); + + if (!string.IsNullOrEmpty(sessionData)) + sessionDictionary = JsonConvert.DeserializeObject>(sessionData); + + _sessionDataCache[Path.GetFileNameWithoutExtension(file)] = new SessionCacheObject(DateTime.Now, sessionDictionary); + } + } + + public async Task DeleteSessionsAsync() + { + try + { + await Task.Run(() => + { + lock (_sessionDataCache) + { + var sessionIds = + _sessionDataCache.Where( + s => + s.Value.LastAccessTime < + DateTime.Now.AddMinutes(-_sessionConfiguration.SessionTimeOut)).Select(s => s.Key).ToList(); + + if (sessionIds.Count > 0) + { + foreach (var sid in sessionIds) + { + _sessionDataCache.Remove(sid); + } + + string[] files = Directory.GetFiles(GetStoragePath()); + + var filesToDelete = files.Where(s => sessionIds.Contains(Path.GetFileNameWithoutExtension(s))).ToList(); + + foreach (string file in filesToDelete) + { + FileInfo fi = new FileInfo(file); + fi.Delete(); + } + } + } + }); + + return true; + } + catch (Exception) + { + return false; + } + } + + public async Task DeleteSessionAsync(string sessionId) + { + try + { + await Task.Run(() => + { + lock (_sessionDataCache) + { + _sessionDataCache.Remove(sessionId); + + if (File.Exists(GetFilePath(sessionId))) + { + FileInfo fi = new FileInfo(GetFilePath(sessionId)); + fi.Delete(); + } + } + }); + + return true; + } + catch (Exception) + { + return false; + } + } + + public async Task> GetDataAsync(string sessionId) + { + try + { + Dictionary data = null; + + await Task.Run(() => + { + lock (_sessionDataCache) + { + if (_sessionDataCache.ContainsKey(sessionId)) + { + _sessionDataCache[sessionId].LastAccessTime = DateTime.Now; + data = (Dictionary)_sessionDataCache[sessionId].SessionData; + } + else if (File.Exists(GetFilePath(sessionId))) + { + var fileData = File.ReadAllText(GetFilePath(sessionId)); + data = new Dictionary(); + + if (!string.IsNullOrEmpty(fileData)) + data = JsonConvert.DeserializeObject>(fileData); + + _sessionDataCache[sessionId] = new SessionCacheObject(DateTime.Now, data); + } + } + }); + + return data; + } + catch (Exception) + { + return null; + } + } + + public async Task> GetSessionMetadata(string sessionId) + { + throw new NotImplementedException(); + } + + public async Task SaveDataAsync(string sessionId, IDictionary data) + { + try + { + bool isSaved = false; + await Task.Run(() => + { + lock (_sessionDataCache) + { + _sessionDataCache[sessionId] = new SessionCacheObject(DateTime.Now, data); + + var filePath = GetFilePath(sessionId); + var sessionData = JsonConvert.SerializeObject(data); + File.WriteAllText(filePath, sessionData); + + isSaved = true; + } + }); + + return isSaved; + } + catch (Exception) + { + return false; + } + } + + public bool UpdateSessionExpireTime(string sessionId) + { + lock (_sessionDataCache) + { + if (_sessionDataCache.ContainsKey(sessionId)) + { + _sessionDataCache[sessionId].LastAccessTime = DateTime.Now; + return true; + } + + return false; + } + } + + private string GetStoragePath() + { + string fullStorageFilePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, _sessionConfiguration.StorageFolder); + + if (!Directory.Exists(fullStorageFilePath)) + Directory.CreateDirectory(fullStorageFilePath); + + return fullStorageFilePath; + } + + private string GetFilePath(string fileName) + { + string fullFilePath = Path.Combine(GetStoragePath(), fileName); + + if (!string.IsNullOrWhiteSpace(_sessionFileExtension)) + fullFilePath = fullFilePath + "." + _sessionFileExtension; + + return fullFilePath; + } + } + + public class SessionCacheObject + { + public DateTime LastAccessTime { get; set; } + public IDictionary SessionData { get; set; } + + public SessionCacheObject(DateTime lastAccessTime, IDictionary sessionData) + { + LastAccessTime = lastAccessTime; + SessionData = sessionData; + } + } +} diff --git a/IotWeb NET45/HttpServer.cs b/IotWeb NET45/HttpServer.cs index 1dfdfcc..4e35945 100644 --- a/IotWeb NET45/HttpServer.cs +++ b/IotWeb NET45/HttpServer.cs @@ -1,12 +1,20 @@ using IotWeb.Common.Http; +using IotWeb.Common.Util; +using IotWeb.Server.Helper; namespace IotWeb.Server { public class HttpServer : BaseHttpServer { - public HttpServer(int port) - : base(new SocketServer(port)) - { + public HttpServer(int port) + : base(new SocketServer(port), new HybridSessionStorageHandler(new SessionConfiguration())) + { + // No configuration required + } + + public HttpServer(int port, SessionConfiguration sessionConfiguration) + : base(new SocketServer(port), new HybridSessionStorageHandler(sessionConfiguration)) + { // No configuration required } } diff --git a/IotWeb NET45/IotWeb NET45.csproj b/IotWeb NET45/IotWeb NET45.csproj index 4fb68bd..8d61192 100644 --- a/IotWeb NET45/IotWeb NET45.csproj +++ b/IotWeb NET45/IotWeb NET45.csproj @@ -29,9 +29,32 @@ prompt 4 + + true + bin\x64\Debug\ + TRACE;DEBUG;NET45 + full + x64 + prompt + MinimumRecommendedRules.ruleset + + + bin\x64\Release\ + TRACE;NET45 + true + pdbonly + x64 + prompt + MinimumRecommendedRules.ruleset + + + ..\packages\Newtonsoft.Json.9.0.1\lib\net45\Newtonsoft.Json.dll + True + + @@ -39,13 +62,17 @@ + + + + - {e788ca1a-9838-48a1-a961-1137e74eaf70} + {E788CA1A-9838-48A1-A961-1137E74EAF70} IotWeb Portable diff --git a/IotWeb NET45/packages.config b/IotWeb NET45/packages.config new file mode 100644 index 0000000..e1fae9c --- /dev/null +++ b/IotWeb NET45/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/IotWeb Portable/Http/BaseHttpServer.cs b/IotWeb Portable/Http/BaseHttpServer.cs index fed52c8..8653c01 100644 --- a/IotWeb Portable/Http/BaseHttpServer.cs +++ b/IotWeb Portable/Http/BaseHttpServer.cs @@ -1,6 +1,7 @@ using System; using System.IO; using System.Collections.Generic; +using IotWeb.Common.Interfaces; namespace IotWeb.Common.Http { @@ -17,7 +18,19 @@ public class BaseHttpServer : IServer public bool Running { get { return SocketServer.Running; } } - protected BaseHttpServer(ISocketServer server) + private ISessionStorageHandler _sessionStorageHandler; + public ISessionStorageHandler SessionStorageHandler + { + get { return _sessionStorageHandler; } + } + + protected BaseHttpServer(ISocketServer server, ISessionStorageHandler sessionStorageHandler) + : this(server) + { + _sessionStorageHandler = sessionStorageHandler; + } + + protected BaseHttpServer(ISocketServer server) { SocketServer = server; SocketServer.ConnectionRequested = ConnectionRequested; @@ -204,4 +217,15 @@ internal IWebSocketRequestHandler GetHandlerForWebSocket(string uri, out string } + + /////////////////////////////////Changes done locally to fix HTTP 1.1 on Safari 10 websocket error on 22.11.2016///////////////////// + /// + /// Defines HTTP version + /// + public enum HttpVersion + { + Ver1_0, + Ver1_1 + } + /////////////////////////////////Changes done locally to fix HTTP 1.1 on Safari 10 websocket error on 22.11.2016///////////////////// } diff --git a/IotWeb Portable/Http/HttpContext.cs b/IotWeb Portable/Http/HttpContext.cs index ca2c65e..fd05cec 100644 --- a/IotWeb Portable/Http/HttpContext.cs +++ b/IotWeb Portable/Http/HttpContext.cs @@ -1,4 +1,7 @@ -namespace IotWeb.Common.Http +using System.Collections.Generic; +using IotWeb.Common.Util; + +namespace IotWeb.Common.Http { public class HttpContext : CaseInsensitiveDictionary { @@ -6,5 +9,7 @@ public class HttpContext : CaseInsensitiveDictionary /// Common name for session entry. /// public const string Session = "Session"; + + public SessionHandler SessionHandler; } } diff --git a/IotWeb Portable/Http/HttpRequestProcessor.cs b/IotWeb Portable/Http/HttpRequestProcessor.cs index 668cdc7..45f80b6 100644 --- a/IotWeb Portable/Http/HttpRequestProcessor.cs +++ b/IotWeb Portable/Http/HttpRequestProcessor.cs @@ -1,9 +1,12 @@ using System; +using System.Collections.Generic; using System.IO; using System.Net; using System.Text; using System.Text.RegularExpressions; +using System.Threading.Tasks; using IotWeb.Common.Util; +using Newtonsoft.Json; namespace IotWeb.Common.Http { @@ -35,9 +38,10 @@ private enum RequestParseState private const int InputBufferSize = 1024; private const byte CR = 0x0d; private const byte LF = 0x0a; + private const string SessionName = "IoTSession"; - // WebSocket header fields - private static string SecWebSocketKey = "Sec-WebSocket-Key"; + // WebSocket header fields + private static string SecWebSocketKey = "Sec-WebSocket-Key"; private static string SecWebSocketProtocol = "Sec-WebSocket-Protocol"; private static string SecWebSocketVersion = "Sec-WebSocket-Version"; private static string SecWebSocketAccept = "Sec-WebSocket-Accept"; @@ -72,12 +76,14 @@ public HttpRequestProcessor(BaseHttpServer server) /// /// public void ProcessHttpRequest(Stream input, Stream output) - { + { // Set up state HttpRequest request = null; HttpResponse response = null; HttpException parseError = null; HttpContext context = null; + SessionHandler sessionHandler = null; + // Process the request try { @@ -97,12 +103,25 @@ public void ProcessHttpRequest(Stream input, Stream output) throw new HttpRequestEntityTooLargeException(); // Read the data in MemoryStream content = new MemoryStream(); + //23.08.2016 - Changes for supporting POST Method + int contentCopyCounter = 0; + int bodyReadCount = 0; while (m_connected && (content.Length != length)) { - ReadData(input); + if (contentCopyCounter > 0) + { + int bytesToRead = ((length - bodyReadCount) > InputBufferSize) ? InputBufferSize : (length - bodyReadCount); + ReadData(input, bytesToRead); + } + + bodyReadCount += m_index; + content.Write(m_buffer, 0, m_index); ExtractBytes(m_index); + contentCopyCounter++; } + //23.08.2016 - End of Changes for supporting POST Method + // Did the connection drop while reading? if (!m_connected) return; @@ -120,15 +139,51 @@ public void ProcessHttpRequest(Stream input, Stream output) Cookie c = new Cookie(); c.Name = parts[0].Trim(); if (parts.Length > 1) - c.Value = parts[1].Trim(); + c.Value = parts[1].Trim(); request.Cookies.Add(c); } } // We have at least a partial request, create the matching response context = new HttpContext(); - response = new HttpResponse(); - // Apply filters - if (m_server.ApplyBeforeFilters(request, response, context)) + response = new HttpResponse(); + + //Get session id from cookies + var sessionId = GetSessionIdentifier(request.Cookies); + var isNewRequest = string.IsNullOrEmpty(sessionId); + + if (isNewRequest) + { + sessionId = Utilities.GetNewSessionIdentifier(); + } + + sessionHandler = new SessionHandler(sessionId, m_server.SessionStorageHandler); + context.SessionHandler = sessionHandler; + + sessionHandler.DestroyExpiredSessions(); + + //Update session data + if (isNewRequest) + { + sessionHandler.SaveSessionData(); + response.Cookies.Add(new Cookie(SessionName, sessionHandler.SessionId)); + } + else + { + sessionHandler.UpdateSessionTimeOut(); + + var isRetrieved = sessionHandler.GetSessionData(); + if (!isRetrieved) + { + sessionId = Utilities.GetNewSessionIdentifier(); + sessionHandler = new SessionHandler(sessionId, m_server.SessionStorageHandler); + context.SessionHandler = sessionHandler; + sessionHandler.SaveSessionData(); + response.Cookies.Add(new Cookie(SessionName, sessionHandler.SessionId)); + } + } + + // Apply filters + if (m_server.ApplyBeforeFilters(request, response, context)) { // Check for WebSocket upgrade IWebSocketRequestHandler wsHandler = UpgradeToWebsocket(request, response); @@ -136,8 +191,9 @@ public void ProcessHttpRequest(Stream input, Stream output) { // Apply the after filters here m_server.ApplyAfterFilters(request, response, context); - // Write the response back to accept the connection - response.Send(output); + // Write the response back to accept the connection + /////////////////////////////////Changes done locally to fix HTTP 1.1 on Safari 10 websocket error on 22.11.2016///////////////////// + response.Send(output, HttpVersion.Ver1_1); output.Flush(); // Now we can process the websocket WebSocket ws = new WebSocket(input, output); @@ -151,8 +207,8 @@ public void ProcessHttpRequest(Stream input, Stream output) IHttpRequestHandler handler = m_server.GetHandlerForUri(request.URI, out partialUri); if (handler == null) throw new HttpNotFoundException(); - handler.HandleRequest(partialUri, request, response, context); - } + handler.HandleRequest(partialUri, request, response, context); + } } catch (HttpException ex) { @@ -171,12 +227,26 @@ public void ProcessHttpRequest(Stream input, Stream output) } // Apply the after filters here m_server.ApplyAfterFilters(request, response, context); - // Write the response + + //Update the session before sending the response + if (sessionHandler != null) + { + if (sessionHandler.IsChanged) + sessionHandler.SaveSessionData(); + + if (sessionHandler.IsSessionDestroyed) + { + sessionHandler.IsSessionDestroyed = false; + response.Cookies.Add(new Cookie(SessionName, sessionHandler.SessionId)); + } + } + + // Write the response response.Send(output); output.Flush(); } - - #region Internal Implementation + + #region Internal Implementation /// /// Check for an upgrade to a web socket connection. /// @@ -391,6 +461,35 @@ private bool ReadLine(Stream input, out string line) ReadData(input); return false; } - #endregion - } + + /// + /// 23.08.2016 - Changes for supporting POST Method - Added a new overload for reading the content data from the input stream + /// Read data from the stream into the buffer. + /// + /// + /// + /// + private void ReadData(Stream input, int count) + { + try + { + int read = input.Read(m_buffer, 0, count); + m_index += read; + if (read == 0) + m_connected = false; + } + catch (Exception exp) + { + // Any error causes the connection to close + m_connected = false; + } + } + + private string GetSessionIdentifier(CookieCollection cookies) + { + return cookies[SessionName]?.Value; + } + + #endregion + } } diff --git a/IotWeb Portable/Http/HttpResourceHandler.cs b/IotWeb Portable/Http/HttpResourceHandler.cs index 2525133..72f61aa 100644 --- a/IotWeb Portable/Http/HttpResourceHandler.cs +++ b/IotWeb Portable/Http/HttpResourceHandler.cs @@ -1,4 +1,5 @@ -using System.IO; +using System; +using System.IO; using System.Linq; using System.Reflection; diff --git a/IotWeb Portable/Http/HttpResponse.cs b/IotWeb Portable/Http/HttpResponse.cs index a1be42c..05009cb 100644 --- a/IotWeb Portable/Http/HttpResponse.cs +++ b/IotWeb Portable/Http/HttpResponse.cs @@ -21,15 +21,24 @@ internal HttpResponse() ResponseMessage = null; Content = new MemoryStream(); } - + /// /// Write the response to the output stream /// /// - internal void Send(Stream output) + internal void Send(Stream output, HttpVersion version = HttpVersion.Ver1_0) { // Write the response start line - WriteLine(output, String.Format("HTTP/1.0 {0} {1}", ResponseCode.ResponseCode(), ResponseCode.ResponseMessage(ResponseMessage))); + /////////////////////////////////Changes done locally to fix HTTP 1.1 on Safari 10 websocket error on 22.11.2016///////////////////// + if (version == HttpVersion.Ver1_0) + { + WriteLine(output, String.Format("HTTP/1.0 {0} {1}", ResponseCode.ResponseCode(), ResponseCode.ResponseMessage(ResponseMessage))); + } + else if(version == HttpVersion.Ver1_1) + { + WriteLine(output, String.Format("HTTP/1.1 {0} {1}", ResponseCode.ResponseCode(), ResponseCode.ResponseMessage(ResponseMessage))); + } + /////////////////////////////////Changes done locally to fix HTTP 1.1 on Safari 10 websocket error on 22.11.2016///////////////////// // Set content length accordingly Headers[HttpHeaders.ContentLength] = Content.Position.ToString(); // Write the headers @@ -85,5 +94,18 @@ private void WriteLine(Stream output, string line) byte[] bytes = Encoding.UTF8.GetBytes(line + "\r\n"); output.Write(bytes, 0, bytes.Length); } - } + + public void RedirectPermanently(string page) + { + this.ResponseCode = HttpResponseCode.MovedPermanently; + this.Headers.Add("Location", page); + } + + public void RedirectTemporarily(string page) + { + this.ResponseCode = HttpResponseCode.MovedTemporarily; + this.Headers.Add("Location", page); + } + + } } diff --git a/IotWeb Portable/Http/HttpResponseCode.cs b/IotWeb Portable/Http/HttpResponseCode.cs index 71f9c22..63aab40 100644 --- a/IotWeb Portable/Http/HttpResponseCode.cs +++ b/IotWeb Portable/Http/HttpResponseCode.cs @@ -11,7 +11,8 @@ public enum HttpResponseCode SwitchingProtocols, Ok, MovedPermanently, - SeeOther, + MovedTemporarily, + SeeOther, BadRequest, Unauthorized, Forbidden, @@ -36,6 +37,7 @@ public static class HttpResponseCodeExtensions 101, // SwitchingProtocols 200, // Ok 301, // MovedPermanently + 302, // MovedTemporarily 303, // SeeOther 400, // BadRequest 401, // Unauthorized diff --git a/IotWeb Portable/Http/IHttpRequestHandler.cs b/IotWeb Portable/Http/IHttpRequestHandler.cs index 446c8fd..6335115 100644 --- a/IotWeb Portable/Http/IHttpRequestHandler.cs +++ b/IotWeb Portable/Http/IHttpRequestHandler.cs @@ -1,5 +1,6 @@ namespace IotWeb.Common.Http { + /// /// Defines a request handler. /// diff --git a/IotWeb Portable/Interfaces/ISessionStorageHandler.cs b/IotWeb Portable/Interfaces/ISessionStorageHandler.cs new file mode 100644 index 0000000..d68975b --- /dev/null +++ b/IotWeb Portable/Interfaces/ISessionStorageHandler.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using IotWeb.Common.Util; + +namespace IotWeb.Common.Interfaces +{ + public interface ISessionStorageHandler + { + Task SaveDataAsync(string sessionId, IDictionary data); + Task> GetDataAsync(string sessionId); + Task DeleteSessionsAsync(); + Task DeleteSessionAsync(string sessionId); + Task> GetSessionMetadata(string sessionId); + bool UpdateSessionExpireTime(string sessionId); + } +} \ No newline at end of file diff --git a/IotWeb Portable/IotWeb Portable.csproj b/IotWeb Portable/IotWeb Portable.csproj index 4ed53c2..7afe68d 100644 --- a/IotWeb Portable/IotWeb Portable.csproj +++ b/IotWeb Portable/IotWeb Portable.csproj @@ -33,6 +33,24 @@ prompt 4 + + true + bin\x64\Debug\ + TRACE;DEBUG;PORTABLE + full + x64 + prompt + MinimumRecommendedRules.ruleset + + + bin\x64\Release\ + TRACE;PORTABLE + true + pdbonly + x64 + prompt + MinimumRecommendedRules.ruleset + @@ -51,15 +69,31 @@ + + + + + + + + + + ..\packages\Newtonsoft.Json.9.0.1\lib\portable-net45+wp80+win8+wpa81\Newtonsoft.Json.dll + True + + + C:\Program Files (x86)\Windows Kits\10\References\Windows.Foundation.UniversalApiContract\2.0.0.0\Windows.Foundation.UniversalApiContract.winmd + +