diff --git a/CMakeLists.txt b/CMakeLists.txt
index eceddacd06c3e7c4cbe373601a00f30cc105c3f1..33642ab15c3caad16f0093fb797beeb2fe161554 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -99,8 +99,9 @@ option(LWS_WITH_ACCESS_LOG "Support generating Apache-compatible access logs" OF
 option(LWS_WITH_SERVER_STATUS "Support json + jscript server monitoring" OFF)
 option(LWS_WITH_LEJP "With the Lightweight JSON Parser" OFF)
 option(LWS_WITH_LEJP_CONF "With LEJP configuration parser as used by lwsws" OFF)
+option(LWS_WITH_GENERIC_SESSIONS "With the Generic Sessions plugin" OFF)
+option(LWS_WITH_SQLITE3 "Require SQLITE3 support" OFF)
 option(LWS_WITH_SMTP "Provide SMTP support" OFF)
-option(LWS_WITH_STATEFUL_URLDECODE "Provide stateful URLDECODE apis" OFF)
 
 if (LWS_WITH_LWSWS)
  message(STATUS "LWS_WITH_LWSWS --> Enabling LWS_WITH_PLUGINS and LWS_WITH_LIBUV")
@@ -112,10 +113,6 @@ if (LWS_WITH_LWSWS)
  set(LWS_WITH_LEJP_CONF 1)
 endif()
 
-if (LWS_WITH_PLUGINS)
- set(LWS_WITH_STATEFUL_URLDECODE 1)
-endif()
-
 if (LWS_WITH_PLUGINS AND NOT LWS_WITH_LIBUV)
 message(STATUS "LWS_WITH_PLUGINS --> Enabling LWS_WITH_LIBUV")
  set(LWS_WITH_LIBUV 1)
@@ -126,6 +123,16 @@ message(STATUS "LWS_WITH_SMTP --> Enabling LWS_WITH_LIBUV")
  set(LWS_WITH_LIBUV 1)
 endif()
 
+if (LWS_WITH_GENERIC_SESSIONS)
+ set(LWS_WITH_SQLITE3 1)
+ set(LWS_WITH_SMTP 1)
+endif()
+
+if (LWS_WITH_SMTP AND NOT LWS_WITH_LIBUV)
+message(STATUS "LWS_WITH_SMTP --> Enabling LWS_WITH_LIBUV")
+ set(LWS_WITH_LIBUV 1)
+endif()
+
 if (DEFINED YOTTA_WEBSOCKETS_VERSION_STRING)
 
 set(LWS_WITH_SHARED OFF)
@@ -198,6 +205,9 @@ set( CACHE PATH "Path to the libev library")
 set(LWS_LIBEV_INCLUDE_DIRS CACHE PATH "Path to the libev include directory")
 set(LWS_LIBUV_LIBRARIES CACHE PATH "Path to the libuv library")
 set(LWS_LIBUV_INCLUDE_DIRS CACHE PATH "Path to the libuv include directory")
+set(LWS_SQLITE3_LIBRARIES CACHE PATH "Path to the libuv library")
+set(LWS_SQLITE3_INCLUDE_DIRS CACHE PATH "Path to the libuv include directory")
+
 
 if (NOT LWS_WITH_SSL)
 	set(LWS_WITHOUT_BUILTIN_SHA1 OFF)
@@ -285,6 +295,15 @@ if (LWS_WITH_LIBUV)
 	endif()
 endif()
 
+if (LWS_WITH_SQLITE3)
+	if ("${LWS_SQLITE3_LIBRARIES}" STREQUAL "" OR "${LWS_SQLITE3_INCLUDE_DIRS}" STREQUAL "")
+	else()
+		set(SQLITE3_LIBRARIES ${LWS_SQLITE3_LIBRARIES})
+		set(SQLITE3_INCLUDE_DIRS ${LWS_SQLITE3_INCLUDE_DIRS})
+		set(SQLITE3_FOUND 1)
+	endif()
+endif()
+
 
 # FIXME: This must be runtime-only option.
 # The base dir where the test-apps look for the SSL certs.
@@ -612,6 +631,17 @@ endif()
 if (WIN32)
 	set(WIN32_HELPERS_PATH win32port/win32helpers)
 	include_directories(${WIN32_HELPERS_PATH})
+
+		if (WIN32)
+			list(APPEND SOURCES
+				${WIN32_HELPERS_PATH}/gettimeofday.c
+			)
+
+			list(APPEND HDR_PRIVATE
+				${WIN32_HELPERS_PATH}/gettimeofday.h
+			)
+		endif(WIN32)
+
 else()
 	# Unix.
 	if (NOT LWS_WITHOUT_DAEMONIZE)
@@ -870,6 +900,22 @@ if (LWS_WITH_LIBUV)
 	include_directories("${LIBUV_INCLUDE_DIRS}")
 	list(APPEND LIB_LIST ${LIBUV_LIBRARIES})
 endif()
+
+if (LWS_WITH_SQLITE3)
+	if (NOT SQLITE3_FOUND)
+		find_path(SQLITE3_INCLUDE_DIRS NAMES sqlite3.h)
+		find_library(SQLITE3_LIBRARIES NAMES sqlite3)
+		if(SQLITE3_INCLUDE_DIRS AND SQLITE3_LIBRARIES)
+			set(SQLITE3_FOUND 1)
+		endif()
+	endif()
+	message("sqlite3 include dir: ${SQLITE3_INCLUDE_DIRS}")
+	message("sqlite3 libraries: ${SQLITE3_LIBRARIES}")
+	include_directories("${SQLITE3_INCLUDE_DIRS}")
+	list(APPEND LIB_LIST ${SQLITE3_LIBRARIES})
+endif()
+
+
 if (LWS_WITH_HTTP_PROXY)
 	find_library(LIBHUBBUB_LIBRARIES NAMES libhubbub)
 	list(APPEND LIB_LIST ${LIBHUBBUB_LIBRARIES} )
@@ -1204,10 +1250,19 @@ if (NOT LWS_WITHOUT_TESTAPPS)
 	
 	
 	if (LWS_WITH_PLUGINS AND LWS_WITH_SHARED)
-		macro(create_plugin PLUGIN_NAME MAIN_SRC)
+		macro(create_plugin PLUGIN_NAME MAIN_SRC S2 S3)
 
 		set(PLUGIN_SRCS ${MAIN_SRC})
 
+		if ("${S2}" STREQUAL "")
+		else()
+			list(APPEND PLUGIN_SRCS ${S2})
+		endif()
+		if ("${S3}" STREQUAL "")
+		else()
+			list(APPEND PLUGIN_SRCS ${S3})
+		endif()
+
 		if (WIN32)
 			list(APPEND PLUGIN_SRCS
 				${WIN32_HELPERS_PATH}/getopt.c
@@ -1239,27 +1294,42 @@ if (NOT LWS_WITHOUT_TESTAPPS)
 #			OUTPUT_NAME ${PLUGIN_NAME})
 
 		list(APPEND PLUGINS_LIST ${PLUGIN_NAME})
+
 		endmacro()
 		
 
 		create_plugin(protocol_dumb_increment
-			      "plugins/protocol_dumb_increment.c")
+			      "plugins/protocol_dumb_increment.c" "" "")
 		create_plugin(protocol_lws_mirror
-			      "plugins/protocol_lws_mirror.c")
+			      "plugins/protocol_lws_mirror.c" "" "")
 		create_plugin(protocol_lws_status
-			      "plugins/protocol_lws_status.c")
+			      "plugins/protocol_lws_status.c" "" "")
 		create_plugin(protocol_post_demo
-			      "plugins/protocol_post_demo.c")
+			      "plugins/protocol_post_demo.c" "" "")
 if (LWS_WITH_SERVER_STATUS)
 		create_plugin(protocol_lws_server_status
-			      "plugins/protocol_lws_server_status.c")
+			      "plugins/protocol_lws_server_status.c" "" "")
 endif()
 
 if (NOT LWS_WITHOUT_CLIENT)
 		create_plugin(protocol_client_loopback_test
-                              "plugins/protocol_client_loopback_test.c")
+                              "plugins/protocol_client_loopback_test.c" "" "")
 endif(NOT LWS_WITHOUT_CLIENT)
 
+if (LWS_WITH_GENERIC_SESSIONS)
+	create_plugin(protocol_generic_sessions
+                      "plugins/generic-sessions/protocol_generic_sessions.c"
+		      "plugins/generic-sessions/utils.c"
+		      "plugins/generic-sessions/handlers.c")
+
+	if (WIN32)
+		target_link_libraries(protocol_generic_sessions ${LWS_SQLITE3_LIBRARIES})
+	else()
+		target_link_libraries(protocol_generic_sessions sqlite3 )
+	endif(WIN32)
+endif(LWS_WITH_GENERIC_SESSIONS)
+
+
 	endif(LWS_WITH_PLUGINS AND LWS_WITH_SHARED)
 
 	#
@@ -1460,6 +1530,31 @@ if (LWS_WITH_SERVER_STATUS)
 		DESTINATION share/libwebsockets-test-server/server-status
 			COMPONENT examples)
 endif()
+if (LWS_WITH_GENERIC_SESSIONS)
+	install(FILES
+		      plugins/generic-sessions/assets/lwsgs-logo.png
+		      plugins/generic-sessions/assets/seats.jpg
+		      plugins/generic-sessions/assets/failed-login.html
+		      plugins/generic-sessions/assets/lwsgs.js
+		      plugins/generic-sessions/assets/post-register-fail.html
+		      plugins/generic-sessions/assets/post-register-ok.html
+		      plugins/generic-sessions/assets/post-verify-ok.html
+		      plugins/generic-sessions/assets/post-verify-fail.html
+		      plugins/generic-sessions/assets/sent-forgot-ok.html
+		      plugins/generic-sessions/assets/sent-forgot-fail.html
+		      plugins/generic-sessions/assets/post-forgot-ok.html
+		      plugins/generic-sessions/assets/post-forgot-fail.html
+		      plugins/generic-sessions/assets/index.html
+		DESTINATION share/libwebsockets-test-server/generic-sessions
+			COMPONENT examples)
+	install(FILES plugins/generic-sessions/assets/successful-login.html 
+		DESTINATION share/libwebsockets-test-server/generic-sessions/needauth
+			COMPONENT examples)
+	install(FILES plugins/generic-sessions/assets/admin-login.html
+		DESTINATION share/libwebsockets-test-server/generic-sessions/needadmin
+			COMPONENT examples)
+endif()
+
 endif()
 
 # Install the LibwebsocketsConfig.cmake and LibwebsocketsConfigVersion.cmake
@@ -1531,7 +1626,8 @@ message(" LWS_WITH_SERVER_STATUS = ${LWS_WITH_SERVER_STATUS}")
 message(" LWS_WITH_LEJP = ${LWS_WITH_LEJP}")
 message(" LWS_WITH_LEJP_CONF = ${LWS_WITH_LEJP_CONF}")
 message(" LWS_WITH_SMTP = ${LWS_WITH_SMTP}")
-message(" LWS_WITH_STATEFUL_URLDECODE = ${LWS_WITH_STATEFUL_URLDECODE}")
+message(" LWS_WITH_GENERIC_SESSIONS = ${LWS_WITH_GENERIC_SESSIONS}")
+
 
 message("---------------------------------------------------------------------")
 
diff --git a/README.generic-sessions.md b/README.generic-sessions.md
new file mode 100644
index 0000000000000000000000000000000000000000..dab287349ed84fd472616f1728e3ab1750641615
--- /dev/null
+++ b/README.generic-sessions.md
@@ -0,0 +1,383 @@
+Generic Sessions Plugin
+-----------------------
+
+Enabling for build
+------------------
+
+Enable at CMake with -DLWS_WITH_GENERIC_SESSIONS=1
+
+This also needs sqlite3 (libsqlite3-dev or similar package)
+
+
+Introduction
+------------
+
+The generic-sessions protocol plugin provides cookie-based login
+authentication for lws web and ws connections.
+
+The plugin handles everything about generic account registration,
+email verification, lost password, account deletion, and other generic account
+management.
+
+Other code, in another eg, ws protocol handler, only needs very high-level
+state information from generic-sessions, ie, which user the client is
+authenticated as.  Everything underneath is managed in generic-sessions.
+
+
+ - random 20-byte session id managed in a cookie
+
+ - all information related to the session held at the server, nothing managed clientside
+
+ - sqlite3 used at the server to manage active sessions and users
+
+ - defaults to creating anonymous sessions with no user associated
+
+ - admin account (with user-selectable username) is defined in config with a SHA-1 of the password; rest of the accounts are in sqlite3
+ 
+  - user account passwords stored as salted SHA-1 with additional confounder
+  only stored in the JSON config, not the database 
+
+ - login, logout, register account + email verification built-in with examples
+
+ - in a mount, some file suffixes (ie, .js) can be associated with a protocol for the purposes of rewriting symbolnames.  These are read-only copies of logged-in server state.
+
+ - When your page fetches .js or other rewritten files from that mount, "$lwsgs_user" and so on are rewritten on the fly using chunked transfer encoding
+
+ - Eliminates server-side scripting with a few rewritten symbols and
+ javascript on client side
+
+ - 32-bit bitfield for authentication sectoring, mounts can provide a mask on the loggin-in session's associated server-side bitfield that must be set for access.
+
+ - No code (just config) required for, eg, private URL namespace that requires login to access. 
+ 
+
+Integration to HTML
+-------------------
+
+Only three steps are needed to integrate lwsgs in your HTML.
+
+1) lwsgs HTML UI is bundled with the javascript it uses in `lwsgs.js`, so
+import that script file in your head section
+
+2) define an empty div of id "lwsgs" somewhere
+
+3) Call lwsgs_initial() in your page
+
+That's it.  An example is below
+
+
+```
+<html>
+ <head>
+  <script src="lwsgs.js"></script>
+  <style>
+     .body { font-size: 12 }
+     .gstitle { font-size: 18 }
+  </style>
+  </head>
+  <body style="background-image:url(seats.jpg)">
+    <table style="width:100%;transition: max-height 2s;">
+     <tr>
+      <td style="vertical-align:top;text-align:left;width=200px">
+       <img src="lwsgs-logo.png">
+      </td>
+      <td style="vertical-align:top;float:right">
+	<div id=lwsgs style="text-align:right;background-color: rgba(255, 255, 255, 0.8);"></div>
+      </td>
+     </tr>
+    </table>
+   </form>
+   
+   <script>lwsgs_initial();</script>
+
+ </body>
+</html>
+```
+
+Overall Flow
+------------
+
+When the protocol is initialized, it gets per-vhost information from the config, such
+as where the sqlite3 databases are to be stored.  The admin username and sha-1 of the
+admin password are also taken from here.
+
+In the mounts using protocol-generic-sessions, a cookie is maintained against any requests; if no cookie was active on the initial request a new session is
+created with no attached user.
+
+So there should always be an active session after any transactions with the server.
+
+In the example html going to the mount /lwsgs loads a login / register page as the default.
+
+The <form> in the login page contains 'next url' hidden inputs that let the html 'program' where the form handler will go after a successful admin login, a successful user login and a failed login.
+
+After a successful login, the sqlite record at the server for the current session is updated to have the logged-in username associated with it. 
+
+
+
+Configuration
+-------------
+
+"auth-mask" defines the autorization sector bits that must be enabled on the session to gain access.
+
+"auth-mask" 0 is the default.
+
+  - b0 is set if you are logged in as a user at all.
+  - b1 is set if you are logged in with the user configured to be admin
+  - b2 is set if the account has been verified (the account configured for admin is always verified)
+
+```
+      {
+        # things in here can always be served
+        "mountpoint": "/lwsgs",
+        "origin": "file:///usr/share/libwebsockets-test-server/generic-sessions",
+        "origin": "callback://protocol-lws-messageboard",
+        "default": "generic-sessions-login-example.html",
+        "auth-mask": "0",
+        "interpret": {
+                ".js": "protocol-lws-messageboard"
+        }
+       }, {
+        # things in here can only be served if logged in as a user
+        "mountpoint": "/lwsgs/needauth",
+        "origin": "file:///usr/share/libwebsockets-test-server/generic-sessions/needauth",
+        "origin": "callback://protocol-lws-messageboard",
+        "default": "generic-sessions-login-example.html",
+        "auth-mask": "5", # logged in as a verified user
+        "interpret": {
+                ".js": "protocol-lws-messageboard"
+        }
+       }, {
+        # things in here can only be served if logged in as admin
+        "mountpoint": "/lwsgs/needadmin",
+        "origin": "file:///usr/share/libwebsockets-test-server/generic-sessions/needadmin",
+        "origin": "callback://protocol-lws-messageboard",
+        "default": "generic-sessions-login-example.html",
+        "auth-mask": "7", # b2 = verified (by email / or admin), b1 = admin, b0 = logged in with any user name
+        "interpret": {
+                ".js": "protocol-lws-messageboard"
+        }
+       }
+```
+
+Note that the name of the real application protocol that uses generic-sessions
+is used, not generic-sessions itself. 
+
+The vhost configures the storage dir, admin credentials and session cookie lifetimes:
+
+```
+     "ws-protocols": [{
+       "protocol-generic-sessions": {
+         "status": "ok",
+         "admin-user": "admin",
+
+# create the pw hash like this (for the example pw, "jipdocesExunt" )
+# $ echo -n "jipdocesExunt" | sha1sum
+# 046ce9a9cca769e85798133be06ef30c9c0122c9 -
+#
+# Obviously ** change this password hash to a secret one before deploying **
+#
+         "admin-password-sha1": "046ce9a9cca769e85798133be06ef30c9c0122c9",
+         "session-db": "/var/www/sessions/lws.sqlite3",
+         "timeout-idle-secs": "600",
+	 "timeout-anon-idle-secs": "1200",
+         "timeout-absolute-secs": "6000",
+# the confounder is part of the salted password hashes.  If this config
+# file is in a 0700 root:root dir, an attacker with apache credentials
+# will have to get the confounder out of the process image to even try
+# to guess the password hashes.
+         "confounder": "Change to <=31 chars of junk",
+
+         "email-from": "noreply@example.com",
+         "email-smtp-ip": "127.0.0.1",
+         "email-expire": "3600",
+         "email-helo": "myhost.com",
+         "email-contact-person": "Set Me <real-person@email.com>",
+         "email-confirm-url-base": "http://localhost:7681/lwsgs"
+       }
+```
+
+The email- related settings control generation of automatic emails for
+registration and forgotten password.
+
+ - `email-from`: The email address automatic emails are sent from
+
+ - `email-smtp-ip`: Normally 127.0.0.1, if you have a suitable server on port
+   25 on your lan you can use this instead here.
+
+ - `email-expire`: Seconds that links sent in email will work before being
+   deleted
+
+ - `email-helo`: HELO to use when communicating with your SMTP server
+
+ - `email-contact-person`: mentioned in the automatic emails as a human who can
+   answer questions
+
+ - `email-confirm-url-base`: the URL to start links with in the emails, so the
+   recipient can get back to the web server
+   
+The real protocol that makes use of generic-sessions must also be listed and
+any configuration it needs given
+
+```
+       "protocol-lws-messageboard": {
+         "status": "ok",
+         "message-db": "/var/www/sessions/messageboard.sqlite3"
+       },
+```
+Notice the real application uses his own sqlite db, no details about how
+generic-sessions works or how it stores data are available to it.
+
+
+Password Confounder
+-------------------
+
+You can also define a per-vhost confounder shown in the example above, used
+when aggregating the password with the salt when it is hashed.  Any attacker
+will also need to get the confounder along with the database, which you can
+make harder by making the config dir only eneterable / readable by root.
+
+
+Preparing the db directory
+--------------------------
+
+You will have to prepare the db directory so it's suitable for the lwsws user to use,
+that usually means apache, eg
+
+```
+# mkdir -p /var/www/sessions
+# chown root:apache /var/www/sessions
+# chmod 770 /var/www/sessions
+```
+
+Email configuration
+-------------------
+
+lwsgs will can send emails by talking to an SMTP server on localhost:25.  That
+will usually be sendmail or postfix, you should confirm that works first by
+itself using the `mail` application to send on it.
+
+lwsgs has been tested on stock Fedora sendmail and postfix.
+
+
+Integration with another protocol
+---------------------------------
+
+lwsgs is designed to provide sessions and accounts in a standalone and generic way.
+
+But it's not useful by itself, there will always be the actual application who wants
+to make use of generic-sessions features.
+
+The basic approach is the 'real' protocol handler (usually a plugin itself)
+subclasses the generic-sessions plugin and calls through to it by default.
+
+The "real" protocol handler entirely deals with ws-related stuff itself, since
+generic-sessions does not use ws.  But for
+
+ - LWS_CALLBACK_HTTP
+ - LWS_CALLBACK_HTTP_BODY
+ - LWS_CALLBACK_HTTP_BODY_COMPLETION
+ - LWS_CALLBACK_HTTP_DROP_PROTOCOL
+ 
+the "real" protocol handler checks if it recognizes the activity (eg, his own
+POST form URL) and if not, passes stuff through to the generic-sessions protocol callback to handle it.  To simplify matters the real protocol can just pass
+through any unhandled messages to generic-sessions.
+
+The "real" protocol can get a pointer to generic-sessions protocol on the
+same vhost using
+
+```
+	vhd->gsp = lws_vhost_name_to_protocol(vhd->vh, "protocol-generic-sessions");
+```
+
+The "real" protocol must also arrange generic-sessions per_session_data in his
+own per-session allocation.  To allow keeping generic-sessions opaque, the
+real protocol must allocate that space at runtime, using the pss size
+the generic-sessions protocol struct exposes
+
+```
+struct per_session_data__myapp {
+	void *pss_gs;
+...
+
+	pss->pss_gs = malloc(vhd->gsp->per_session_data_size);
+```
+
+The allocation reserved for generic-sessions is then used as user_space when
+the real protocol calls through to the generic-sessions callback
+
+```
+	vhd->gsp->callback(wsi, reason, &pss->pss_gs, in, len);
+```
+
+In that way the "real" protocol can subclass generic-sessions functionality.
+
+
+To ease management of these secondary allocations, there are callbacks that
+occur when a wsi binds to a protocol and when the binding is dropped.  These
+should be used to malloc and free and kind of per-connection
+secondary allocations.
+
+
+```
+	case LWS_CALLBACK_HTTP_BIND_PROTOCOL:
+		if (!pss || pss->pss_gs)
+			break;
+
+		pss->pss_gs = malloc(vhd->gsp->per_session_data_size);
+		if (!pss->pss_gs)
+			return -1;
+
+		memset(pss->pss_gs, 0, vhd->gsp->per_session_data_size);
+		break;
+
+	case LWS_CALLBACK_HTTP_DROP_PROTOCOL:
+		if (vhd->gsp->callback(wsi, reason, pss ? pss->pss_gs : NULL, in, len))
+			return -1;
+
+		if (pss->pss_gs) {
+			free(pss->pss_gs);
+			pss->pss_gs = NULL;
+		}
+		break;
+```
+
+
+Getting session-specific information from another protocol
+----------------------------------------------------------
+
+At least at the time when someone tries to upgrade an http(s) connection to
+ws(s) with your real protocol, it is necessary to confirm the cookie the http(s)
+connection has with generic-sessions and find out his username and other info.
+
+Generic sessions lets another protocol check it again by calling his callback,
+and lws itself provides a generic session info struct to pass the related data
+
+```
+struct lws_session_info {
+	char username[32];
+	char email[100];
+	char ip[72];
+	unsigned int mask;
+	char session[42];
+};
+```
+
+```
+	struct lws_session_info sinfo;
+	...
+	vhd->gsp->callback(wsi, LWS_CALLBACK_SESSION_INFO,
+				   &pss->pss_gs, &sinfo, 0);
+```
+
+After the call to generic-sessions, the results can be
+
+ -  all the strings will be zero-length and .mask zero, there is no usable cookie
+ 
+  - only .ip and .session are set: the cookie is OK but no user logged in
+  
+  - all the strings contain information about the logged-in user
+
+the real protocol can use this to reject attempts to open ws connections from
+http connections that are not authenticated; afterwards there's no need to
+check the ws connection auth status again.
+
diff --git a/appveyor.yml b/appveyor.yml
index b5cb9afcd7a1acc991b2875fa25da609ba27eb34..4bbc0f1aef4e03aded2ffcb6f2c12c760994b326 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -1,7 +1,7 @@
 environment:
   matrix:
     - LWS_METHOD: lwsws
-      CMAKE_ARGS: -DLWS_WITH_LWSWS=1 -DLIBUV_INCLUDE_DIRS=C:\assets\libuv\include -DLIBUV_LIBRARIES=C:\assets\libuv\libuv.lib
+      CMAKE_ARGS: -DLWS_WITH_LWSWS=1 -DSQLITE3_INCLUDE_DIRS=C:\assets\sqlite3 -DSQLITE3_LIBRARIES=C:\assets\sqlite3\sqlite3.lib -DLIBUV_INCLUDE_DIRS=C:\assets\libuv\include -DLIBUV_LIBRARIES=C:\assets\libuv\libuv.lib
 
     - LWS_METHOD: default
 
@@ -26,6 +26,9 @@ install:
   - Win32OpenSSL-1_0_2h.exe /silent /verysilent /sp- /suppressmsgboxes
   - appveyor DownloadFile https://libwebsockets.org:444/nsis-3.0rc1-setup.exe
   - cmd /c start /wait nsis-3.0rc1-setup.exe /S /D=C:\nsis
+  - appveyor DownloadFile https://libwebsockets.org:444/sqlite-dll-win32-x86-3130000.zip
+  - mkdir c:\assets\sqlite3
+  - 7z x -oc:\assets\sqlite3 sqlite-dll-win32-x86-3130000.zip
   - SET PATH=C:\Program Files\NSIS\;C:\Program Files (x86)\NSIS\;c:\nsis;%PATH%
 build:
 
diff --git a/lib/context.c b/lib/context.c
index 0d3a41c13ec3f74ea2039fdbeaeb25781b10c65f..d3c22d6b7477ed6f0670c609aaee1bf0878836d8 100644
--- a/lib/context.c
+++ b/lib/context.c
@@ -67,8 +67,15 @@ lws_protocol_vh_priv_zalloc(struct lws_vhost *vhost, const struct lws_protocols
 	while (n < vhost->count_protocols && &vhost->protocols[n] != prot)
 		n++;
 
-	if (n == vhost->count_protocols)
-		return NULL;
+	if (n == vhost->count_protocols) {
+		n = 0;
+		while (n < vhost->count_protocols &&
+		       strcmp(vhost->protocols[n].name, prot->name))
+			n++;
+
+		if (n == vhost->count_protocols)
+			return NULL;
+	}
 
 	vhost->protocol_vh_privs[n] = lws_zalloc(size);
 	return vhost->protocol_vh_privs[n];
@@ -86,8 +93,15 @@ lws_protocol_vh_priv_get(struct lws_vhost *vhost, const struct lws_protocols *pr
 		n++;
 
 	if (n == vhost->count_protocols) {
-		lwsl_err("%s: unknown protocol %p\n", __func__, prot);
-		return NULL;
+		n = 0;
+		while (n < vhost->count_protocols &&
+		       strcmp(vhost->protocols[n].name, prot->name))
+			n++;
+
+		if (n == vhost->count_protocols) {
+			lwsl_err("%s: unknown protocol %p\n", __func__, prot);
+			return NULL;
+		}
 	}
 
 	return vhost->protocol_vh_privs[n];
@@ -165,15 +179,17 @@ lws_protocol_init(struct lws_context *context)
 			 * NOTE the wsi is all zeros except for the context, vh and
 			 * protocol ptrs so lws_get_context(wsi) etc can work
 			 */
-			vh->protocols[n].callback(&wsi,
+			if (vh->protocols[n].callback(&wsi,
 				LWS_CALLBACK_PROTOCOL_INIT, NULL,
-				(void *)pvo, 0);
+				(void *)pvo, 0))
+				return 1;
 		}
 
 		vh = vh->vhost_next;
 	}
 
 	context->protocol_init_done = 1;
+	lws_finalize_startup(context);
 
 	return 0;
 }
@@ -287,12 +303,14 @@ lws_create_vhost(struct lws_context *context,
 	struct lws_vhost *vh = lws_zalloc(sizeof(*vh)),
 			 **vh1 = &context->vhost_list;
 	const struct lws_http_mount *mounts;
+	const struct lws_protocol_vhost_options *pvo;
 #ifdef LWS_WITH_PLUGINS
 	struct lws_plugin *plugin = context->plugin_list;
 	struct lws_protocols *lwsp;
-	int m, n, f = !info->pvo;
+	int m, f = !info->pvo;
 #endif
 	char *p;
+	int n;
 
 	if (!vh)
 		return NULL;
@@ -381,6 +399,21 @@ lws_create_vhost(struct lws_context *context,
 		lwsl_notice("   mounting %s%s to %s\n",
 				mount_protocols[mounts->origin_protocol],
 				mounts->origin, mounts->mountpoint);
+
+		/* convert interpreter protocol names to pointers */
+		pvo = mounts->interpret;
+		while (pvo) {
+			for (n = 0; n < vh->count_protocols; n++)
+				if (!strcmp(pvo->value, vh->protocols[n].name)) {
+					((struct lws_protocol_vhost_options *)pvo)->value =
+							(const char *)(long)n;
+					break;
+				}
+			if (n == vh->count_protocols)
+				lwsl_err("ignoring unknown interpret protocol %s\n", pvo->value);
+			pvo = pvo->next;
+		}
+
 		mounts = mounts->mount_next;
 	}
 
diff --git a/lib/lejp-conf.c b/lib/lejp-conf.c
index 1d529a1fd31d8386cb9e3050dccaa63a2f8935e5..91429a54c590ab8de49758a61aa441a0c4a1db81 100644
--- a/lib/lejp-conf.c
+++ b/lib/lejp-conf.c
@@ -59,7 +59,9 @@ static const char * const paths_vhosts[] = {
 	"vhosts[].access-log",
 	"vhosts[].mounts[].mountpoint",
 	"vhosts[].mounts[].origin",
+	"vhosts[].mounts[].protocol",
 	"vhosts[].mounts[].default",
+	"vhosts[].mounts[].auth-mask",
 	"vhosts[].mounts[].cgi-timeout",
 	"vhosts[].mounts[].cgi-env[].*",
 	"vhosts[].mounts[].cache-max-age",
@@ -67,6 +69,7 @@ static const char * const paths_vhosts[] = {
 	"vhosts[].mounts[].cache-revalidate",
 	"vhosts[].mounts[].cache-intermediaries",
 	"vhosts[].mounts[].extra-mimetypes.*",
+	"vhosts[].mounts[].interpret.*",
 	"vhosts[].ws-protocols[].*.*",
 	"vhosts[].ws-protocols[].*",
 	"vhosts[].ws-protocols[]",
@@ -94,7 +97,9 @@ enum lejp_vhost_paths {
 	LEJPVP_ACCESS_LOG,
 	LEJPVP_MOUNTPOINT,
 	LEJPVP_ORIGIN,
+	LEJPVP_MOUNT_PROTOCOL,
 	LEJPVP_DEFAULT,
+	LEJPVP_DEFAULT_AUTH_MASK,
 	LEJPVP_CGI_TIMEOUT,
 	LEJPVP_CGI_ENV,
 	LEJPVP_MOUNT_CACHE_MAX_AGE,
@@ -102,6 +107,7 @@ enum lejp_vhost_paths {
 	LEJPVP_MOUNT_CACHE_REVALIDATE,
 	LEJPVP_MOUNT_CACHE_INTERMEDIARIES,
 	LEJPVP_MOUNT_EXTRA_MIMETYPES,
+	LEJPVP_MOUNT_INTERPRET,
 	LEJPVP_PROTOCOL_NAME_OPT,
 	LEJPVP_PROTOCOL_NAME,
 	LEJPVP_PROTOCOL,
@@ -154,6 +160,7 @@ struct jpargs {
 
 	struct lws_protocol_vhost_options *pvo;
 	struct lws_protocol_vhost_options *pvo_em;
+	struct lws_protocol_vhost_options *pvo_int;
 	struct lws_http_mount m;
 	const char **plugin_dirs;
 	int count_plugin_dirs;
@@ -363,8 +370,10 @@ lejp_vhosts_cb(struct lejp_ctx *ctx, char reason)
 		for (n = 0; n < ARRAY_SIZE(mount_protocols); n++)
 			if (!strncmp(a->m.origin, mount_protocols[n],
 			     strlen(mount_protocols[n]))) {
+				lwsl_err("----%s\n", a->m.origin);
 				m->origin_protocol = n;
-				m->origin = a->m.origin + strlen(mount_protocols[n]);
+				m->origin = a->m.origin +
+					    strlen(mount_protocols[n]);
 				break;
 			}
 
@@ -424,11 +433,18 @@ lejp_vhosts_cb(struct lejp_ctx *ctx, char reason)
 		a->m.mountpoint_len = (unsigned char)strlen(ctx->buf);
 		break;
 	case LEJPVP_ORIGIN:
-		a->m.origin = a->p;
+		if (!strncmp(ctx->buf, "callback://", 11))
+			a->m.protocol = a->p + 11;
+
+		if (!a->m.origin)
+			a->m.origin = a->p;
 		break;
 	case LEJPVP_DEFAULT:
 		a->m.def = a->p;
 		break;
+	case LEJPVP_DEFAULT_AUTH_MASK:
+		a->m.auth_mask = atoi(ctx->buf);
+		return 0;
 	case LEJPVP_MOUNT_CACHE_MAX_AGE:
 		a->m.cache_max_age = atoi(ctx->buf);
 		return 0;
@@ -505,6 +521,22 @@ lejp_vhosts_cb(struct lejp_ctx *ctx, char reason)
 		a->pvo_em->options = NULL;
 		break;
 
+	case LEJPVP_MOUNT_INTERPRET:
+		a->pvo_int = lwsws_align(a);
+		a->p += sizeof(*a->pvo_int);
+
+		n = lejp_get_wildcard(ctx, 0, a->p, a->end - a->p);
+		/* ie, enable this protocol, no options yet */
+		a->pvo_int->next = a->m.interpret;
+		a->m.interpret = a->pvo_int;
+		a->pvo_int->name = a->p;
+		lwsl_notice("  adding interpret %s -> %s\n", a->p,
+			    ctx->buf);
+		a->p += n;
+		a->pvo_int->value = a->p;
+		a->pvo_int->options = NULL;
+		break;
+
 	case LEJPVP_ENABLE_CLIENT_SSL:
 		a->enable_client_ssl = arg_to_bool(ctx->buf);
 		return 0;
diff --git a/lib/libwebsockets.c b/lib/libwebsockets.c
index cac78db5b662136dd9fd7f85988531a011aac89e..f8d08fc890169493fbd4c2ac4c9de59e0ec311d8 100755
--- a/lib/libwebsockets.c
+++ b/lib/libwebsockets.c
@@ -1735,39 +1735,6 @@ lws_socket_bind(struct lws_vhost *vhost, int sockfd, int port,
 
 static const char *hex = "0123456789ABCDEF";
 
-static int
-urlencode(const char *in, int inlen, char *out, int outlen)
-{
-	char *start = out, *end = out + outlen;
-
-	while (inlen-- && out < end - 4) {
-		if ((*in >= 'A' && *in <= 'Z') ||
-		    (*in >= 'a' && *in <= 'z') ||
-		    (*in >= '0' && *in <= '9') ||
-		    *in == '-' ||
-		    *in == '_' ||
-		    *in == '.' ||
-		    *in == '~') {
-			*out++ = *in++;
-			continue;
-		}
-		if (*in == ' ') {
-			*out++ = '+';
-			in++;
-			continue;
-		}
-		*out++ = '%';
-		*out++ = hex[(*in) >> 4];
-		*out++ = hex[(*in++) & 15];
-	}
-	*out = '\0';
-
-	if (out >= end - 4)
-		return -1;
-
-	return out - start;
-}
-
 /**
  * lws_sql_purify() - like strncpy but with escaping for sql quotes
  *
@@ -1787,7 +1754,7 @@ lws_sql_purify(char *escaped, const char *string, int len)
 
 	while (*p && len-- > 2) {
 		if (*p == '\'') {
-			*q++ = '\\';
+			*q++ = '\'';
 			*q++ = '\'';
 			len --;
 			p++;
@@ -1964,6 +1931,39 @@ lws_is_cgi(struct lws *wsi) {
 
 #ifdef LWS_WITH_CGI
 
+static int
+urlencode(const char *in, int inlen, char *out, int outlen)
+{
+	char *start = out, *end = out + outlen;
+
+	while (inlen-- && out < end - 4) {
+		if ((*in >= 'A' && *in <= 'Z') ||
+		    (*in >= 'a' && *in <= 'z') ||
+		    (*in >= '0' && *in <= '9') ||
+		    *in == '-' ||
+		    *in == '_' ||
+		    *in == '.' ||
+		    *in == '~') {
+			*out++ = *in++;
+			continue;
+		}
+		if (*in == ' ') {
+			*out++ = '+';
+			in++;
+			continue;
+		}
+		*out++ = '%';
+		*out++ = hex[(*in) >> 4];
+		*out++ = hex[(*in++) & 15];
+	}
+	*out = '\0';
+
+	if (out >= end - 4)
+		return -1;
+
+	return out - start;
+}
+
 static struct lws *
 lws_create_basic_wsi(struct lws_context *context, int tsi)
 {
diff --git a/lib/libwebsockets.h b/lib/libwebsockets.h
index b6ec9bbccffd324fed9a0ad9649ed0045f9b3d54..11f6fffcd1f9cf35d8577e1f7941f162151c7895 100644
--- a/lib/libwebsockets.h
+++ b/lib/libwebsockets.h
@@ -476,6 +476,12 @@ enum lws_callback_reasons {
 	LWS_CALLBACK_COMPLETED_CLIENT_HTTP			= 47,
 	LWS_CALLBACK_RECEIVE_CLIENT_HTTP_READ			= 48,
 	LWS_CALLBACK_HTTP_DROP_PROTOCOL				= 49,
+	LWS_CALLBACK_CHECK_ACCESS_RIGHTS			= 50,
+	LWS_CALLBACK_PROCESS_HTML				= 51,
+	LWS_CALLBACK_ADD_HEADERS				= 52,
+	LWS_CALLBACK_SESSION_INFO				= 53,
+
+	LWS_CALLBACK_GS_EVENT					= 54,
 
 	/****** add new things just above ---^ ******/
 
@@ -1338,6 +1344,14 @@ struct lws_protocols {
 	 * This is part of the ABI, don't needlessly break compatibility */
 };
 
+struct lws_session_info {
+	char username[32];
+	char email[100];
+	char ip[72];
+	unsigned int mask;
+	char session[42];
+};
+
 struct lws_process_html_args {
 	char *p;
 	int len;
@@ -1362,6 +1376,33 @@ LWS_VISIBLE LWS_EXTERN int
 lws_chunked_html_process(struct lws_process_html_args *args,
 			 struct lws_process_html_state *s);
 
+/* generic-sessions public api */
+
+#define LWSGS_EMAIL_CONTENT_SIZE 16384
+
+/* SHA-1 binary and hexified versions */
+typedef struct { unsigned char bin[20]; } lwsgw_hash_bin;
+typedef struct { char id[41]; } lwsgw_hash;
+
+enum lwsgs_auth_bits {
+	LWSGS_AUTH_LOGGED_IN = 1,
+	LWSGS_AUTH_ADMIN = 2,
+	LWSGS_AUTH_VERIFIED = 4,
+	LWSGS_AUTH_FORGOT_FLOW = 8,
+};
+
+enum lws_gs_event {
+	LWSGSE_CREATED,
+	LWSGSE_DELETED
+};
+
+struct lws_gs_event_args {
+	enum lws_gs_event event;
+	const char *username;
+	const char *email;
+};
+
+
 enum lws_ext_options_types {
 	EXTARG_NONE,
 	EXTARG_DEC,
@@ -1470,12 +1511,15 @@ struct lws_http_mount {
 	const char *mountpoint; /* mountpoint in http pathspace, eg, "/" */
 	const char *origin; /* path to be mounted, eg, "/var/www/warmcat.com" */
 	const char *def; /* default target, eg, "index.html" */
+	const char *protocol; /* "protocol-name" to handle mount */
 
 	const struct lws_protocol_vhost_options *cgienv;
 	const struct lws_protocol_vhost_options *extra_mimetypes;
+	const struct lws_protocol_vhost_options *interpret;
 
 	int cgi_timeout;
 	int cache_max_age;
+	unsigned int auth_mask;
 
 	unsigned int cache_reusable:1;
 	unsigned int cache_revalidate:1;
@@ -1737,6 +1781,9 @@ LWS_VISIBLE LWS_EXTERN int
 lws_init_vhost_client_ssl(const struct lws_context_creation_info *info,
 			  struct lws_vhost *vhost);
 
+LWS_VISIBLE LWS_EXTERN const struct lws_protocols *
+lws_vhost_name_to_protocol(struct lws_vhost *vh, const char *name);
+
 /* deprecated: use lws_get_vhost() */
 LWS_VISIBLE LWS_EXTERN struct lws_vhost *
 lws_vhost_get(struct lws *wsi) LWS_WARN_DEPRECATED;
diff --git a/lib/output.c b/lib/output.c
index f14a4a3f5216950dc4d3cf5ee5f1f3b34e82f4ce..be0c73c59ce0bbdf59833f80dd1d4fabc24e5778 100644
--- a/lib/output.c
+++ b/lib/output.c
@@ -568,7 +568,9 @@ LWS_VISIBLE int lws_serve_http_file_fragment(struct lws *wsi)
 {
 	struct lws_context *context = wsi->context;
 	struct lws_context_per_thread *pt = &context->pt[(int)wsi->tsi];
-	unsigned long amount;
+	struct lws_process_html_args args;
+	unsigned long amount, poss;
+	unsigned char *p = pt->serv_buf;
 	int n, m;
 
 	while (wsi->http2_substream || !lws_send_pipe_choked(wsi)) {
@@ -585,31 +587,58 @@ LWS_VISIBLE int lws_serve_http_file_fragment(struct lws *wsi)
 		if (wsi->u.http.filepos == wsi->u.http.filelen)
 			goto all_sent;
 
-		if (lws_plat_file_read(wsi, wsi->u.http.fd, &amount,
-				       pt->serv_buf,
-				       context->pt_serv_buf_size) < 0)
+		poss = context->pt_serv_buf_size;
+
+		if (wsi->sending_chunked) {
+			/* we need to drop the chunk size in here */
+			p += 10;
+			/* allow for the chunk to grow by 128 in translation */
+			poss -= 10 + 128;
+		}
+
+		if (lws_plat_file_read(wsi, wsi->u.http.fd, &amount, p, poss) < 0)
 			return -1; /* caller will close */
 
 		n = (int)amount;
 		if (n) {
 			lws_set_timeout(wsi, PENDING_TIMEOUT_HTTP_CONTENT,
 					context->timeout_secs);
-			wsi->u.http.filepos += n;
-			m = lws_write(wsi, pt->serv_buf, n,
+
+			if (wsi->sending_chunked) {
+				args.p = (char *)p;
+				args.len = n;
+				args.max_len = poss + 128;
+				args.final = wsi->u.http.filepos + n ==
+					     wsi->u.http.filelen;
+				if (user_callback_handle_rxflow(
+				     wsi->vhost->protocols[(int)wsi->protocol_interpret_idx].callback, wsi,
+				     LWS_CALLBACK_PROCESS_HTML,
+				     wsi->user_space, &args, 0) < 0)
+					return -1;
+				n = args.len;
+				p = (unsigned char *)args.p;
+			}
+
+			m = lws_write(wsi, p, n,
 				      wsi->u.http.filepos == wsi->u.http.filelen ?
-					LWS_WRITE_HTTP_FINAL : LWS_WRITE_HTTP);
+					LWS_WRITE_HTTP_FINAL :
+					LWS_WRITE_HTTP
+				);
 			if (m < 0)
 				return -1;
 
-			if (m != n)
+			wsi->u.http.filepos += amount;
+			if (m != n) {
 				/* adjust for what was not sent */
 				if (lws_plat_file_seek_cur(wsi, wsi->u.http.fd,
 							   m - n) ==
 							     (unsigned long)-1)
 					return -1;
+			}
 		}
 all_sent:
-		if (!wsi->trunc_len && wsi->u.http.filepos == wsi->u.http.filelen) {
+		if (!wsi->trunc_len &&
+		    wsi->u.http.filepos == wsi->u.http.filelen) {
 			wsi->state = LWSS_HTTP;
 			/* we might be in keepalive, so close it off here */
 			lws_plat_file_close(wsi, wsi->u.http.fd);
@@ -622,11 +651,11 @@ all_sent:
 				     LWS_CALLBACK_HTTP_FILE_COMPLETION,
 				     wsi->user_space, NULL, 0) < 0)
 					return -1;
+
 			return 1;  /* >0 indicates completed */
 		}
 	}
 
-	lwsl_info("choked before able to send whole file (post)\n");
 	lws_callback_on_writable(wsi);
 
 	return 0; /* indicates further processing must be done */
diff --git a/lib/private-libwebsockets.h b/lib/private-libwebsockets.h
index 5ce641314f463759f56caf1d3d806a1ffa9ccb95..12719fcbc7af9739e5ec77400091593e357d6219 100644
--- a/lib/private-libwebsockets.h
+++ b/lib/private-libwebsockets.h
@@ -1257,6 +1257,7 @@ struct lws {
 	unsigned int cache_revalidate:1;
 	unsigned int cache_intermediaries:1;
 	unsigned int favoured_pollin:1;
+	unsigned int sending_chunked:1;
 #ifdef LWS_WITH_ACCESS_LOG
 	unsigned int access_log_pending:1;
 #endif
@@ -1295,6 +1296,7 @@ struct lws {
 	char pending_timeout; /* enum pending_timeout */
 	char pps; /* enum lws_pending_protocol_send */
 	char tsi; /* thread service index we belong to */
+	char protocol_interpret_idx;
 #ifdef LWS_WITH_CGI
 	char cgi_channel; /* which of stdin/out/err */
 	char hdr_state;
diff --git a/lib/server.c b/lib/server.c
index 8aafdbf58de1bc66b4fc30ab3d6d40b733b62db9..d2398e31f2191cca14f0074666ce8a72e744a598 100644
--- a/lib/server.c
+++ b/lib/server.c
@@ -206,6 +206,27 @@ lws_select_vhost(struct lws_context *context, int port, const char *servername)
 	return NULL;
 }
 
+/**
+ * lws_vhost_name_to_protocol() - get vhost's protocol object from its name
+ *
+ * @vh: vhost to search
+ * @name: protocol name
+ *
+ * Returns NULL or a pointer to the vhost's protocol of the requested name
+ */
+
+LWS_VISIBLE LWS_EXTERN const struct lws_protocols *
+lws_vhost_name_to_protocol(struct lws_vhost *vh, const char *name)
+{
+	int n;
+
+	for (n = 0; n < vh->count_protocols; n++)
+		if (!strcmp(name, vh->protocols[n].name))
+			return &vh->protocols[n];
+
+	return NULL;
+}
+
 static const char *
 get_mimetype(const char *file, const struct lws_http_mount *m)
 {
@@ -271,11 +292,13 @@ static int
 lws_http_serve(struct lws *wsi, char *uri, const char *origin,
 	       const struct lws_http_mount *m)
 {
+	const struct lws_protocol_vhost_options *pvo = m->interpret;
+	struct lws_process_html_args args;
 	const char *mimetype;
 #ifndef _WIN32_WCE
 	struct stat st;
 #endif
-	char path[256], sym[256];
+	char path[256], sym[512];
 	unsigned char *p = (unsigned char *)sym + 32 + LWS_PRE, *start = p;
 	unsigned char *end = p + sizeof(sym) - 32 - LWS_PRE;
 #if !defined(WIN32)
@@ -342,7 +365,7 @@ lws_http_serve(struct lws *wsi, char *uri, const char *origin,
 				return -1;
 
 			n = lws_write(wsi, start, p - start,
-					LWS_WRITE_HTTP_HEADERS);
+				      LWS_WRITE_HTTP_HEADERS);
 			if (n != (p - start)) {
 				lwsl_err("_write returned %d from %d\n", n, p - start);
 				return -1;
@@ -363,6 +386,43 @@ lws_http_serve(struct lws *wsi, char *uri, const char *origin,
 		goto bail;
 	}
 
+	wsi->sending_chunked = 0;
+
+	/*
+	 * check if this is in the list of file suffixes to be interpreted by
+	 * a protocol
+	 */
+	while (pvo) {
+		n = strlen(path);
+		if (n > (int)strlen(pvo->name) &&
+		    !strcmp(&path[n - strlen(pvo->name)], pvo->name)) {
+			wsi->sending_chunked = 1;
+			wsi->protocol_interpret_idx = (char)(long)pvo->value;
+			lwsl_info("want %s interpreted by %s\n", path,
+				    wsi->vhost->protocols[(int)(long)(pvo->value)].name);
+			wsi->protocol = &wsi->vhost->protocols[(int)(long)(pvo->value)];
+			if (lws_ensure_user_space(wsi))
+				return -1;
+			break;
+		}
+		pvo = pvo->next;
+	}
+
+	if (m->protocol) {
+		const struct lws_protocols *pp = lws_vhost_name_to_protocol(
+							wsi->vhost, m->protocol);
+
+		wsi->protocol = pp;
+		if (lws_ensure_user_space(wsi))
+			return -1;
+		args.p = (char *)p;
+		args.max_len = end - p;
+		if (pp->callback(wsi, LWS_CALLBACK_ADD_HEADERS,
+					  wsi->user_space, &args, 0))
+			return -1;
+		p = (unsigned char *)args.p;
+	}
+
 	n = lws_serve_http_file(wsi, path, mimetype, (char *)start, p - start);
 
 	if (n < 0 || ((n > 0) && lws_http_transaction_completed(wsi)))
@@ -381,6 +441,7 @@ lws_http_action(struct lws *wsi)
 	enum http_connection_type connection_type;
 	enum http_version request_version;
 	char content_length_str[32];
+	struct lws_process_html_args args;
 	const struct lws_http_mount *hm, *hit = NULL;
 	unsigned int n, count = 0;
 	char http_version_str[10];
@@ -409,6 +470,9 @@ lws_http_action(struct lws *wsi)
 #endif
 	};
 #endif
+	static const char * const oprot[] = {
+		"http://", "https://"
+	};
 
 	/* it's not websocket.... shall we accept it as http? */
 
@@ -614,7 +678,8 @@ lws_http_action(struct lws *wsi)
 		    ) {
 			if (hm->origin_protocol == LWSMPRO_CALLBACK ||
 			    ((hm->origin_protocol == LWSMPRO_CGI ||
-			     lws_hdr_total_length(wsi, WSI_TOKEN_GET_URI)) &&
+			     lws_hdr_total_length(wsi, WSI_TOKEN_GET_URI) ||
+			     hm->protocol) &&
 			    hm->mountpoint_len > best)) {
 				best = hm->mountpoint_len;
 				hit = hm;
@@ -628,6 +693,38 @@ lws_http_action(struct lws *wsi)
 		lwsl_debug("*** hit %d %d %s\n", hit->mountpoint_len,
 			   hit->origin_protocol , hit->origin);
 
+		if (hit->protocol) {
+			const struct lws_protocols *pp = lws_vhost_name_to_protocol(
+					wsi->vhost, hit->protocol);
+
+			if (!pp) {
+				lwsl_err("unknown protocol %s\n", hit->protocol);
+				return 1;
+			}
+
+			wsi->protocol = pp;
+			if (lws_ensure_user_space(wsi)) {
+				lwsl_err("Unable to allocate user space\n");
+				return 1;
+			}
+		}
+		lwsl_info("wsi %s protocol '%s'\n", uri_ptr, wsi->protocol->name);
+
+		args.p = uri_ptr;
+		args.len = uri_len;
+		args.max_len = hit->auth_mask;
+		args.final = 0; /* used to signal callback dealt with it */
+
+		n = wsi->protocol->callback(wsi, LWS_CALLBACK_CHECK_ACCESS_RIGHTS,
+					    wsi->user_space, &args, 0);
+		if (n) {
+			lws_return_http_status(wsi, HTTP_STATUS_UNAUTHORIZED,
+					       NULL);
+			goto bail_nuke_ah;
+		}
+		if (args.final) /* callback completely handled it well */
+			return 0;
+
 		/*
 		 * if we have a mountpoint like https://xxx.com/yyy
 		 * there is an implied / at the end for our purposes since
@@ -649,12 +746,12 @@ lws_http_action(struct lws *wsi)
 		    (*s != '/' ||
 		     (hit->origin_protocol == LWSMPRO_REDIR_HTTP ||
 		      hit->origin_protocol == LWSMPRO_REDIR_HTTPS)) &&
-		    (hit->origin_protocol != LWSMPRO_CGI && hit->origin_protocol != LWSMPRO_CALLBACK)) {
+		    (hit->origin_protocol != LWSMPRO_CGI &&
+		     hit->origin_protocol != LWSMPRO_CALLBACK //&&
+		     //hit->protocol == NULL
+		     )) {
 			unsigned char *start = pt->serv_buf + LWS_PRE,
 					      *p = start, *end = p + 512;
-			static const char *oprot[] = {
-				"http://", "https://"
-			};
 
 			lwsl_debug("Doing 301 '%s' org %s\n", s, hit->origin);
 
@@ -662,13 +759,14 @@ lws_http_action(struct lws *wsi)
 				goto bail_nuke_ah;
 
 			/* > at start indicates deal with by redirect */
-			if (hit->origin_protocol & 4)
+			if (hit->origin_protocol == LWSMPRO_REDIR_HTTP ||
+			    hit->origin_protocol == LWSMPRO_REDIR_HTTPS)
 				n = snprintf((char *)end, 256, "%s%s",
 					    oprot[hit->origin_protocol & 1],
 					    hit->origin);
 			else
 				n = snprintf((char *)end, 256,
-				    "https://%s/%s/",
+				    "%s%s%s/", oprot[lws_is_ssl(wsi)],
 				    lws_hdr_simple_ptr(wsi, WSI_TOKEN_HOST),
 				    uri_ptr);
 
@@ -686,32 +784,36 @@ lws_http_action(struct lws *wsi)
 		 * For the duration of this http transaction, bind us to the
 		 * associated protocol
 		 */
-		if (hit->origin_protocol == LWSMPRO_CALLBACK) {
-
-			for (n = 0; n < (unsigned int)wsi->vhost->count_protocols; n++)
-				if (!strcmp(wsi->vhost->protocols[n].name,
-					   hit->origin)) {
 
-					if (wsi->protocol != &wsi->vhost->protocols[n])
-						if (!wsi->user_space_externally_allocated)
-							lws_free_set_NULL(wsi->user_space);
-					wsi->protocol = &wsi->vhost->protocols[n];
-					if (lws_ensure_user_space(wsi)) {
-						lwsl_err("Unable to allocate user space\n");
-
-						return 1;
+		if (hit->origin_protocol == LWSMPRO_CALLBACK ||
+		    (hit->protocol && lws_hdr_total_length(wsi, WSI_TOKEN_POST_URI))) {
+			if (! hit->protocol) {
+				for (n = 0; n < (unsigned int)wsi->vhost->count_protocols; n++)
+					if (!strcmp(wsi->vhost->protocols[n].name,
+						   hit->origin)) {
+
+						if (wsi->protocol != &wsi->vhost->protocols[n])
+							if (!wsi->user_space_externally_allocated)
+								lws_free_set_NULL(wsi->user_space);
+						wsi->protocol = &wsi->vhost->protocols[n];
+						if (lws_ensure_user_space(wsi)) {
+							lwsl_err("Unable to allocate user space\n");
+
+							return 1;
+						}
+						break;
 					}
-					break;
-				}
 
-			if (n == wsi->vhost->count_protocols) {
-				n = -1;
-				lwsl_err("Unable to find plugin '%s'\n",
-					 hit->origin);
+				if (n == wsi->vhost->count_protocols) {
+					n = -1;
+					lwsl_err("Unable to find plugin '%s'\n",
+						 hit->origin);
+				}
 			}
-
 			n = wsi->protocol->callback(wsi, LWS_CALLBACK_HTTP,
-						    wsi->user_space, uri_ptr, uri_len);
+						    wsi->user_space,
+						    uri_ptr + hit->mountpoint_len,
+						    uri_len - hit->mountpoint_len);
 
 			goto after;
 		}
@@ -765,17 +867,35 @@ lws_http_action(struct lws *wsi)
 		wsi->cache_revalidate = hit->cache_revalidate;
 		wsi->cache_intermediaries = hit->cache_intermediaries;
 
+
 		n = lws_http_serve(wsi, s, hit->origin, hit);
 		if (n) {
 			/*
 			 * 	lws_return_http_status(wsi, HTTP_STATUS_NOT_FOUND, NULL);
 			 */
-			n = wsi->protocol->callback(wsi, LWS_CALLBACK_HTTP,
+			if (hit->protocol) {
+				const struct lws_protocols *pp = lws_vhost_name_to_protocol(
+						wsi->vhost, hit->protocol);
+
+				wsi->protocol = pp;
+				if (lws_ensure_user_space(wsi)) {
+					lwsl_err("Unable to allocate user space\n");
+					return 1;
+				}
+
+				n = pp->callback(wsi, LWS_CALLBACK_HTTP,
+						 wsi->user_space,
+						 uri_ptr + hit->mountpoint_len,
+						 uri_len - hit->mountpoint_len);
+			} else
+				n = wsi->protocol->callback(wsi, LWS_CALLBACK_HTTP,
 					    wsi->user_space, uri_ptr, uri_len);
 		}
 	} else {
 		/* deferred cleanup and reset to protocols[0] */
 
+		lwsl_notice("no hit\n");
+
 		if (wsi->protocol != &wsi->vhost->protocols[0])
 			if (!wsi->user_space_externally_allocated)
 				lws_free_set_NULL(wsi->user_space);
@@ -1283,6 +1403,7 @@ lws_http_transaction_completed(struct lws *wsi)
 	wsi->state = LWSS_HTTP;
 	wsi->mode = LWSCM_HTTP_SERVING;
 	wsi->u.http.content_length = 0;
+	wsi->u.http.content_remain = 0;
 	wsi->hdr_parsing_completed = 0;
 #ifdef LWS_WITH_ACCESS_LOG
 	wsi->access_log.sent = 0;
@@ -1813,8 +1934,16 @@ lws_serve_http_file(struct lws *wsi, const char *file, const char *content_type,
 					 (unsigned char *)content_type,
 					 strlen(content_type), &p, end))
 		return -1;
-	if (lws_add_http_header_content_length(wsi, wsi->u.http.filelen, &p, end))
-		return -1;
+
+	if (!wsi->sending_chunked) {
+		if (lws_add_http_header_content_length(wsi, wsi->u.http.filelen, &p, end))
+			return -1;
+	} else {
+		if (lws_add_http_header_by_token(wsi, WSI_TOKEN_HTTP_TRANSFER_ENCODING,
+						 (unsigned char *)"chunked",
+						 7, &p, end))
+			return -1;
+	}
 
 	if (wsi->cache_secs && wsi->cache_reuse) {
 		if (wsi->cache_revalidate) {
@@ -2277,8 +2406,6 @@ lws_urldecode_s_destroy(struct lws_urldecode_stateful *s)
 {
 	int ret = 0;
 
-	lwsl_notice("%s\n", __func__);
-
 	if (s->state != US_IDLE)
 		ret = -1;
 
diff --git a/plugins/generic-sessions/assets/admin-login.html b/plugins/generic-sessions/assets/admin-login.html
new file mode 100644
index 0000000000000000000000000000000000000000..113df9cd383a9e7f19fb7803dbca5a8d3f9b140a
--- /dev/null
+++ b/plugins/generic-sessions/assets/admin-login.html
@@ -0,0 +1,5 @@
+<html>
+This is an example destination that will appear after successful Admin login.
+
+This URL cannot be served if you're not logged in as admin.
+</html>
diff --git a/plugins/generic-sessions/assets/failed-login.html b/plugins/generic-sessions/assets/failed-login.html
new file mode 100644
index 0000000000000000000000000000000000000000..9ab065b53c0a02bcb79f5e1679c27d3a647884c3
--- /dev/null
+++ b/plugins/generic-sessions/assets/failed-login.html
@@ -0,0 +1,3 @@
+<html>
+This is an example destination that will appear after a failed login
+</html>
diff --git a/plugins/generic-sessions/assets/index.html b/plugins/generic-sessions/assets/index.html
new file mode 100644
index 0000000000000000000000000000000000000000..ea970eecf5ca3100adbe813c175f833ffca8bcae
--- /dev/null
+++ b/plugins/generic-sessions/assets/index.html
@@ -0,0 +1,35 @@
+<html>
+ <head>
+  <script src="lwsgs.js"></script>
+  <style>
+     .body { font-size: 12 }
+     .gstitle { font-size: 18 }
+  </style>
+  </head>
+  <body style="background-image:url(seats.jpg)">
+    <table style="width:100%;transition: max-height 2s;">
+     <tr>
+      <td style="vertical-align:top;text-align:left;width=200px">
+       <img src="lwsgs-logo.png">
+      </td>
+      <td style="vertical-align:top;float:right">
+	<div id=lwsgs style="text-align:right;background-color: rgba(255, 255, 255, 0.8);"></div>
+      </td>
+     </tr>
+     <tr><td colspan=2>
+     	<div id="nolog" style="display:none">
+     	Register / Login to see the messages
+     	</div>
+     	<div id="logged" style="display:none">
+     	Logged in
+     	</div>
+     </td></tr>
+    </table>
+   </form>
+   <script>lwsgs_initial();
+   document.getElementById("nolog").style.display = !!lwsgs_user ? "none" : "inline";
+   document.getElementById("logged").style.display = !lwsgs_user ? "none" : "inline";
+   </script>
+ </body>
+</html>
+
diff --git a/plugins/generic-sessions/assets/lwsgs-logo.png b/plugins/generic-sessions/assets/lwsgs-logo.png
new file mode 100644
index 0000000000000000000000000000000000000000..723a124431189c21c340de517bd9b82cb35374d8
Binary files /dev/null and b/plugins/generic-sessions/assets/lwsgs-logo.png differ
diff --git a/plugins/generic-sessions/assets/lwsgs.js b/plugins/generic-sessions/assets/lwsgs.js
new file mode 100644
index 0000000000000000000000000000000000000000..5362c9a91c1ab6629ee83e00bbbbca0a734ead0b
--- /dev/null
+++ b/plugins/generic-sessions/assets/lwsgs.js
@@ -0,0 +1,476 @@
+<!-- lwsgs rewrites the below $vars $v $v into the correct values on the fly -->
+
+var lwsgs_user = "$lwsgs_user";
+var lwsgs_auth = "$lwsgs_auth";
+var lwsgs_email = "$lwsgs_email";
+
+var lwsgs_html = '\
+	<div id="dlogin" style="display:none"> \
+        <form action="lwsgs-login" method="post"> \
+         <input type="hidden" name="admin" value="needadmin/admin-login.html"> \
+         <input type="hidden" name="good" value="index.html"> \
+         <input type="hidden" name="bad" value="failed-login.html"> \
+         <input type="hidden" name="forgot-good" value="sent-forgot-ok.html"> \
+         <input type="hidden" name="forgot-bad" value="sent-forgot-fail.html">\
+         <input type="hidden" name="forgot-post-good" value="post-forgot-ok.html">\
+         <input type="hidden" name="forgot-post-bad" value="post-forgot-fail.html">\
+	 <table style="vertical-align:top;text-align:right"\
+          <tr>\
+           <td>User Name\
+            <input type="text" size="10" id="username" name="username" oninput="lwsgs_update()" onchange="lwsgs_update()"></td>\
+           <td>Password\
+            <input type="password" id="password" size="10" name="password" oninput="lwsgs_update()" onchange="lwsgs_update()"><div id="pw1"></div></td>\
+	     </tr><tr>\
+	   <td colspan="2" style="text-align:center"><input type="submit" id="login" name="login" value="Login" style="margin: 4px; padding: 2px; font-weight=bold;">\
+      &nbsp;<input type="submit" id="forgot" name="forgot" value="Forgot password" style="margin: 2px; padding: 2px">\
+           &nbsp;<input type="button" onclick="lwsgs_open_registration()" value="Sign up" style="margin: 2px; padding: 2px"></td>\
+          </tr>\
+         </table>\
+        </form>\
+       </div>\
+\
+       <div id="dlogout" style="display:none;text-align:right">\
+        <form action="lwsgs-logout" method="post" style="text-align:right">\
+         <input type="hidden" name="good" value="index.html">\
+         <table style="vertical-align:top;text-align:right">\
+          <tr><td><span id=grav></span></td>\
+ 	   <td style="text-align:center"><table><tr><td style="text-align:center">\
+		<a href="#" onclick="lwsgs_select_change(); event.preventDefault();">\
+		<span id="curuser"></span></a></td></tr><tr>\
+           <td style="text-align:center"><input type="submit" name="logout" value="Logout" style="margin: 2px; padding: 2px"></td>\
+          </tr></table></td></tr>\
+         </table>\
+        </form></div>\
+\
+	<div id="dregister" style="display:none">\
+	 <form action="lwsgs-login" method="post">\
+	  <input type="hidden" name="admin" value="needadmin/admin-login.html">\
+	  <input type="hidden" name="good" value="successful-login.html">\
+	  <input type="hidden" name="bad" value="failed-login.html">\
+	  <input type="hidden" name="reg-good" value="post-register-ok.html">\
+	  <input type="hidden" name="reg-bad" value="post-register-fail.html">\
+	  <input type="hidden" name="forgot-good" value="sent-forgot-ok.html">\
+	  <input type="hidden" name="forgot-bad" value="sent-forgot-fail.html">\
+	  <input type="hidden" name="forgot-post-good" value="post-forgot-ok.html">\
+	  <input type="hidden" name="forgot-post-bad" value="post-forgot-fail.html">\
+	  <table style="vertical-align:top;text-align:left">\
+	     <tr>\
+	      <td colspan=2 align=center>\
+		<span id="curuser"></span>\
+	<script>\
+		if (lwsgs_user)\
+			document.getElementById("curuser").innerHTML = "currently logged in as " + lwsgs_san(lwsgs_user) + "</br>";\
+	</script>\
+	       <b>Please enter your details to register</b>:\
+	      </td>\
+	     </tr>\
+	    <tr><td align=right>\
+	     User Name:</td>\
+	     <td><input type="text" size="10" id="rusername" name="username" oninput="lwsgs_rupdate(); lwsgs_check_user();">&nbsp;<span id=uchk></span></td>\
+	    </tr>\
+	    <tr>\
+	     <td align=right>Password:</td>\
+	     <td><input type="password" size="10" id="rpassword" name="password" oninput="lwsgs_rupdate()">&nbsp;<span id="rpw1"></span></td>\
+	    </tr>\
+	    <tr>\
+	    </tr>\
+	    <tr>\
+	     <td align=right><span id="pw2">Password (again):</span></td>\
+	     <td><input type="password" size="10" id="password2" name="password2" oninput="lwsgs_rupdate()">&nbsp;<span id="match"></span></td>\
+	    </tr>\
+	    <tr>\
+	     <td align=right>Email:</td>\
+	     <td><input type="email" size="10" id="email" name="email"\
+	          placeholder="me@example.com" oninput="lwsgs_rupdate(); lwsgs_check_email(\'email\')">&nbsp;<span id=echk></span></td>\
+	    </tr>\
+	    <tr>\
+	     <td colspan=2 align=center>\
+<input type="submit" id="register" name="register" value="Register" style="margin: 2px; padding: 2px">\
+<input type="submit" id="rforgot" name="forgot" value="Forgot Password" style="margin: 2px; padding: 2px;display: none">\
+<input type="button" id="cancel" name="cancel" value="Cancel" style="margin: 2px; padding: 2px;" onclick="lwsgs_cancel_registration()">\
+	     </td>\
+	    </tr>\
+         </table>\
+        </form>\
+       </div>\
+       \
+       <div id="dchange" style="display:none">\
+        <form action="lwsgs-change" method="post">\
+         <input type="hidden" id="cusername" name="username">\
+         <input type="hidden" name="admin" value="needadmin/admin-login.html">\
+         <input type="hidden" name="good" value="index.html">\
+         <input type="hidden" name="bad" value="failed-login.html">\
+         <input type="hidden" name="reg-good" value="post-register-ok.html">\
+         <input type="hidden" name="reg-bad" value="post-register-fail.html">\
+         <input type="hidden" name="forgot-good" value="sent-forgot-ok.html">\
+         <input type="hidden" name="forgot-bad" value="sent-forgot-fail.html">\
+         <input type="hidden" name="forgot-post-good" value="post-forgot-ok.html">\
+         <input type="hidden" name="forgot-post-bad" value="post-forgot-fail.html">\
+         <table style="vertical-align:top;text-align:left">\
+	     <tr>\
+	      <td colspan=2 align=center>\
+		<span id="ccuruser"></span>\
+	<script>\
+		if (lwsgs_user)\
+			document.getElementById("ccuruser").innerHTML =\
+			  "<span class=\"gstitle\">Login settings for " +\
+			  lwsgs_san(lwsgs_user) + "</span></br>";\
+	</script>\
+	       <b>Please enter your details to change</b>:\
+	      </td>\
+	     </tr>\
+	    <tr><td align=right id="ccurpw_name">\
+	     Current Password:</td>\
+	     <td><input type="password" size="10" id="ccurpw" name="curpw"\
+	          oninput="lwsgs_cupdate();">&nbsp;<span id=cuchk></span></td>\
+	    </tr>\
+	    <tr>\
+	     <td align=right>Password:</td>\
+	     <td><input type="password" size="10" id="cpassword" name="password"\
+	          oninput="lwsgs_cupdate()">&nbsp;<span id="cpw1"></span></td>\
+	    </tr>\
+	    <tr>\
+	     <td align=right><span id="pw2">Password (again)</span></td>\
+	     <td><input type="password" size="10" id="cpassword2" name="password2"\
+	          oninput="lwsgs_cupdate()">&nbsp;<span id="cmatch"></span></td>\
+	    </tr>\
+	<!-- not supported yet\
+	    <tr>\
+	     <td align=right id="cemail_name">Email:</td>\
+	     <td><input type="email" size="10" id="cemail" name="email"\
+	     	  placeholder="?" oninput="lwsgs_cupdate(); lwsgs_check_email(\'cemail\')">\
+	     	  &nbsp;<span id=cechk></span></td>\
+	    </tr> -->\
+	    <tr>\
+	     <td colspan=2 align=center>\
+	      <input type="submit" id="change" name="change"\
+	       value="Change" style="margin: 6px; padding: 6px">\
+	      <input type="submit" id="cforgot" name="forgot"\
+	       value="Forgot Password" style="margin: 6px; padding: 6px;display: none">\
+	      <input type="button" id="cancel" name="cancel"\
+	       value="Cancel" style="margin: 6px; padding: 6px;"\
+	       onclick="lwsgs_cancel_registration()">\
+	     </td>\
+	    </tr>\
+	    <tr>\
+	     <td colspan=2>\
+	      <input type="checkbox" id="showdel" name="showdel"\
+	       onchange="lwsgs_cupdate();"> Show Delete&nbsp;\
+	      <input type="submit" id="delete" name="delete" \
+	       value="Delete Account" style="margin: 6px; padding: 6px;display: none">\
+	     </td>\
+	    </tr>\
+         </table>\
+        </form>\
+       </div>\
+       \
+       <div id="dadmin" style="display:none">\
+         Admin settings TBD\
+       </div>\
+';
+
+/*-- this came from
+  -- https://raw.githubusercontent.com/blueimp/JavaScript-MD5/master/js/md5.min.js
+  -- under MIT license */
+!function(n){"use strict";function t(n,t){var r=(65535&n)+(65535&t),e=(n>>16)+(t>>16)+(r>>16);return e<<16|65535&r}function r(n,t){return n<<t|n>>>32-t}function e(n,e,o,u,c,f){return t(r(t(t(e,n),t(u,f)),c),o)}function o(n,t,r,o,u,c,f){return e(t&r|~t&o,n,t,u,c,f)}function u(n,t,r,o,u,c,f){return e(t&o|r&~o,n,t,u,c,f)}function c(n,t,r,o,u,c,f){return e(t^r^o,n,t,u,c,f)}function f(n,t,r,o,u,c,f){return e(r^(t|~o),n,t,u,c,f)}function i(n,r){n[r>>5]|=128<<r%32,n[(r+64>>>9<<4)+14]=r;var e,i,a,h,d,l=1732584193,g=-271733879,v=-1732584194,m=271733878;for(e=0;e<n.length;e+=16)i=l,a=g,h=v,d=m,l=o(l,g,v,m,n[e],7,-680876936),m=o(m,l,g,v,n[e+1],12,-389564586),v=o(v,m,l,g,n[e+2],17,606105819),g=o(g,v,m,l,n[e+3],22,-1044525330),l=o(l,g,v,m,n[e+4],7,-176418897),m=o(m,l,g,v,n[e+5],12,1200080426),v=o(v,m,l,g,n[e+6],17,-1473231341),g=o(g,v,m,l,n[e+7],22,-45705983),l=o(l,g,v,m,n[e+8],7,1770035416),m=o(m,l,g,v,n[e+9],12,-1958414417),v=o(v,m,l,g,n[e+10],17,-42063),g=o(g,v,m,l,n[e+11],22,-1990404162),l=o(l,g,v,m,n[e+12],7,1804603682),m=o(m,l,g,v,n[e+13],12,-40341101),v=o(v,m,l,g,n[e+14],17,-1502002290),g=o(g,v,m,l,n[e+15],22,1236535329),l=u(l,g,v,m,n[e+1],5,-165796510),m=u(m,l,g,v,n[e+6],9,-1069501632),v=u(v,m,l,g,n[e+11],14,643717713),g=u(g,v,m,l,n[e],20,-373897302),l=u(l,g,v,m,n[e+5],5,-701558691),m=u(m,l,g,v,n[e+10],9,38016083),v=u(v,m,l,g,n[e+15],14,-660478335),g=u(g,v,m,l,n[e+4],20,-405537848),l=u(l,g,v,m,n[e+9],5,568446438),m=u(m,l,g,v,n[e+14],9,-1019803690),v=u(v,m,l,g,n[e+3],14,-187363961),g=u(g,v,m,l,n[e+8],20,1163531501),l=u(l,g,v,m,n[e+13],5,-1444681467),m=u(m,l,g,v,n[e+2],9,-51403784),v=u(v,m,l,g,n[e+7],14,1735328473),g=u(g,v,m,l,n[e+12],20,-1926607734),l=c(l,g,v,m,n[e+5],4,-378558),m=c(m,l,g,v,n[e+8],11,-2022574463),v=c(v,m,l,g,n[e+11],16,1839030562),g=c(g,v,m,l,n[e+14],23,-35309556),l=c(l,g,v,m,n[e+1],4,-1530992060),m=c(m,l,g,v,n[e+4],11,1272893353),v=c(v,m,l,g,n[e+7],16,-155497632),g=c(g,v,m,l,n[e+10],23,-1094730640),l=c(l,g,v,m,n[e+13],4,681279174),m=c(m,l,g,v,n[e],11,-358537222),v=c(v,m,l,g,n[e+3],16,-722521979),g=c(g,v,m,l,n[e+6],23,76029189),l=c(l,g,v,m,n[e+9],4,-640364487),m=c(m,l,g,v,n[e+12],11,-421815835),v=c(v,m,l,g,n[e+15],16,530742520),g=c(g,v,m,l,n[e+2],23,-995338651),l=f(l,g,v,m,n[e],6,-198630844),m=f(m,l,g,v,n[e+7],10,1126891415),v=f(v,m,l,g,n[e+14],15,-1416354905),g=f(g,v,m,l,n[e+5],21,-57434055),l=f(l,g,v,m,n[e+12],6,1700485571),m=f(m,l,g,v,n[e+3],10,-1894986606),v=f(v,m,l,g,n[e+10],15,-1051523),g=f(g,v,m,l,n[e+1],21,-2054922799),l=f(l,g,v,m,n[e+8],6,1873313359),m=f(m,l,g,v,n[e+15],10,-30611744),v=f(v,m,l,g,n[e+6],15,-1560198380),g=f(g,v,m,l,n[e+13],21,1309151649),l=f(l,g,v,m,n[e+4],6,-145523070),m=f(m,l,g,v,n[e+11],10,-1120210379),v=f(v,m,l,g,n[e+2],15,718787259),g=f(g,v,m,l,n[e+9],21,-343485551),l=t(l,i),g=t(g,a),v=t(v,h),m=t(m,d);return[l,g,v,m]}function a(n){var t,r="";for(t=0;t<32*n.length;t+=8)r+=String.fromCharCode(n[t>>5]>>>t%32&255);return r}function h(n){var t,r=[];for(r[(n.length>>2)-1]=void 0,t=0;t<r.length;t+=1)r[t]=0;for(t=0;t<8*n.length;t+=8)r[t>>5]|=(255&n.charCodeAt(t/8))<<t%32;return r}function d(n){return a(i(h(n),8*n.length))}function l(n,t){var r,e,o=h(n),u=[],c=[];for(u[15]=c[15]=void 0,o.length>16&&(o=i(o,8*n.length)),r=0;16>r;r+=1)u[r]=909522486^o[r],c[r]=1549556828^o[r];return e=i(u.concat(h(t)),512+8*t.length),a(i(c.concat(e),640))}function g(n){var t,r,e="0123456789abcdef",o="";for(r=0;r<n.length;r+=1)t=n.charCodeAt(r),o+=e.charAt(t>>>4&15)+e.charAt(15&t);return o}function v(n){return unescape(encodeURIComponent(n))}function m(n){return d(v(n))}function p(n){return g(m(n))}function s(n,t){return l(v(n),v(t))}function C(n,t){return g(s(n,t))}function A(n,t,r){return t?r?s(t,n):C(t,n):r?m(n):p(n)}"function"==typeof define&&define.amd?define(function(){return A}):"object"==typeof module&&module.exports?module.exports=A:n.md5=A}(this);
+
+if (lwsgs_user.substring(0, 1) == "$") {
+	alert("lwsgs.js: lws generic sessions misconfigured and not providing vars");
+}
+function lwsgs_san(s)
+{
+	if (s.search("<") != -1)
+		return "invalid string";
+	
+	return s;
+}
+
+function lwsgs_update()
+{
+	var en_login = 1, en_forgot = 1;
+	
+	if (document.getElementById('password').value.length &&
+	    document.getElementById('password').value.length < 8)
+		en_login = 0;
+	
+	if (!document.getElementById('username').value ||
+	    !document.getElementById('password').value)
+		en_login = 0;
+	
+	if (!document.getElementById('username').value ||
+	     document.getElementById('password').value)
+		en_forgot = 0;
+	
+	document.getElementById('login').disabled = !en_login;
+	document.getElementById('forgot').disabled = !en_forgot;
+	
+	if (lwsgs_user)
+		document.getElementById("curuser").innerHTML = lwsgs_san(lwsgs_user);
+
+	if (lwsgs_user === "")
+		document.getElementById("dlogin").style.display = "inline";
+	else
+		document.getElementById("dlogout").style.display = "inline";
+ }
+
+function lwsgs_open_registration()
+{
+	document.getElementById("dadmin").style.display = "none";
+	document.getElementById("dlogin").style.display = "none";
+	document.getElementById("dlogout").style.display = "none";
+	document.getElementById("dchange").style.display = "none";
+	document.getElementById("dregister").style.display = "inline";
+}
+
+function lwsgs_cancel_registration()
+{
+	document.getElementById("dadmin").style.display = "none";
+	document.getElementById("dregister").style.display = "none";
+	document.getElementById("dchange").style.display = "none";
+
+	if (lwsgs_user === "")
+		document.getElementById("dlogin").style.display = "inline";
+	else
+		document.getElementById("dlogout").style.display = "inline";
+}
+
+function lwsgs_select_change()
+{
+	document.getElementById("dlogin").style.display = "none";
+	document.getElementById("dlogout").style.display = "none";
+	document.getElementById("dregister").style.display = "none";
+	if (lwsgs_auth & 2) {
+		document.getElementById("dadmin").style.display = "inline";
+		document.getElementById("dchange").style.display = "none";
+	} else {
+		document.getElementById("dadmin").style.display = "none";
+		document.getElementById("dchange").style.display = "inline";
+	}
+}
+
+var lwsgs_user_check = '0';
+var lwsgs_email_check = '0';
+
+function lwsgs_rupdate()
+{
+	var en_register = 1, en_forgot = 0;
+
+	if (document.getElementById('rpassword').value ==
+	    document.getElementById('password2').value) {
+		if (document.getElementById('rpassword').value.length)
+			document.getElementById('match').innerHTML = 
+				"<b style=\"color:green\">\u2713</b>";
+		else
+			document.getElementById('match').innerHTML = "";
+		document.getElementById('pw2').style = "";
+	} else {
+		if (document.getElementById('password2').value ||
+		    document.getElementById('email').value) { // ie, he is filling in "register" path and cares
+			document.getElementById('match').innerHTML =
+				"<span style=\"color: red\">\u2718 <b>Passwords do not match</b></span>";
+		} else
+			document.getElementById('match').innerHTML =
+				"<span style=\"color: gray\">\u2718 Passwords do not match</span>";
+
+		en_register = 0;
+	}
+
+	if (document.getElementById('rpassword').value.length &&
+	    document.getElementById('rpassword').value.length < 8) {
+		en_register = 0;
+		document.getElementById('rpw1').innerHTML = "Need 8 chars";
+	} else
+		if (document.getElementById('rpassword').value.length)
+			document.getElementById('rpw1').innerHTML = "<b style=\"color:green\">\u2713</b>";
+		else
+			document.getElementById('rpw1').innerHTML = "";
+
+	if (!document.getElementById('rpassword').value ||
+	    !document.getElementById('password2').value ||
+	    !document.getElementById('rusername').value ||
+	    !document.getElementById('email').value ||
+	    lwsgs_email_check === '1'||
+	    lwsgs_user_check === '1')
+		en_register = 0;
+
+	document.getElementById('register').disabled = !en_register;
+	document.getElementById('rpassword').disabled = lwsgs_user_check === '1';
+	document.getElementById('password2').disabled = lwsgs_user_check === '1';
+	document.getElementById('email').disabled = lwsgs_user_check === '1';
+
+	if (lwsgs_user_check === '0') {
+		if (document.getElementById('rusername').value)
+			document.getElementById('uchk').innerHTML = "<b style=\"color:green\">\u2713</b>";
+		else
+			document.getElementById('uchk').innerHTML = "";
+	} else {
+		document.getElementById('uchk').innerHTML = "<b style=\"color:red\">\u2718 Already registered</b>";
+		en_forgot = 1;
+	}
+
+	if (lwsgs_email_check === '0') {
+		if (document.getElementById('email').value)
+			document.getElementById('echk').innerHTML = "<b style=\"color:green\">\u2713</b>";
+		else
+			document.getElementById('echk').innerHTML = "";
+	} else {
+		document.getElementById('echk').innerHTML = "<b style=\"color:red\">\u2718 Already registered</b>";
+		en_forgot = 1;
+	}
+
+	if (en_forgot)
+		document.getElementById('rforgot').style.display = "inline";
+	else
+		document.getElementById('rforgot').style.display = "none";
+
+	if (lwsgs_user_check === '1')
+		op = '0.5';
+	else
+		op = '1.0';
+	document.getElementById('rpassword').style.opacity = op;
+ 	document.getElementById('password2').style.opacity = op;
+	document.getElementById('email').style.opacity = op;
+ }
+
+function lwsgs_cupdate()
+{
+	var en_change = 1, en_forgot = 1, pwok = 1;
+	
+	if (lwsgs_auth & 8) {
+		document.getElementById('ccurpw').style.display = "none";
+		document.getElementById('ccurpw_name').style.display = "none";
+	} else {
+		if (!document.getElementById('ccurpw').value ||
+		    document.getElementById('ccurpw').value.length < 8) {
+			en_change = 0;
+			pwok = 0;
+			document.getElementById('cuchk').innerHTML = "<b style=\"color:red\">\u2718</b>";
+		} else {
+			en_forgot = 0;
+			document.getElementById('cuchk').innerHTML = "";
+		}
+		document.getElementById('ccurpw').style.display = "inline";
+		document.getElementById('ccurpw_name').style.display = "inline";
+	}
+
+	if (document.getElementById('cpassword').value ==
+	    document.getElementById('cpassword2').value) {
+		if (document.getElementById('cpassword').value.length)
+			document.getElementById('cmatch').innerHTML = "<b style=\"color:green\">\u2713</b>";
+		else
+			document.getElementById('cmatch').innerHTML = "";
+		document.getElementById('pw2').style = "";
+	} else {
+		if (document.getElementById('cpassword2').value //||
+		    //document.getElementById('cemail').value
+		) { // ie, he is filling in "register" path and cares
+			document.getElementById('cmatch').innerHTML =
+				"<span style=\"color: red\">\u2718 <b>Passwords do not match</b></span>";
+		} else
+			document.getElementById('cmatch').innerHTML = "<span style=\"color: gray\">\u2718 Passwords do not match</span>";
+
+		en_change = 0;
+	}
+
+	if (document.getElementById('cpassword').value.length &&
+	    document.getElementById('cpassword').value.length < 8) {
+		en_change = 0;
+		document.getElementById('cpw1').innerHTML = "Need 8 chars";
+	} else
+		if (document.getElementById('cpassword').value.length)
+			document.getElementById('cpw1').innerHTML = "<b style=\"color:green\">\u2713</b>";
+		else
+			document.getElementById('cpw1').innerHTML = "";
+
+	if (!document.getElementById('cpassword').value ||
+	    !document.getElementById('cpassword2').value ||
+	    pwok == 0)
+		en_change = 0;
+	
+	if (document.getElementById('showdel').checked)
+		document.getElementById('delete').style.display = "inline";
+	else
+		document.getElementById('delete').style.display = "none";
+
+	document.getElementById('change').disabled = !en_change;
+	document.getElementById('cpassword').disabled = pwok === 0;
+	document.getElementById('cpassword2').disabled = pwok === 0;
+	document.getElementById('showdel').disabled = pwok === 0;
+	document.getElementById('delete').disabled = pwok === 0;
+	//document.getElementById('cemail').disabled = pwok === 0;
+
+	/*
+	if (lwsgs_auth & 8) {
+		document.getElementById('cemail').style.display = "none";
+		document.getElementById('cemail_name').style.display = "none";
+	} else {
+		document.getElementById('cemail').style.display = "inline";
+		document.getElementById('cemail_name').style.display = "inline";
+		if (lwsgs_email_check === '0'  &&
+		    document.getElementById('cemail').value != lwsgs_email) {
+			if (document.getElementById('cemail').value)
+				document.getElementById('cechk').innerHTML = "<b style=\"color:green\">\u2713</b>";
+			else
+				document.getElementById('cechk').innerHTML = "";
+		} else {
+			document.getElementById('cechk').innerHTML = "<b style=\"color:red\">\u2718 Already registered</b>";
+			en_forgot = 1;
+		}
+	} */
+	
+	if (lwsgs_auth & 8)
+		en_forgot = 0;
+
+	if (en_forgot)
+		document.getElementById('cforgot').style.display = "inline";
+	else
+		document.getElementById('cforgot').style.display = "none";
+
+	if (pwok == 0)
+		op = '0.5';
+	else
+		op = '1.0';
+	document.getElementById('cpassword').style.opacity = op;
+ 	document.getElementById('cpassword2').style.opacity = op;
+	// document.getElementById('cemail').style.opacity = op;
+ }
+
+function lwsgs_check_user()
+{
+    var xmlHttp = new XMLHttpRequest();
+    xmlHttp.onreadystatechange = function() { 
+        if (xmlHttp.readyState == 4 && xmlHttp.status == 200) {
+            lwsgs_user_check = xmlHttp.responseText;
+	    lwsgs_rupdate();
+        }
+    }
+    xmlHttp.open("GET", "lwsgs-check?username="+document.getElementById('rusername').value, true);
+    xmlHttp.send(null);
+}
+
+function lwsgs_check_email(id)
+{
+    var xmlHttp = new XMLHttpRequest();
+    xmlHttp.onreadystatechange = function() { 
+        if (xmlHttp.readyState == 4 && xmlHttp.status == 200) {
+            lwsgs_email_check = xmlHttp.responseText;
+	    lwsgs_rupdate();
+        }
+    }
+    xmlHttp.open("GET", "lwsgs-check?email="+document.getElementById(id).value, true);
+    xmlHttp.send(null);
+}
+
+function lwsgs_initial()
+{
+	document.getElementById('lwsgs').innerHTML = lwsgs_html;
+	if (lwsgs_email)
+		document.getElementById('grav').innerHTML =
+			"<img src=\"https://www.gravatar.com/avatar/" + md5(lwsgs_email) +
+			"?d=identicon\">";
+	//if (lwsgs_email)
+		//document.getElementById('cemail').placeholder = lwsgs_email;
+	document.getElementById('cusername').value = lwsgs_user;
+	lwsgs_update();
+	lwsgs_cupdate();
+}
diff --git a/plugins/generic-sessions/assets/md5.min.js b/plugins/generic-sessions/assets/md5.min.js
new file mode 100644
index 0000000000000000000000000000000000000000..4bd9de1e95be0b14f8ae3453a647a1c9641e6d1f
--- /dev/null
+++ b/plugins/generic-sessions/assets/md5.min.js
@@ -0,0 +1,2 @@
+!function(n){"use strict";function t(n,t){var r=(65535&n)+(65535&t),e=(n>>16)+(t>>16)+(r>>16);return e<<16|65535&r}function r(n,t){return n<<t|n>>>32-t}function e(n,e,o,u,c,f){return t(r(t(t(e,n),t(u,f)),c),o)}function o(n,t,r,o,u,c,f){return e(t&r|~t&o,n,t,u,c,f)}function u(n,t,r,o,u,c,f){return e(t&o|r&~o,n,t,u,c,f)}function c(n,t,r,o,u,c,f){return e(t^r^o,n,t,u,c,f)}function f(n,t,r,o,u,c,f){return e(r^(t|~o),n,t,u,c,f)}function i(n,r){n[r>>5]|=128<<r%32,n[(r+64>>>9<<4)+14]=r;var e,i,a,h,d,l=1732584193,g=-271733879,v=-1732584194,m=271733878;for(e=0;e<n.length;e+=16)i=l,a=g,h=v,d=m,l=o(l,g,v,m,n[e],7,-680876936),m=o(m,l,g,v,n[e+1],12,-389564586),v=o(v,m,l,g,n[e+2],17,606105819),g=o(g,v,m,l,n[e+3],22,-1044525330),l=o(l,g,v,m,n[e+4],7,-176418897),m=o(m,l,g,v,n[e+5],12,1200080426),v=o(v,m,l,g,n[e+6],17,-1473231341),g=o(g,v,m,l,n[e+7],22,-45705983),l=o(l,g,v,m,n[e+8],7,1770035416),m=o(m,l,g,v,n[e+9],12,-1958414417),v=o(v,m,l,g,n[e+10],17,-42063),g=o(g,v,m,l,n[e+11],22,-1990404162),l=o(l,g,v,m,n[e+12],7,1804603682),m=o(m,l,g,v,n[e+13],12,-40341101),v=o(v,m,l,g,n[e+14],17,-1502002290),g=o(g,v,m,l,n[e+15],22,1236535329),l=u(l,g,v,m,n[e+1],5,-165796510),m=u(m,l,g,v,n[e+6],9,-1069501632),v=u(v,m,l,g,n[e+11],14,643717713),g=u(g,v,m,l,n[e],20,-373897302),l=u(l,g,v,m,n[e+5],5,-701558691),m=u(m,l,g,v,n[e+10],9,38016083),v=u(v,m,l,g,n[e+15],14,-660478335),g=u(g,v,m,l,n[e+4],20,-405537848),l=u(l,g,v,m,n[e+9],5,568446438),m=u(m,l,g,v,n[e+14],9,-1019803690),v=u(v,m,l,g,n[e+3],14,-187363961),g=u(g,v,m,l,n[e+8],20,1163531501),l=u(l,g,v,m,n[e+13],5,-1444681467),m=u(m,l,g,v,n[e+2],9,-51403784),v=u(v,m,l,g,n[e+7],14,1735328473),g=u(g,v,m,l,n[e+12],20,-1926607734),l=c(l,g,v,m,n[e+5],4,-378558),m=c(m,l,g,v,n[e+8],11,-2022574463),v=c(v,m,l,g,n[e+11],16,1839030562),g=c(g,v,m,l,n[e+14],23,-35309556),l=c(l,g,v,m,n[e+1],4,-1530992060),m=c(m,l,g,v,n[e+4],11,1272893353),v=c(v,m,l,g,n[e+7],16,-155497632),g=c(g,v,m,l,n[e+10],23,-1094730640),l=c(l,g,v,m,n[e+13],4,681279174),m=c(m,l,g,v,n[e],11,-358537222),v=c(v,m,l,g,n[e+3],16,-722521979),g=c(g,v,m,l,n[e+6],23,76029189),l=c(l,g,v,m,n[e+9],4,-640364487),m=c(m,l,g,v,n[e+12],11,-421815835),v=c(v,m,l,g,n[e+15],16,530742520),g=c(g,v,m,l,n[e+2],23,-995338651),l=f(l,g,v,m,n[e],6,-198630844),m=f(m,l,g,v,n[e+7],10,1126891415),v=f(v,m,l,g,n[e+14],15,-1416354905),g=f(g,v,m,l,n[e+5],21,-57434055),l=f(l,g,v,m,n[e+12],6,1700485571),m=f(m,l,g,v,n[e+3],10,-1894986606),v=f(v,m,l,g,n[e+10],15,-1051523),g=f(g,v,m,l,n[e+1],21,-2054922799),l=f(l,g,v,m,n[e+8],6,1873313359),m=f(m,l,g,v,n[e+15],10,-30611744),v=f(v,m,l,g,n[e+6],15,-1560198380),g=f(g,v,m,l,n[e+13],21,1309151649),l=f(l,g,v,m,n[e+4],6,-145523070),m=f(m,l,g,v,n[e+11],10,-1120210379),v=f(v,m,l,g,n[e+2],15,718787259),g=f(g,v,m,l,n[e+9],21,-343485551),l=t(l,i),g=t(g,a),v=t(v,h),m=t(m,d);return[l,g,v,m]}function a(n){var t,r="";for(t=0;t<32*n.length;t+=8)r+=String.fromCharCode(n[t>>5]>>>t%32&255);return r}function h(n){var t,r=[];for(r[(n.length>>2)-1]=void 0,t=0;t<r.length;t+=1)r[t]=0;for(t=0;t<8*n.length;t+=8)r[t>>5]|=(255&n.charCodeAt(t/8))<<t%32;return r}function d(n){return a(i(h(n),8*n.length))}function l(n,t){var r,e,o=h(n),u=[],c=[];for(u[15]=c[15]=void 0,o.length>16&&(o=i(o,8*n.length)),r=0;16>r;r+=1)u[r]=909522486^o[r],c[r]=1549556828^o[r];return e=i(u.concat(h(t)),512+8*t.length),a(i(c.concat(e),640))}function g(n){var t,r,e="0123456789abcdef",o="";for(r=0;r<n.length;r+=1)t=n.charCodeAt(r),o+=e.charAt(t>>>4&15)+e.charAt(15&t);return o}function v(n){return unescape(encodeURIComponent(n))}function m(n){return d(v(n))}function p(n){return g(m(n))}function s(n,t){return l(v(n),v(t))}function C(n,t){return g(s(n,t))}function A(n,t,r){return t?r?s(t,n):C(t,n):r?m(n):p(n)}"function"==typeof define&&define.amd?define(function(){return A}):"object"==typeof module&&module.exports?module.exports=A:n.md5=A}(this);
+//# sourceMappingURL=md5.min.js.map
\ No newline at end of file
diff --git a/plugins/generic-sessions/assets/post-forgot-fail.html b/plugins/generic-sessions/assets/post-forgot-fail.html
new file mode 100644
index 0000000000000000000000000000000000000000..ead3d13ecbf7a5b6515ffbd7a84efd1cb0d7d681
--- /dev/null
+++ b/plugins/generic-sessions/assets/post-forgot-fail.html
@@ -0,0 +1,5 @@
+<html>
+Sorry, something went wrong.
+
+Click <a href="../">here</a> to continue.
+</html>
diff --git a/plugins/generic-sessions/assets/post-forgot-ok.html b/plugins/generic-sessions/assets/post-forgot-ok.html
new file mode 100644
index 0000000000000000000000000000000000000000..3e8e9cf59995524ba05c54fa68b6a1a858748a2f
--- /dev/null
+++ b/plugins/generic-sessions/assets/post-forgot-ok.html
@@ -0,0 +1,6 @@
+<html>
+This is a one-time password recovery login.
+
+Please click <a href="./">here</a> and click your username at the top to reset your password.
+</html>
+
diff --git a/plugins/generic-sessions/assets/post-register-fail.html b/plugins/generic-sessions/assets/post-register-fail.html
new file mode 100644
index 0000000000000000000000000000000000000000..063c3c50fa0162e3971358a3992ba52940350337
--- /dev/null
+++ b/plugins/generic-sessions/assets/post-register-fail.html
@@ -0,0 +1 @@
+Registration failed, sorry
diff --git a/plugins/generic-sessions/assets/post-register-ok.html b/plugins/generic-sessions/assets/post-register-ok.html
new file mode 100644
index 0000000000000000000000000000000000000000..2d150358132dfeca7d282408d9cdb3a1dbcae4df
--- /dev/null
+++ b/plugins/generic-sessions/assets/post-register-ok.html
@@ -0,0 +1,27 @@
+<html>
+ <head>
+  <script src="lwsgs.js"></script>
+ </head>
+ <body>
+  <table>
+    <tr>
+     <td colspan=2 align=center>
+      <img src="lwsgs-logo.png">
+     </td>
+    </tr>
+    <tr>
+     <td>
+      Your registration as <span id="u"></span> is accepted,<br>
+      you will receive an email shortly with instructions<br>
+      to verify and enable the account for normal use.<br><br>
+      The link is only valid for an hour, after that if it has<br>
+      not been verified your account will be deleted.
+     </td>
+    </tr>
+   </table>
+  </body>
+ <script>
+	document.getElementById('u').innerHTML = "<b>" + lwsgs_san(lwsgs_user) + "</b>";
+ </script>
+</html>
+
diff --git a/plugins/generic-sessions/assets/post-verify-fail.html b/plugins/generic-sessions/assets/post-verify-fail.html
new file mode 100644
index 0000000000000000000000000000000000000000..d1d89ca56bf4a5991fd91dceba3519e31e13d845
--- /dev/null
+++ b/plugins/generic-sessions/assets/post-verify-fail.html
@@ -0,0 +1,20 @@
+<html>
+ <head>
+  <script src="lwsgs.js"></script>
+ </head>
+ <body>
+  <table>
+    <tr>
+     <td colspan=2 align=center>
+      <img src="lwsws-logo.png">
+     </td>
+    </tr>
+    <tr>
+     <td>
+	Sorry, the link was invalid.
+     </td>
+    </tr>
+   </table>
+  </body>
+</html>
+
diff --git a/plugins/generic-sessions/assets/post-verify-ok.html b/plugins/generic-sessions/assets/post-verify-ok.html
new file mode 100644
index 0000000000000000000000000000000000000000..e968f6a753523aafa306cb5cc418a7c986c5a4a1
--- /dev/null
+++ b/plugins/generic-sessions/assets/post-verify-ok.html
@@ -0,0 +1,25 @@
+<html>
+ <head>
+  <script src="lwsgs.js"></script>
+ </head>
+ <body>
+  <table>
+    <tr>
+     <td colspan=2 align=center>
+      <img src="lwsgs-logo.png">
+     </td>
+    </tr>
+    <tr>
+     <td>
+        Thanks for signing up, your registration as <span id="u"></span> is verified.<br>
+	<br>
+	Click <a href="/lwsgs">here</a> to continue.
+     </td>
+    </tr>
+   </table>
+  </body>
+ <script>
+	document.getElementById('u').innerHTML = "<b>" + san(lwsgs_user) + "</b>";
+ </script>
+</html>
+
diff --git a/plugins/generic-sessions/assets/seats.jpg b/plugins/generic-sessions/assets/seats.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..5bed40d919872359f2fcc00a8430e7dcfabaab4a
Binary files /dev/null and b/plugins/generic-sessions/assets/seats.jpg differ
diff --git a/plugins/generic-sessions/assets/sent-forgot-fail.html b/plugins/generic-sessions/assets/sent-forgot-fail.html
new file mode 100644
index 0000000000000000000000000000000000000000..ead3d13ecbf7a5b6515ffbd7a84efd1cb0d7d681
--- /dev/null
+++ b/plugins/generic-sessions/assets/sent-forgot-fail.html
@@ -0,0 +1,5 @@
+<html>
+Sorry, something went wrong.
+
+Click <a href="../">here</a> to continue.
+</html>
diff --git a/plugins/generic-sessions/assets/sent-forgot-ok.html b/plugins/generic-sessions/assets/sent-forgot-ok.html
new file mode 100644
index 0000000000000000000000000000000000000000..83df7510af2fc5f9a35098390c02495c4af51b98
--- /dev/null
+++ b/plugins/generic-sessions/assets/sent-forgot-ok.html
@@ -0,0 +1,4 @@
+An email has been sent to your registered address.
+
+Please follow the instructions to reset your password.
+
diff --git a/plugins/generic-sessions/assets/successful-login.html b/plugins/generic-sessions/assets/successful-login.html
new file mode 100644
index 0000000000000000000000000000000000000000..dfc25cf74a82a1d9b8535681b843d000bce7e499
--- /dev/null
+++ b/plugins/generic-sessions/assets/successful-login.html
@@ -0,0 +1,4 @@
+<html>
+This is an example destination that will appear after successful non-Admin login
+</html>
+
diff --git a/plugins/generic-sessions/handlers.c b/plugins/generic-sessions/handlers.c
new file mode 100644
index 0000000000000000000000000000000000000000..bd00c2b4c1e8a75cfdc0d4a5cdb76488028c1678
--- /dev/null
+++ b/plugins/generic-sessions/handlers.c
@@ -0,0 +1,598 @@
+/*
+ * ws protocol handler plugin for "generic sessions"
+ *
+ * Copyright (C) 2010-2016 Andy Green <andy@warmcat.com>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation:
+ * version 2.1 of the License.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ * MA  02110-1301  USA
+ */
+
+#include "private-lwsgs.h"
+
+/* handle account confirmation links */
+
+int
+lwsgs_handler_confirm(struct per_vhost_data__gs *vhd, struct lws *wsi,
+		      struct per_session_data__gs *pss)
+{
+	char cookie[1024], s[256], esc[50];
+	struct lws_gs_event_args a;
+	struct lwsgs_user u;
+
+	if (lws_hdr_copy_fragment(wsi, cookie, sizeof(cookie),
+				  WSI_TOKEN_HTTP_URI_ARGS, 0) < 0)
+		goto verf_fail;
+
+	if (strncmp(cookie, "token=", 6))
+		goto verf_fail;
+
+	u.username[0] = '\0';
+	snprintf(s, sizeof(s) - 1,
+		 "select username,email,verified from users where token = '%s';",
+		 lws_sql_purify(esc, &cookie[6], sizeof(esc) - 1));
+	if (sqlite3_exec(vhd->pdb, s, lwsgs_lookup_callback_user, &u, NULL) !=
+	    SQLITE_OK) {
+		lwsl_err("Unable to lookup token: %s\n",
+			 sqlite3_errmsg(vhd->pdb));
+		goto verf_fail;
+	}
+
+	if (!u.username[0] || u.verified != 1) {
+		lwsl_notice("verify token doesn't map to unverified user\n");
+		goto verf_fail;
+	}
+
+	lwsl_notice("Verifying %s\n", u.username);
+	snprintf(s, sizeof(s) - 1,
+		 "update users set verified=%d where username='%s';",
+		 LWSGS_VERIFIED_ACCEPTED,
+		 lws_sql_purify(esc, u.username, sizeof(esc) - 1));
+	if (sqlite3_exec(vhd->pdb, s, lwsgs_lookup_callback_user, &u, NULL) !=
+	    SQLITE_OK) {
+		lwsl_err("Unable to lookup token: %s\n",
+			 sqlite3_errmsg(vhd->pdb));
+
+		goto verf_fail;
+	}
+
+	lwsl_notice("deleting account\n");
+
+	a.event = LWSGSE_CREATED;
+	a.username = u.username;
+	a.email = u.email;
+	lws_callback_vhost_protocols(wsi, LWS_CALLBACK_GS_EVENT, &a, 0);
+
+	snprintf(pss->onward, sizeof(pss->onward),
+		 "%s/post-verify-ok.html", vhd->email_confirm_url);
+
+	pss->login_expires = lws_now_secs() + vhd->timeout_absolute_secs;
+
+	pss->delete_session.id[0] = '\0';
+	lwsgs_get_sid_from_wsi(wsi, &pss->delete_session);
+
+	/* we need to create a new, authorized session */
+
+	if (lwsgs_new_session_id(vhd, &pss->login_session, u.username,
+				 pss->login_expires))
+		goto verf_fail;
+
+	lwsl_notice("Creating new session: %s, redir to %s\n",
+		    pss->login_session.id, pss->onward);
+
+	return 0;
+
+verf_fail:
+	pss->delete_session.id[0] = '\0';
+	lwsgs_get_sid_from_wsi(wsi, &pss->delete_session);
+	pss->login_expires = 0;
+
+	snprintf(pss->onward, sizeof(pss->onward), "%s/post-verify-fail.html",
+		 vhd->email_confirm_url);
+
+	return 1;
+}
+
+/* handle forgot password confirmation links */
+
+int
+lwsgs_handler_forgot(struct per_vhost_data__gs *vhd, struct lws *wsi,
+		     struct per_session_data__gs *pss)
+{
+	char cookie[1024], s[256], esc[50];
+	struct lwsgs_user u;
+	const char *a;
+
+	a = lws_get_urlarg_by_name(wsi, "token=", cookie, sizeof(cookie));
+	if (!a)
+		goto forgot_fail;
+
+	u.username[0] = '\0';
+	snprintf(s, sizeof(s) - 1,
+		 "select username,verified from users where verified=%d and "
+		 "token = '%s' and token_time != 0;",
+		 LWSGS_VERIFIED_ACCEPTED,
+		 lws_sql_purify(esc, &cookie[6], sizeof(esc) - 1));
+	if (sqlite3_exec(vhd->pdb, s, lwsgs_lookup_callback_user, &u, NULL) !=
+	    SQLITE_OK) {
+		lwsl_err("Unable to lookup token: %s\n",
+			 sqlite3_errmsg(vhd->pdb));
+
+		goto forgot_fail;
+	}
+
+	if (!u.username[0]) {
+		puts(s);
+		lwsl_notice("forgot token doesn't map to verified user\n");
+		goto forgot_fail;
+	}
+
+	/* mark user as having validated forgot flow just now */
+
+	snprintf(s, sizeof(s) - 1,
+		 "update users set token_time=0,last_forgot_validated=%lu "
+		 "where username='%s';",
+		 (unsigned long)lws_now_secs(),
+		 lws_sql_purify(esc, u.username, sizeof(esc) - 1));
+
+	if (sqlite3_exec(vhd->pdb, s, lwsgs_lookup_callback_user, &u, NULL) !=
+	    SQLITE_OK) {
+		lwsl_err("Unable to lookup token: %s\n",
+			 sqlite3_errmsg(vhd->pdb));
+		goto forgot_fail;
+	}
+
+	a = lws_get_urlarg_by_name(wsi, "good=", cookie, sizeof(cookie));
+	if (!a)
+		a = "broken-forget-post-good-url";
+
+	snprintf(pss->onward, sizeof(pss->onward),
+		 "%s/%s", vhd->email_confirm_url, a);
+
+	pss->login_expires = lws_now_secs() + vhd->timeout_absolute_secs;
+
+	pss->delete_session.id[0] = '\0';
+	lwsgs_get_sid_from_wsi(wsi, &pss->delete_session);
+
+	/* we need to create a new, authorized session */
+	if (lwsgs_new_session_id(vhd, &pss->login_session,
+				 u.username,
+				 pss->login_expires))
+		goto forgot_fail;
+
+	lwsl_notice("Creating new session: %s, redir to %s\n",
+		    pss->login_session.id, pss->onward);
+
+	return 0;
+
+forgot_fail:
+	pss->delete_session.id[0] = '\0';
+	lwsgs_get_sid_from_wsi(wsi, &pss->delete_session);
+	pss->login_expires = 0;
+
+	a = lws_get_urlarg_by_name(wsi, "bad=", cookie, sizeof(cookie));
+	if (!a)
+		a = "broken-forget-post-bad-url";
+
+	snprintf(pss->onward, sizeof(pss->onward), "%s/%s",
+		 vhd->email_confirm_url, a);
+
+	return 1;
+}
+
+/* support dynamic username / email checking */
+
+int
+lwsgs_handler_check(struct per_vhost_data__gs *vhd,
+		    struct lws *wsi, struct per_session_data__gs *pss)
+{
+	static const char * const colname[] = { "username", "email" };
+	char cookie[1024], s[256], esc[50], *pc;
+	unsigned char *p, *start, *end, buffer[LWS_PRE + 256];
+	struct lwsgs_user u;
+	int n;
+
+	/*
+	 * either /check?email=xxx@yyy   or: /check?username=xxx
+	 * returns '0' if not already registered, else '1'
+	 */
+
+	u.username[0] = '\0';
+	if (lws_hdr_copy_fragment(wsi, cookie, sizeof(cookie),
+				  WSI_TOKEN_HTTP_URI_ARGS, 0) < 0)
+		goto reply;
+
+	n = !strncmp(cookie, "email=", 6);
+	pc = strchr(cookie, '=');
+	if (!pc) {
+		lwsl_notice("cookie has no =\n");
+		goto reply;
+	}
+	pc++;
+
+	/* admin user cannot be registered in user db */
+	if (!strcmp(vhd->admin_user, pc)) {
+		u.username[0] = 'a';
+		goto reply;
+	}
+
+	snprintf(s, sizeof(s) - 1,
+		 "select username, email from users where %s = '%s';",
+		 colname[n], lws_sql_purify(esc, pc, sizeof(esc) - 1));
+	if (sqlite3_exec(vhd->pdb, s, lwsgs_lookup_callback_user, &u, NULL) !=
+	    SQLITE_OK) {
+		lwsl_err("Unable to lookup token: %s\n",
+			 sqlite3_errmsg(vhd->pdb));
+		goto reply;
+	}
+
+reply:
+	s[0] = '0' + !!u.username[0];
+	p = buffer + LWS_PRE;
+	start = p;
+	end = p + sizeof(buffer) - LWS_PRE;
+
+	if (lws_add_http_header_status(wsi, 200, &p, end))
+		return -1;
+	if (lws_add_http_header_by_token(wsi, WSI_TOKEN_HTTP_CONTENT_TYPE,
+					 (unsigned char *)"text/plain", 10,
+					 &p, end))
+		return -1;
+
+	if (lws_add_http_header_content_length(wsi, 1, &p, end))
+		return -1;
+
+	if (lws_finalize_http_header(wsi, &p, end))
+		return -1;
+
+	n = lws_write(wsi, start, p - start, LWS_WRITE_HTTP_HEADERS);
+	if (n != (p - start)) {
+		lwsl_err("_write returned %d from %d\n", n, (p - start));
+		return -1;
+	}
+	n = lws_write(wsi, (unsigned char *)s, 1, LWS_WRITE_HTTP);
+	if (n != 1)
+		return -1;
+
+	return 0;
+}
+
+/* handle forgot password confirmation links */
+
+int
+lwsgs_handler_change_password(struct per_vhost_data__gs *vhd, struct lws *wsi,
+			      struct per_session_data__gs *pss)
+{
+	char s[256], esc[50], username[50];
+	struct lwsgs_user u;
+	lwsgw_hash sid;
+	int n = 0;
+
+	/* see if he's logged in */
+	username[0] = '\0';
+	if (!lwsgs_get_sid_from_wsi(wsi, &sid)) {
+		u.username[0] = '\0';
+		if (!lwsgs_lookup_session(vhd, &sid, username, sizeof(username))) {
+			n = 1; /* yes, logged in */
+			if (lwsgs_lookup_user(vhd, username, &u))
+				return 1;
+
+			/* did a forgot pw ? */
+			if (u.last_forgot_validated > lws_now_secs() - 300)
+				n |= LWSGS_AUTH_FORGOT_FLOW;
+		}
+	}
+
+	/* if he just did forgot pw flow, don't need old pw */
+	if (!(n & (LWSGS_AUTH_FORGOT_FLOW | 1))) {
+		/* otherwise user:pass must be right */
+		if (lwsgs_check_credentials(vhd,
+			   lws_spa_get_string(pss->spa, FGS_USERNAME),
+			   lws_spa_get_string(pss->spa, FGS_CURPW))) {
+			lwsl_notice("credentials bad\n");
+			return 1;
+		}
+
+		strcpy(u.username, lws_spa_get_string(pss->spa, FGS_USERNAME));
+	}
+
+	/* does he want to delete his account? */
+
+	if (lws_spa_get_length(pss->spa, FGS_DELETE)) {
+		struct lws_gs_event_args a;
+
+		lwsl_notice("deleting account\n");
+
+		a.event = LWSGSE_DELETED;
+		a.username = u.username;
+		a.email = "";
+		lws_callback_vhost_protocols(wsi, LWS_CALLBACK_GS_EVENT, &a, 0);
+
+		snprintf(s, sizeof(s) - 1,
+			 "delete from users where username='%s';"
+			 "delete from sessions where username='%s';",
+			 lws_sql_purify(esc, u.username, sizeof(esc) - 1),
+			 lws_sql_purify(esc, u.username, sizeof(esc) - 1));
+		goto sql;
+	}
+
+	if (lwsgs_hash_password(vhd, lws_spa_get_string(pss->spa, FGS_PASSWORD), &u))
+		return 1;
+
+	lwsl_notice("updating password hash\n");
+
+	snprintf(s, sizeof(s) - 1,
+		 "update users set pwhash='%s', pwsalt='%s', "
+		 "last_forgot_validated=0 where username='%s';",
+		 u.pwhash.id, u.pwsalt.id,
+		 lws_sql_purify(esc, u.username, sizeof(esc) - 1));
+
+sql:
+	if (sqlite3_exec(vhd->pdb, s, NULL, NULL, NULL) != SQLITE_OK) {
+		lwsl_err("Unable to update pw hash: %s\n",
+			 sqlite3_errmsg(vhd->pdb));
+		return 1;
+	}
+
+	return 0;
+}
+
+int
+lwsgs_handler_forgot_pw_form(struct per_vhost_data__gs *vhd,
+			     struct lws *wsi,
+			     struct per_session_data__gs *pss)
+{
+	char s[LWSGS_EMAIL_CONTENT_SIZE];
+	unsigned char buffer[LWS_PRE + LWSGS_EMAIL_CONTENT_SIZE];
+	char esc[50], esc1[50], esc2[50], esc3[50], esc4[50];
+	struct lwsgs_user u;
+	lwsgw_hash hash;
+	unsigned char sid_rand[20];
+	int n;
+
+	lwsl_notice("FORGOT %s %s\n",
+		    lws_spa_get_string(pss->spa, FGS_USERNAME),
+		    lws_spa_get_string(pss->spa, FGS_EMAIL));
+
+	if (!lws_spa_get_string(pss->spa, FGS_USERNAME) &&
+	    !lws_spa_get_string(pss->spa, FGS_EMAIL)) {
+		lwsl_err("Form must provide either "
+			  "username or email\n");
+		return -1;
+	}
+
+	if (!lws_spa_get_string(pss->spa, FGS_FORGOT_GOOD) ||
+	    !lws_spa_get_string(pss->spa, FGS_FORGOT_BAD) ||
+	    !lws_spa_get_string(pss->spa, FGS_FORGOT_POST_GOOD) ||
+	    !lws_spa_get_string(pss->spa, FGS_FORGOT_POST_BAD)) {
+		lwsl_err("Form must provide reg-good "
+			  "and reg-bad (and post-*)"
+			  "targets\n");
+		return -1;
+	}
+
+	u.username[0] = '\0';
+	if (lws_spa_get_string(pss->spa, FGS_USERNAME))
+		snprintf(s, sizeof(s) - 1,
+		 "select username,email "
+		 "from users where username = '%s';",
+		 lws_sql_purify(esc, lws_spa_get_string(pss->spa, FGS_USERNAME),
+				 sizeof(esc) - 1));
+	else
+		snprintf(s, sizeof(s) - 1,
+		 "select username,email "
+		 "from users where email = '%s';",
+		 lws_sql_purify(esc, lws_spa_get_string(pss->spa, FGS_EMAIL), sizeof(esc) - 1));
+	if (sqlite3_exec(vhd->pdb, s, lwsgs_lookup_callback_user, &u, NULL) !=
+	    SQLITE_OK) {
+		lwsl_err("Unable to lookup token: %s\n",
+			 sqlite3_errmsg(vhd->pdb));
+		return 1;
+	}
+	if (!u.username[0]) {
+		lwsl_err("No match found %s\n", s);
+		return 1;
+	}
+
+	lws_get_peer_simple(wsi, pss->ip, sizeof(pss->ip));
+	if (lws_get_random(vhd->context, sid_rand,
+			   sizeof(sid_rand)) !=
+			   sizeof(sid_rand)) {
+		lwsl_err("Problem getting random for token\n");
+		return 1;
+	}
+	sha1_to_lwsgw_hash(sid_rand, &hash);
+	n = snprintf(s, sizeof(s),
+		"From: Forgot Password Assistant Noreply <%s>\n"
+		"To: %s <%s>\n"
+		  "Subject: Password reset request\n"
+		  "\n"
+		  "Hello, %s\n\n"
+		  "We received a password reset request from IP %s for this email,\n"
+		  "to confirm you want to do that, please click the link below.\n\n",
+		lws_sql_purify(esc, vhd->email.email_from, sizeof(esc) - 1),
+		lws_sql_purify(esc1, u.username, sizeof(esc1) - 1),
+		lws_sql_purify(esc2, u.email, sizeof(esc2) - 1),
+		lws_sql_purify(esc3, u.username, sizeof(esc3) - 1),
+		lws_sql_purify(esc4, pss->ip, sizeof(esc4) - 1));
+	snprintf(s + n, sizeof(s) -n,
+		  "%s/lwsgs-forgot?token=%s"
+		   "&good=%s"
+		   "&bad=%s\n\n"
+		  "If this request is unexpected, please ignore it and\n"
+		  "no further action will be taken.\n\n"
+		"If you have any questions or concerns about this\n"
+		"automated email, you can contact a real person at\n"
+		"%s.\n"
+		"\n.\n",
+		vhd->email_confirm_url, hash.id,
+		lws_urlencode(esc1,
+			     lws_spa_get_string(pss->spa, FGS_FORGOT_POST_GOOD),
+			     sizeof(esc1) - 1),
+		lws_urlencode(esc3,
+			      lws_spa_get_string(pss->spa, FGS_FORGOT_POST_BAD),
+			      sizeof(esc3) - 1),
+		vhd->email_contact_person);
+
+	snprintf((char *)buffer, sizeof(buffer) - 1,
+		 "insert into email(username, content)"
+		 " values ('%s', '%s');",
+		lws_sql_purify(esc, u.username, sizeof(esc) - 1), s);
+	if (sqlite3_exec(vhd->pdb, (char *)buffer, NULL,
+			 NULL, NULL) != SQLITE_OK) {
+		lwsl_err("Unable to insert email: %s\n",
+			 sqlite3_errmsg(vhd->pdb));
+		return 1;
+	}
+
+	snprintf(s, sizeof(s) - 1,
+		 "update users set token='%s',token_time='%ld' where username='%s';",
+		 hash.id, (long)lws_now_secs(),
+		 lws_sql_purify(esc, u.username, sizeof(esc) - 1));
+	if (sqlite3_exec(vhd->pdb, s, NULL, NULL, NULL) !=
+	    SQLITE_OK) {
+		lwsl_err("Unable to set token: %s\n",
+			 sqlite3_errmsg(vhd->pdb));
+		return 1;
+	}
+
+	return 0;
+}
+
+int
+lwsgs_handler_register_form(struct per_vhost_data__gs *vhd,
+			     struct lws *wsi,
+			     struct per_session_data__gs *pss)
+{
+	unsigned char buffer[LWS_PRE + LWSGS_EMAIL_CONTENT_SIZE];
+	char esc[50], esc1[50], esc2[50], esc3[50], esc4[50];
+	char s[LWSGS_EMAIL_CONTENT_SIZE];
+	unsigned char sid_rand[20];
+	struct lwsgs_user u;
+	lwsgw_hash hash;
+
+	lwsl_notice("REGISTER %s %s %s\n",
+			lws_spa_get_string(pss->spa, FGS_USERNAME),
+			lws_spa_get_string(pss->spa, FGS_PASSWORD),
+			lws_spa_get_string(pss->spa, FGS_EMAIL));
+	if (lwsgs_get_sid_from_wsi(wsi,
+	    &pss->login_session))
+		return 1;
+
+	lws_get_peer_simple(wsi, pss->ip, sizeof(pss->ip));
+	lwsl_notice("IP=%s\n", pss->ip);
+
+	if (!lws_spa_get_string(pss->spa, FGS_REG_GOOD) ||
+	    !lws_spa_get_string(pss->spa, FGS_REG_BAD)) {
+		lwsl_info("Form must provide reg-good and reg-bad targets\n");
+		return -1;
+	}
+
+	/* admin user cannot be registered in user db */
+	if (!strcmp(vhd->admin_user,
+		    lws_spa_get_string(pss->spa, FGS_USERNAME)))
+		return 1;
+
+	if (!lwsgs_lookup_user(vhd,
+			lws_spa_get_string(pss->spa, FGS_USERNAME), &u)) {
+		lwsl_notice("user %s already registered\n",
+			    lws_spa_get_string(pss->spa, FGS_USERNAME));
+		return 1;
+	}
+
+	u.username[0] = '\0';
+	snprintf(s, sizeof(s) - 1, "select username, email from users where email = '%s';",
+		 lws_sql_purify(esc, lws_spa_get_string(pss->spa, FGS_EMAIL),
+		 sizeof(esc) - 1));
+
+	if (sqlite3_exec(vhd->pdb, s,
+			 lwsgs_lookup_callback_user, &u, NULL) != SQLITE_OK) {
+		lwsl_err("Unable to lookup token: %s\n",
+			 sqlite3_errmsg(vhd->pdb));
+		return 1;
+	}
+
+	if (u.username[0]) {
+		lwsl_notice("email %s already in use\n",
+			    lws_spa_get_string(pss->spa, FGS_USERNAME));
+		return 1;
+	}
+
+	if (lwsgs_hash_password(vhd, lws_spa_get_string(pss->spa, FGS_PASSWORD),
+			        &u)) {
+		lwsl_err("Password hash failed\n");
+		return 1;
+	}
+
+	if (lws_get_random(vhd->context, sid_rand, sizeof(sid_rand)) !=
+	    sizeof(sid_rand)) {
+		lwsl_err("Problem getting random for token\n");
+		return 1;
+	}
+	sha1_to_lwsgw_hash(sid_rand, &hash);
+
+	snprintf((char *)buffer, sizeof(buffer) - 1,
+		 "insert into users(username,"
+		 " creation_time, ip, email, verified,"
+		 " pwhash, pwsalt, token, last_forgot_validated)"
+		 " values ('%s', %lu, '%s', '%s', 0,"
+		 " '%s', '%s', '%s', 0);",
+		lws_sql_purify(esc, lws_spa_get_string(pss->spa, FGS_USERNAME), sizeof(esc) - 1),
+		(unsigned long)lws_now_secs(),
+		lws_sql_purify(esc1, pss->ip, sizeof(esc1) - 1),
+		lws_sql_purify(esc2, lws_spa_get_string(pss->spa, FGS_EMAIL), sizeof(esc2) - 1),
+		u.pwhash.id, u.pwsalt.id, hash.id);
+
+	if (sqlite3_exec(vhd->pdb, (char *)buffer, NULL, NULL, NULL) != SQLITE_OK) {
+		lwsl_err("Unable to insert user: %s\n",
+			 sqlite3_errmsg(vhd->pdb));
+		return 1;
+	}
+
+	snprintf(s, sizeof(s),
+		"From: Noreply <%s>\n"
+		"To: %s <%s>\n"
+		  "Subject: Registration verification\n"
+		  "\n"
+		  "Hello, %s\n\n"
+		  "We received a registration from IP %s using this email,\n"
+		  "to confirm it is legitimate, please click the link below.\n\n"
+		  "%s/lwsgs-confirm?token=%s\n\n"
+		  "If this request is unexpected, please ignore it and\n"
+		  "no further action will be taken.\n\n"
+		"If you have any questions or concerns about this\n"
+		"automated email, you can contact a real person at\n"
+		"%s.\n"
+		"\n.\n",
+		lws_sql_purify(esc, vhd->email.email_from, sizeof(esc) - 1),
+		lws_sql_purify(esc1, lws_spa_get_string(pss->spa, FGS_USERNAME), sizeof(esc1) - 1),
+		lws_sql_purify(esc2, lws_spa_get_string(pss->spa, FGS_EMAIL), sizeof(esc2) - 1),
+		lws_sql_purify(esc3, lws_spa_get_string(pss->spa, FGS_USERNAME), sizeof(esc3) - 1),
+		lws_sql_purify(esc4, pss->ip, sizeof(esc4) - 1),
+		vhd->email_confirm_url, hash.id,
+		vhd->email_contact_person);
+
+	snprintf((char *)buffer, sizeof(buffer) - 1,
+		 "insert into email(username, content) values ('%s', '%s');",
+		lws_sql_purify(esc, lws_spa_get_string(pss->spa, FGS_USERNAME),
+			       sizeof(esc) - 1), s);
+
+	if (sqlite3_exec(vhd->pdb, (char *)buffer, NULL, NULL, NULL) != SQLITE_OK) {
+		lwsl_err("Unable to insert email: %s\n",
+			 sqlite3_errmsg(vhd->pdb));
+		return 1;
+	}
+
+	return 0;
+}
diff --git a/plugins/generic-sessions/private-lwsgs.h b/plugins/generic-sessions/private-lwsgs.h
new file mode 100644
index 0000000000000000000000000000000000000000..cb408ae8c2d73443d252b781965a73a6cb46ecd8
--- /dev/null
+++ b/plugins/generic-sessions/private-lwsgs.h
@@ -0,0 +1,161 @@
+/*
+ * ws protocol handler plugin for "generic sessions"
+ *
+ * Copyright (C) 2010-2016 Andy Green <andy@warmcat.com>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation:
+ * version 2.1 of the License.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ * MA  02110-1301  USA
+ */
+
+#define LWS_DLL
+#define LWS_INTERNAL
+#include "../lib/libwebsockets.h"
+
+#include <sqlite3.h>
+#include <string.h>
+
+#define LWSGS_VERIFIED_ACCEPTED 100
+
+enum {
+	FGS_USERNAME,
+	FGS_PASSWORD,
+	FGS_PASSWORD2,
+	FGS_EMAIL,
+	FGS_REGISTER,
+	FGS_GOOD,
+	FGS_BAD,
+	FGS_REG_GOOD,
+	FGS_REG_BAD,
+	FGS_ADMIN,
+	FGS_FORGOT,
+	FGS_FORGOT_GOOD,
+	FGS_FORGOT_BAD,
+	FGS_FORGOT_POST_GOOD,
+	FGS_FORGOT_POST_BAD,
+	FGS_CHANGE,
+	FGS_CURPW,
+	FGS_DELETE,
+};
+
+struct lwsgs_user {
+	char username[32];
+	char ip[16];
+	lwsgw_hash pwhash;
+	lwsgw_hash pwsalt;
+	lwsgw_hash token;
+	time_t created;
+	time_t last_forgot_validated;
+	char email[100];
+	int verified;
+};
+
+struct per_vhost_data__gs {
+	struct lws_email email;
+	struct lws_context *context;
+	char session_db[256];
+	char admin_user[32];
+	char confounder[32];
+	char email_contact_person[128];
+	char email_title[128];
+	char email_template[128];
+	char email_confirm_url[128];
+	lwsgw_hash admin_password_sha1;
+	sqlite3 *pdb;
+	int timeout_idle_secs;
+	int timeout_absolute_secs;
+	int timeout_anon_absolute_secs;
+	int timeout_email_secs;
+	time_t last_session_expire;
+	struct lwsgs_user u;
+};
+
+struct per_session_data__gs {
+	struct lws_spa *spa;
+	lwsgw_hash login_session;
+	lwsgw_hash delete_session;
+	unsigned int login_expires;
+	char onward[256];
+	char result[500 + LWS_PRE];
+	char urldec[500 + LWS_PRE];
+	int result_len;
+	char ip[46];
+	struct lws_process_html_state phs;
+	int spos;
+
+	unsigned int logging_out:1;
+};
+
+/* utils.c */
+
+int
+lwsgs_lookup_callback_user(void *priv, int cols, char **col_val,
+			   char **col_name);
+void
+lwsgw_cookie_from_session(lwsgw_hash *sid, time_t expires, char **p, char *end);
+int
+lwsgs_get_sid_from_wsi(struct lws *wsi, lwsgw_hash *sid);
+int
+lwsgs_lookup_session(struct per_vhost_data__gs *vhd,
+		     const lwsgw_hash *sid, char *username, int len);
+int
+lwsgs_get_auth_level(struct per_vhost_data__gs *vhd,
+		     const char *username);
+int
+lwsgs_check_credentials(struct per_vhost_data__gs *vhd,
+			const char *username, const char *password);
+void
+sha1_to_lwsgw_hash(unsigned char *hash, lwsgw_hash *shash);
+unsigned int
+lwsgs_now_secs(void);
+int
+lwsgw_check_admin(struct per_vhost_data__gs *vhd,
+		  const char *username, const char *password);
+int
+lwsgs_hash_password(struct per_vhost_data__gs *vhd,
+		    const char *password, struct lwsgs_user *u);
+int
+lwsgs_new_session_id(struct per_vhost_data__gs *vhd,
+		     lwsgw_hash *sid, const char *username, int exp);
+int
+lwsgs_lookup_user(struct per_vhost_data__gs *vhd,
+		  const char *username, struct lwsgs_user *u);
+int
+lwsgw_update_session(struct per_vhost_data__gs *vhd,
+		     lwsgw_hash *hash, const char *user);
+int
+lwsgw_expire_old_sessions(struct per_vhost_data__gs *vhd);
+
+
+/* handlers.c */
+
+int
+lwsgs_handler_confirm(struct per_vhost_data__gs *vhd, struct lws *wsi,
+		      struct per_session_data__gs *pss);
+int
+lwsgs_handler_forgot(struct per_vhost_data__gs *vhd, struct lws *wsi,
+		     struct per_session_data__gs *pss);
+int
+lwsgs_handler_check(struct per_vhost_data__gs *vhd, struct lws *wsi,
+		      struct per_session_data__gs *pss);
+int
+lwsgs_handler_change_password(struct per_vhost_data__gs *vhd, struct lws *wsi,
+			      struct per_session_data__gs *pss);
+int
+lwsgs_handler_forgot_pw_form(struct per_vhost_data__gs *vhd, struct lws *wsi,
+			     struct per_session_data__gs *pss);
+int
+lwsgs_handler_register_form(struct per_vhost_data__gs *vhd, struct lws *wsi,
+			     struct per_session_data__gs *pss);
+
diff --git a/plugins/generic-sessions/protocol_generic_sessions.c b/plugins/generic-sessions/protocol_generic_sessions.c
new file mode 100644
index 0000000000000000000000000000000000000000..6bf5d2b2d0a726cdf2ef45ed990f4f3d3da7e21a
--- /dev/null
+++ b/plugins/generic-sessions/protocol_generic_sessions.c
@@ -0,0 +1,901 @@
+/*
+ * ws protocol handler plugin for "generic sessions"
+ *
+ * Copyright (C) 2010-2016 Andy Green <andy@warmcat.com>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation:
+ * version 2.1 of the License.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ * MA  02110-1301  USA
+ */
+
+#include "private-lwsgs.h"
+
+/* keep changes in sync with the enum in lwsgs.h */
+static const char * const param_names[] = {
+	"username",
+	"password",
+	"password2",
+	"email",
+	"register",
+	"good",
+	"bad",
+	"reg-good",
+	"reg-bad",
+	"admin",
+	"forgot",
+	"forgot-good",
+	"forgot-bad",
+	"forgot-post-good",
+	"forgot-post-bad",
+	"change",
+	"curpw",
+	"delete"
+};
+
+struct lwsgs_fill_args {
+	char *buf;
+	int len;
+};
+
+static const struct lws_protocols protocols[];
+
+static int
+lwsgs_lookup_callback_email(void *priv, int cols, char **col_val,
+			    char **col_name)
+{
+	struct lwsgs_fill_args *a = (struct lwsgs_fill_args *)priv;
+	int n;
+
+	for (n = 0; n < cols; n++) {
+		if (!strcmp(col_name[n], "content")) {
+			strncpy(a->buf, col_val[n], a->len - 1);
+			a->buf[a->len - 1] = '\0';
+			continue;
+		}
+	}
+	return 0;
+}
+
+static int
+lwsgs_email_cb_get_body(struct lws_email *email, char *buf, int len)
+{
+	struct per_vhost_data__gs *vhd = (struct per_vhost_data__gs *)email->data;
+	struct lwsgs_fill_args a;
+	char ss[150], esc[50];
+
+	a.buf = buf;
+	a.len = len;
+
+	snprintf(ss, sizeof(ss) - 1,
+		 "select content from email where username='%s';",
+		 lws_sql_purify(esc, vhd->u.username, sizeof(esc) - 1));
+
+	strncpy(buf, "failed", len);
+	if (sqlite3_exec(vhd->pdb, ss, lwsgs_lookup_callback_email, &a,
+			 NULL) != SQLITE_OK) {
+		lwsl_err("Unable to lookup email: %s\n",
+			 sqlite3_errmsg(vhd->pdb));
+
+		return 1;
+	}
+
+	return 0;
+}
+
+static int
+lwsgs_email_cb_sent(struct lws_email *email)
+{
+	struct per_vhost_data__gs *vhd = (struct per_vhost_data__gs *)email->data;
+	char s[200], esc[50];
+
+	/* mark the user as having sent the verification email */
+	snprintf(s, sizeof(s) - 1,
+		 "update users set verified=1 where username='%s' and verified==0;",
+		 lws_sql_purify(esc, vhd->u.username, sizeof(esc) - 1));
+	if (sqlite3_exec(vhd->pdb, s, NULL, NULL, NULL) != SQLITE_OK) {
+		lwsl_err("%s: Unable to update user: %s\n", __func__,
+			 sqlite3_errmsg(vhd->pdb));
+		return 1;
+	}
+
+	snprintf(s, sizeof(s) - 1,
+		 "delete from email where username='%s';",
+		 lws_sql_purify(esc, vhd->u.username, sizeof(esc) - 1));
+	if (sqlite3_exec(vhd->pdb, s, NULL, NULL, NULL) != SQLITE_OK) {
+		lwsl_err("%s: Unable to delete email text: %s\n", __func__,
+			 sqlite3_errmsg(vhd->pdb));
+		return 1;
+	}
+
+	return 0;
+}
+
+static int
+lwsgs_email_cb_on_next(struct lws_email *email)
+{
+	struct per_vhost_data__gs *vhd = lws_container_of(email,
+			struct per_vhost_data__gs, email);
+	char s[LWSGS_EMAIL_CONTENT_SIZE], esc[50];
+	time_t now = lws_now_secs();
+
+	/*
+	 * users not verified in 24h get deleted
+	 */
+	snprintf(s, sizeof(s) - 1, "delete from users where ((verified != %d)"
+		 " and (creation_time <= %lu));", LWSGS_VERIFIED_ACCEPTED,
+		 (unsigned long)now - vhd->timeout_email_secs);
+	if (sqlite3_exec(vhd->pdb, s, NULL, NULL, NULL) != SQLITE_OK) {
+		lwsl_err("Unable to expire users: %s\n",
+			 sqlite3_errmsg(vhd->pdb));
+		return 1;
+	}
+
+	snprintf(s, sizeof(s) - 1, "update users set token_time=0 where "
+		 "(token_time <= %lu);",
+		 (unsigned long)now - vhd->timeout_email_secs);
+	if (sqlite3_exec(vhd->pdb, s, NULL, NULL, NULL) != SQLITE_OK) {
+		lwsl_err("Unable to expire users: %s\n",
+			 sqlite3_errmsg(vhd->pdb));
+		return 1;
+	}
+
+	vhd->u.username[0] = '\0';
+	snprintf(s, sizeof(s) - 1, "select username from email limit 1;");
+	if (sqlite3_exec(vhd->pdb, s, lwsgs_lookup_callback_user, &vhd->u,
+			 NULL) != SQLITE_OK) {
+		lwsl_err("Unable to lookup user: %s\n", sqlite3_errmsg(vhd->pdb));
+		return 1;
+	}
+
+	snprintf(s, sizeof(s) - 1,
+		 "select username, creation_time, email, ip, verified, token"
+		 " from users where username='%s' limit 1;",
+		 lws_sql_purify(esc, vhd->u.username, sizeof(esc) - 1));
+	if (sqlite3_exec(vhd->pdb, s, lwsgs_lookup_callback_user, &vhd->u,
+			 NULL) != SQLITE_OK) {
+		lwsl_err("Unable to lookup user: %s\n",
+			 sqlite3_errmsg(vhd->pdb));
+		return 1;
+	}
+
+	if (!vhd->u.username[0])
+		/*
+		 * nothing to do, we are idle and no suitable
+		 * accounts waiting for verification.  When a new user
+		 * is added we will get kicked to try again.
+		 */
+		return 1;
+
+	strncpy(email->email_to, vhd->u.email, sizeof(email->email_to) - 1);
+
+	return 0;
+}
+
+
+struct lwsgs_subst_args
+{
+	struct per_session_data__gs *pss;
+	struct per_vhost_data__gs *vhd;
+	struct lws *wsi;
+};
+
+static const char *
+lwsgs_subst(void *data, int index)
+{
+	struct lwsgs_subst_args *a = (struct lwsgs_subst_args *)data;
+	struct lwsgs_user u;
+	lwsgw_hash sid;
+	char esc[50], s[100];
+	int n;
+
+	a->pss->result[0] = '\0';
+	u.email[0] = '\0';
+	if (!lwsgs_get_sid_from_wsi(a->wsi, &sid)) {
+		if (lwsgs_lookup_session(a->vhd, &sid, a->pss->result, 31)) {
+			lwsl_notice("sid lookup for %s failed\n", sid.id);
+			a->pss->delete_session = sid;
+			return NULL;
+		}
+		snprintf(s, sizeof(s) - 1, "select username,email "
+			 "from users where username = '%s';",
+			 lws_sql_purify(esc, a->pss->result, sizeof(esc) - 1));
+		if (sqlite3_exec(a->vhd->pdb, s, lwsgs_lookup_callback_user,
+				 &u, NULL) != SQLITE_OK) {
+			lwsl_err("Unable to lookup token: %s\n",
+				 sqlite3_errmsg(a->vhd->pdb));
+			a->pss->delete_session = sid;
+			return NULL;
+		}
+	} else
+		lwsl_notice("no sid\n");
+
+	strncpy(a->pss->result + 32, u.email, 100);
+
+	switch (index) {
+	case 0:
+		return a->pss->result;
+
+	case 1:
+		n = lwsgs_get_auth_level(a->vhd, a->pss->result);
+		sprintf(a->pss->result, "%d", n);
+		return a->pss->result;
+	case 2:
+		return a->pss->result + 32;
+	}
+
+	return NULL;
+}
+
+static int
+callback_generic_sessions(struct lws *wsi, enum lws_callback_reasons reason,
+			  void *user, void *in, size_t len)
+{
+	struct per_session_data__gs *pss = (struct per_session_data__gs *)user;
+	const struct lws_protocol_vhost_options *pvo;
+	struct per_vhost_data__gs *vhd = (struct per_vhost_data__gs *)
+			lws_protocol_vh_priv_get(lws_get_vhost(wsi),
+					&protocols[0]);
+	char cookie[1024], username[32], *pc = cookie;
+	unsigned char buffer[LWS_PRE + LWSGS_EMAIL_CONTENT_SIZE];
+	struct lws_process_html_args *args;
+	struct lws_session_info *sinfo;
+	char s[LWSGS_EMAIL_CONTENT_SIZE];
+	unsigned char *p, *start, *end;
+	sqlite3_stmt *sm;
+	lwsgw_hash sid;
+	const char *cp;
+	int n;
+
+	switch (reason) {
+	case LWS_CALLBACK_PROTOCOL_INIT: /* per vhost */
+
+		vhd = lws_protocol_vh_priv_zalloc(lws_get_vhost(wsi),
+			&protocols[0], sizeof(struct per_vhost_data__gs));
+		if (!vhd)
+			return 1;
+		vhd->context = lws_get_context(wsi);
+
+		/* defaults */
+		vhd->timeout_idle_secs = 600;
+		vhd->timeout_absolute_secs = 36000;
+		vhd->timeout_anon_absolute_secs = 1200;
+		vhd->timeout_email_secs = 24 * 3600;
+		strcpy(vhd->email.email_helo, "unconfigured.com");
+		strcpy(vhd->email.email_from, "noreply@unconfigured.com");
+		strcpy(vhd->email_title, "Registration Email from unconfigured");
+		strcpy(vhd->email.email_smtp_ip, "127.0.0.1");
+
+		vhd->email.on_next = lwsgs_email_cb_on_next;
+		vhd->email.on_get_body = lwsgs_email_cb_get_body;
+		vhd->email.on_sent = lwsgs_email_cb_sent;
+		vhd->email.data = (void *)vhd;
+
+		pvo = (const struct lws_protocol_vhost_options *)in;
+		while (pvo) {
+			if (!strcmp(pvo->name, "admin-user"))
+				strncpy(vhd->admin_user, pvo->value,
+					sizeof(vhd->admin_user) - 1);
+			if (!strcmp(pvo->name, "admin-password-sha1"))
+				strncpy(vhd->admin_password_sha1.id, pvo->value,
+					sizeof(vhd->admin_password_sha1.id) - 1);
+			if (!strcmp(pvo->name, "session-db"))
+				strncpy(vhd->session_db, pvo->value,
+					sizeof(vhd->session_db) - 1);
+			if (!strcmp(pvo->name, "confounder"))
+				strncpy(vhd->confounder, pvo->value,
+					sizeof(vhd->confounder) - 1);
+			if (!strcmp(pvo->name, "email-from"))
+				strncpy(vhd->email.email_from, pvo->value,
+					sizeof(vhd->email.email_from) - 1);
+			if (!strcmp(pvo->name, "email-helo"))
+				strncpy(vhd->email.email_helo, pvo->value,
+					sizeof(vhd->email.email_helo) - 1);
+			if (!strcmp(pvo->name, "email-template"))
+				strncpy(vhd->email_template, pvo->value,
+					sizeof(vhd->email_template) - 1);
+			if (!strcmp(pvo->name, "email-title"))
+				strncpy(vhd->email_title, pvo->value,
+					sizeof(vhd->email_title) - 1);
+			if (!strcmp(pvo->name, "email-contact-person"))
+				strncpy(vhd->email_contact_person, pvo->value,
+					sizeof(vhd->email_contact_person) - 1);
+			if (!strcmp(pvo->name, "email-confirm-url-base"))
+				strncpy(vhd->email_confirm_url, pvo->value,
+					sizeof(vhd->email_confirm_url) - 1);
+			if (!strcmp(pvo->name, "email-server-ip"))
+				strncpy(vhd->email.email_smtp_ip, pvo->value,
+					sizeof(vhd->email.email_smtp_ip) - 1);
+
+			if (!strcmp(pvo->name, "timeout-idle-secs"))
+				vhd->timeout_idle_secs = atoi(pvo->value);
+			if (!strcmp(pvo->name, "timeout-absolute-secs"))
+				vhd->timeout_absolute_secs = atoi(pvo->value);
+			if (!strcmp(pvo->name, "timeout-anon-absolute-secs"))
+				vhd->timeout_anon_absolute_secs = atoi(pvo->value);
+			if (!strcmp(pvo->name, "email-expire"))
+				vhd->timeout_email_secs = atoi(pvo->value);
+			pvo = pvo->next;
+		}
+		if (!vhd->admin_user[0] ||
+		    !vhd->admin_password_sha1.id[0] ||
+		    !vhd->session_db[0]) {
+			lwsl_err("generic-sessions: "
+				 "You must give \"admin-user\", "
+				 "\"admin-password-sha1\", "
+				 "and \"session_db\" per-vhost options\n");
+			return 1;
+		}
+
+		if (sqlite3_open_v2(vhd->session_db, &vhd->pdb,
+				    SQLITE_OPEN_READWRITE |
+				    SQLITE_OPEN_CREATE, NULL) != SQLITE_OK) {
+			lwsl_err("Unable to open session db %s: %s\n",
+				 vhd->session_db, sqlite3_errmsg(vhd->pdb));
+
+			return 1;
+		}
+
+		if (sqlite3_prepare(vhd->pdb,
+				    "create table if not exists sessions ("
+				    " name char(40),"
+				    " username varchar(32),"
+				    " expire integer"
+				    ");",
+				    -1, &sm, NULL) != SQLITE_OK) {
+			lwsl_err("Unable to prepare session table init: %s\n",
+				 sqlite3_errmsg(vhd->pdb));
+
+			return 1;
+		}
+
+		if (sqlite3_step(sm) != SQLITE_DONE) {
+			lwsl_err("Unable to run session table init: %s\n",
+				 sqlite3_errmsg(vhd->pdb));
+
+			return 1;
+		}
+		sqlite3_finalize(sm);
+
+		if (sqlite3_exec(vhd->pdb,
+				 "create table if not exists users ("
+				 " username varchar(32),"
+				 " creation_time integer,"
+				 " ip varchar(46),"
+				 " email varchar(100),"
+				 " pwhash varchar(42),"
+				 " pwsalt varchar(42),"
+				 " pwchange_time integer,"
+				 " token varchar(42),"
+				 " verified integer,"
+				 " token_time integer,"
+				 " last_forgot_validated integer,"
+				 " primary key (username)"
+				 ");",
+				 NULL, NULL, NULL) != SQLITE_OK) {
+			lwsl_err("Unable to create user table: %s\n",
+				 sqlite3_errmsg(vhd->pdb));
+
+			return 1;
+		}
+
+		sprintf(s, "create table if not exists email ("
+				 " username varchar(32),"
+				 " content blob,"
+				 " primary key (username)"
+				 ");");
+		if (sqlite3_exec(vhd->pdb, s, NULL, NULL, NULL) != SQLITE_OK) {
+			lwsl_err("Unable to create user table: %s\n",
+				 sqlite3_errmsg(vhd->pdb));
+
+			return 1;
+		}
+
+		lws_email_init(&vhd->email, lws_uv_getloop(vhd->context, 0),
+				LWSGS_EMAIL_CONTENT_SIZE);
+		break;
+
+	case LWS_CALLBACK_PROTOCOL_DESTROY:
+		if (vhd->pdb) {
+			sqlite3_close(vhd->pdb);
+			vhd->pdb = NULL;
+		}
+		lws_email_destroy(&vhd->email);
+		break;
+
+	case LWS_CALLBACK_HTTP:
+		lwsl_info("LWS_CALLBACK_HTTP: %s\n", in);
+
+		pss->login_session.id[0] = '\0';
+		pss->phs.pos = 0;
+		strncpy(pss->onward, (char *)in, sizeof(pss->onward));
+
+		if (!strcmp((const char *)in, "/lwsgs-forgot")) {
+			lwsgs_handler_forgot(vhd, wsi, pss);
+			goto redirect_with_cookie;
+		}
+
+		if (!strcmp((const char *)in, "/lwsgs-confirm")) {
+			lwsgs_handler_confirm(vhd, wsi, pss);
+			goto redirect_with_cookie;
+		}
+		if (!strcmp((const char *)in, "/lwsgs-check")) {
+			lwsgs_handler_check(vhd, wsi, pss);
+			goto try_to_reuse;
+		}
+
+		if (!strcmp((const char *)in, "/lwsgs-login"))
+			break;
+		if (!strcmp((const char *)in, "/lwsgs-logout"))
+			break;
+		if (!strcmp((const char *)in, "/lwsgs-forgot"))
+			break;
+		if (!strcmp((const char *)in, "/lwsgs-change"))
+			break;
+
+		/* if no legitimate url for GET, return 404 */
+
+		lwsl_err("http doing 404 on %s\n", in);
+		lws_return_http_status(wsi, HTTP_STATUS_NOT_FOUND, NULL);
+		goto try_to_reuse;
+
+	case LWS_CALLBACK_CHECK_ACCESS_RIGHTS:
+		n = 0;
+		username[0] = '\0';
+		sid.id[0] = '\0';
+		args = (struct lws_process_html_args *)in;
+		lwsl_debug("LWS_CALLBACK_CHECK_ACCESS_RIGHTS\n");
+		if (!lwsgs_get_sid_from_wsi(wsi, &sid)) {
+			if (lwsgs_lookup_session(vhd, &sid, username, sizeof(username))) {
+				static const char * const oprot[] = {
+					"http://", "https://"
+				};
+				lwsl_notice("session lookup for %s failed, probably expired\n", sid.id);
+				pss->delete_session = sid;
+				args->final = 1; /* signal we dealt with it */
+				if (lws_hdr_copy(wsi, cookie, sizeof(cookie) - 1,
+					     WSI_TOKEN_HOST) < 0)
+					return 1;
+				snprintf(pss->onward, sizeof(pss->onward) - 1,
+					 "%s%s%s", oprot[lws_is_ssl(wsi)],
+					    cookie, args->p);
+				lwsl_notice("redirecting to ourselves with cookie refresh\n");
+				/* we need a redirect to ourselves, session cookie is expired */
+				goto redirect_with_cookie;
+			}
+		} else
+			lwsl_notice("failed to get sid from wsi\n");
+
+		n = lwsgs_get_auth_level(vhd, username);
+
+		if ((args->max_len & n) != args->max_len) {
+			lwsl_notice("Access rights fail 0x%X vs 0x%X (cookie %s)\n",
+					args->max_len, n, sid.id);
+			return 1;
+		}
+		lwsl_debug("Access rights OK\n");
+		break;
+
+	case LWS_CALLBACK_SESSION_INFO:
+	{
+		struct lwsgs_user u;
+		sinfo = (struct lws_session_info *)in;
+		sinfo->username[0] = '\0';
+		sinfo->email[0] = '\0';
+		sinfo->ip[0] = '\0';
+		sinfo->session[0] = '\0';
+		sinfo->mask = 0;
+
+		sid.id[0] = '\0';
+		lwsl_debug("LWS_CALLBACK_SESSION_INFO\n");
+		if (lwsgs_get_sid_from_wsi(wsi, &sid))
+			break;
+		if (lwsgs_lookup_session(vhd, &sid, username, sizeof(username)))
+			break;
+
+		snprintf(s, sizeof(s) - 1,
+			 "select username, email from users where username='%s';",
+			 username);
+		if (sqlite3_exec(vhd->pdb, s, lwsgs_lookup_callback_user, &u, NULL) !=
+		    SQLITE_OK) {
+			lwsl_err("Unable to lookup token: %s\n",
+				 sqlite3_errmsg(vhd->pdb));
+			break;
+		}
+		strncpy(sinfo->username, u.username, sizeof(sinfo->username));
+		strncpy(sinfo->email, u.email, sizeof(sinfo->email));
+		strncpy(sinfo->session, sid.id, sizeof(sinfo->session));
+		sinfo->mask = lwsgs_get_auth_level(vhd, username);
+		lws_get_peer_simple(wsi, sinfo->ip, sizeof(sinfo->ip));
+	}
+
+		break;
+
+	case LWS_CALLBACK_PROCESS_HTML:
+
+		args = (struct lws_process_html_args *)in;
+		{
+			static const char * const vars[] = {
+				"$lwsgs_user",
+				"$lwsgs_auth",
+				"$lwsgs_email"
+			};
+			struct lwsgs_subst_args a;
+
+			a.vhd = vhd;
+			a.pss = pss;
+			a.wsi = wsi;
+
+			pss->phs.vars = vars;
+			pss->phs.count_vars = ARRAY_SIZE(vars);
+			pss->phs.replace = lwsgs_subst;
+			pss->phs.data = &a;
+
+			if (lws_chunked_html_process(args, &pss->phs))
+				return -1;
+		}
+		break;
+
+	case LWS_CALLBACK_HTTP_BODY:
+		if (len < 2)
+			break;
+
+		if (!pss->spa) {
+			pss->spa = lws_spa_create(wsi, param_names,
+						ARRAY_SIZE(param_names), 1024,
+						NULL, NULL);
+			if (!pss->spa)
+				return -1;
+		}
+
+		if (lws_spa_process(pss->spa, in, len)) {
+			lwsl_notice("spa process blew\n");
+			return -1;
+		}
+		break;
+
+	case LWS_CALLBACK_HTTP_BODY_COMPLETION:
+
+		if (!pss->spa)
+			break;
+
+		lwsl_info("LWS_CALLBACK_HTTP_BODY_COMPLETION: %s\n", pss->onward);
+		lws_spa_finalize(pss->spa);
+
+		if (!strcmp((char *)pss->onward, "/lwsgs-change")) {
+			if (!lwsgs_handler_change_password(vhd, wsi, pss)) {
+				cp = lws_spa_get_string(pss->spa, FGS_GOOD);
+				goto pass;
+			}
+
+			cp = lws_spa_get_string(pss->spa, FGS_BAD);
+			lwsl_notice("user/password no good %s\n",
+				lws_spa_get_string(pss->spa, FGS_USERNAME));
+			strncpy(pss->onward, cp, sizeof(pss->onward) - 1);
+			pss->onward[sizeof(pss->onward) - 1] = '\0';
+			goto completion_flow;
+		}
+
+		if (!strcmp((char *)pss->onward, "/lwsgs-login")) {
+			if (lws_spa_get_string(pss->spa, FGS_FORGOT) &&
+			    lws_spa_get_string(pss->spa, FGS_FORGOT)[0]) {
+				if (lwsgs_handler_forgot_pw_form(vhd, wsi, pss)) {
+					n = FGS_FORGOT_BAD;
+					goto reg_done;
+				}
+				/* get the email monitor to take a look */
+				lws_email_check(&vhd->email);
+				n = FGS_FORGOT_GOOD;
+				goto reg_done;
+			}
+
+			if (!lws_spa_get_string(pss->spa, FGS_USERNAME) ||
+			    !lws_spa_get_string(pss->spa, FGS_PASSWORD)) {
+				lwsl_notice("username '%s' or pw '%s' missing\n",
+						lws_spa_get_string(pss->spa, FGS_USERNAME),
+						lws_spa_get_string(pss->spa, FGS_PASSWORD));
+				return -1;
+			}
+
+			if (lws_spa_get_string(pss->spa, FGS_REGISTER) &&
+			    lws_spa_get_string(pss->spa, FGS_REGISTER)[0]) {
+
+				if (lwsgs_handler_register_form(vhd, wsi, pss))
+					n = FGS_REG_BAD;
+				else {
+					n = FGS_REG_GOOD;
+
+					/* get the email monitor to take a look */
+					lws_email_check(&vhd->email);
+				}
+reg_done:
+				strncpy(pss->onward, lws_spa_get_string(pss->spa, n),
+					sizeof(pss->onward) - 1);
+				pss->onward[sizeof(pss->onward) - 1] = '\0';
+				pss->login_expires = 0;
+				pss->logging_out = 1;
+				goto completion_flow;
+			}
+
+			/* we have the username and password... check if admin */
+			if (lwsgw_check_admin(vhd, lws_spa_get_string(pss->spa, FGS_USERNAME),
+					      lws_spa_get_string(pss->spa, FGS_PASSWORD))) {
+				if (lws_spa_get_string(pss->spa, FGS_ADMIN))
+					cp = lws_spa_get_string(pss->spa, FGS_ADMIN);
+				else
+					if (lws_spa_get_string(pss->spa, FGS_GOOD))
+						cp = lws_spa_get_string(pss->spa, FGS_GOOD);
+					else {
+						lwsl_info("No admin or good target url in form\n");
+						return -1;
+					}
+				lwsl_debug("admin\n");
+				goto pass;
+			}
+
+			/* check users in database */
+
+			if (!lwsgs_check_credentials(vhd, lws_spa_get_string(pss->spa, FGS_USERNAME),
+						     lws_spa_get_string(pss->spa, FGS_PASSWORD))) {
+				lwsl_info("pw hash check met\n");
+				cp = lws_spa_get_string(pss->spa, FGS_GOOD);
+				goto pass;
+			} else
+				lwsl_notice("user/password no good %s\n",
+						lws_spa_get_string(pss->spa, FGS_USERNAME));
+
+			if (!lws_spa_get_string(pss->spa, FGS_BAD)) {
+				lwsl_info("No admin or good target url in form\n");
+				return -1;
+			}
+
+			strncpy(pss->onward, lws_spa_get_string(pss->spa, FGS_BAD),
+				sizeof(pss->onward) - 1);
+			pss->onward[sizeof(pss->onward) - 1] = '\0';
+			lwsl_debug("failed\n");
+
+			goto completion_flow;
+		}
+
+		if (!strcmp((char *)pss->onward, "/lwsgs-logout")) {
+
+			lwsl_notice("/logout\n");
+
+			if (lwsgs_get_sid_from_wsi(wsi, &pss->login_session)) {
+				lwsl_notice("not logged in...\n");
+				return 1;
+			}
+
+			lwsgw_update_session(vhd, &pss->login_session, "");
+
+			if (!lws_spa_get_string(pss->spa, FGS_GOOD)) {
+				lwsl_info("No admin or good target url in form\n");
+				return -1;
+			}
+
+			strncpy(pss->onward, lws_spa_get_string(pss->spa, FGS_GOOD), sizeof(pss->onward) - 1);
+			pss->onward[sizeof(pss->onward) - 1] = '\0';
+
+			pss->login_expires = 0;
+			pss->logging_out = 1;
+
+			goto completion_flow;
+		}
+
+		break;
+
+pass:
+		strncpy(pss->onward, cp, sizeof(pss->onward) - 1);
+		pss->onward[sizeof(pss->onward) - 1] = '\0';
+
+		if (lwsgs_get_sid_from_wsi(wsi, &sid))
+			sid.id[0] = '\0';
+
+		pss->login_expires = lws_now_secs() +
+				     vhd->timeout_absolute_secs;
+
+		if (!sid.id[0]) {
+			/* we need to create a new, authorized session */
+
+			if (lwsgs_new_session_id(vhd, &pss->login_session,
+						 lws_spa_get_string(pss->spa, FGS_USERNAME),
+						 pss->login_expires))
+				goto try_to_reuse;
+
+			lwsl_info("Creating new session: %s\n",
+				    pss->login_session.id);
+		} else {
+			/*
+			 * we can just update the existing session to be
+			 * authorized
+			 */
+			lwsl_info("Authorizing existing session %s", sid.id);
+			lwsgw_update_session(vhd, &sid,
+				lws_spa_get_string(pss->spa, FGS_USERNAME));
+			pss->login_session = sid;
+		}
+
+completion_flow:
+		lwsgw_expire_old_sessions(vhd);
+		goto redirect_with_cookie;
+
+	case LWS_CALLBACK_HTTP_DROP_PROTOCOL:
+		if (pss->spa) {
+			lws_spa_destroy(pss->spa);
+			pss->spa = NULL;
+		}
+		break;
+
+	case LWS_CALLBACK_ADD_HEADERS:
+		lwsgw_expire_old_sessions(vhd);
+
+		args = (struct lws_process_html_args *)in;
+
+		if (pss->delete_session.id[0]) {
+			pc = cookie;
+			lwsgw_cookie_from_session(&pss->delete_session, 0, &pc,
+						  cookie + sizeof(cookie) - 1);
+
+			lwsl_info("deleting cookie '%s'\n", cookie);
+
+			if (lws_add_http_header_by_name(wsi,
+					(unsigned char *)"set-cookie:",
+					(unsigned char *)cookie, pc - cookie,
+					(unsigned char **)&args->p,
+					(unsigned char *)args->p + args->max_len))
+				return 1;
+		}
+
+		if (!pss->login_session.id[0])
+			lwsgs_get_sid_from_wsi(wsi, &pss->login_session);
+
+		if (!pss->login_session.id[0] && !pss->logging_out) {
+
+			pss->login_expires = lws_now_secs() +
+					     vhd->timeout_anon_absolute_secs;
+			if (lwsgs_new_session_id(vhd, &pss->login_session, "",
+						 pss->login_expires))
+				goto try_to_reuse;
+			pc = cookie;
+			lwsgw_cookie_from_session(&pss->login_session,
+						  pss->login_expires, &pc,
+						  cookie + sizeof(cookie) - 1);
+
+			lwsl_info("LWS_CALLBACK_ADD_HEADERS: setting cookie '%s'\n", cookie);
+			if (lws_add_http_header_by_name(wsi,
+					(unsigned char *)"set-cookie:",
+					(unsigned char *)cookie, pc - cookie,
+					(unsigned char **)&args->p,
+					(unsigned char *)args->p + args->max_len))
+				return 1;
+		}
+		break;
+
+	default:
+		break;
+	}
+
+	return 0;
+
+redirect_with_cookie:
+	p = buffer + LWS_PRE;
+	start = p;
+	end = p + sizeof(buffer) - LWS_PRE;
+
+	if (lws_add_http_header_status(wsi, HTTP_STATUS_SEE_OTHER, &p, end))
+		return 1;
+
+	if (lws_add_http_header_by_token(wsi, WSI_TOKEN_HTTP_LOCATION,
+			(unsigned char *)pss->onward,
+			strlen(pss->onward), &p, end))
+		return 1;
+
+	if (lws_add_http_header_by_token(wsi, WSI_TOKEN_HTTP_CONTENT_TYPE,
+			(unsigned char *)"text/html", 9, &p, end))
+		return 1;
+	if (lws_add_http_header_content_length(wsi, 0, &p, end))
+		return 1;
+
+	if (pss->delete_session.id[0]) {
+		lwsgw_cookie_from_session(&pss->delete_session, 0, &pc,
+					  cookie + sizeof(cookie) - 1);
+
+		lwsl_notice("deleting cookie '%s'\n", cookie);
+
+		if (lws_add_http_header_by_name(wsi,
+				(unsigned char *)"set-cookie:",
+				(unsigned char *)cookie, pc - cookie,
+				&p, end))
+			return 1;
+	}
+
+	if (!pss->login_session.id[0]) {
+		pss->login_expires = lws_now_secs() +
+				     vhd->timeout_anon_absolute_secs;
+		if (lwsgs_new_session_id(vhd, &pss->login_session, "",
+					 pss->login_expires))
+			return 1;
+	} else
+		pss->login_expires = lws_now_secs() +
+				     vhd->timeout_absolute_secs;
+
+	if (pss->login_session.id[0] || pss->logging_out) {
+		/*
+		 * we succeeded to login, we must issue a login
+		 * cookie with the prepared data
+		 */
+		pc = cookie;
+
+		lwsgw_cookie_from_session(&pss->login_session,
+					  pss->login_expires, &pc,
+					  cookie + sizeof(cookie) - 1);
+
+		lwsl_info("setting cookie '%s'\n", cookie);
+
+		pss->logging_out = 0;
+
+		if (lws_add_http_header_by_name(wsi,
+				(unsigned char *)"set-cookie:",
+				(unsigned char *)cookie, pc - cookie,
+				&p, end))
+			return 1;
+	}
+
+	if (lws_finalize_http_header(wsi, &p, end))
+		return 1;
+
+	n = lws_write(wsi, start, p - start, LWS_WRITE_HTTP_HEADERS);
+	if (n < 0)
+		return 1;
+
+	/* fallthru */
+
+try_to_reuse:
+	if (lws_http_transaction_completed(wsi))
+		return -1;
+
+	return 0;
+}
+
+static const struct lws_protocols protocols[] = {
+	{
+		"protocol-generic-sessions",
+		callback_generic_sessions,
+		sizeof(struct per_session_data__gs),
+		1024,
+	},
+};
+
+LWS_EXTERN LWS_VISIBLE int
+init_protocol_generic_sessions(struct lws_context *context,
+			struct lws_plugin_capability *c)
+{
+	if (c->api_magic != LWS_PLUGIN_API_MAGIC) {
+		lwsl_err("Plugin API %d, library API %d", LWS_PLUGIN_API_MAGIC,
+			 c->api_magic);
+		return 1;
+	}
+
+	c->protocols = protocols;
+	c->count_protocols = ARRAY_SIZE(protocols);
+	c->extensions = NULL;
+	c->count_extensions = 0;
+
+	return 0;
+}
+
+LWS_EXTERN LWS_VISIBLE int
+destroy_protocol_generic_sessions(struct lws_context *context)
+{
+	return 0;
+}
diff --git a/plugins/generic-sessions/utils.c b/plugins/generic-sessions/utils.c
new file mode 100644
index 0000000000000000000000000000000000000000..0d458d7b0008fadb926477701be8f084f2b2e8ee
--- /dev/null
+++ b/plugins/generic-sessions/utils.c
@@ -0,0 +1,450 @@
+/*
+ * ws protocol handler plugin for "generic sessions"
+ *
+ * Copyright (C) 2010-2016 Andy Green <andy@warmcat.com>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation:
+ * version 2.1 of the License.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ * MA  02110-1301  USA
+ */
+
+#include "private-lwsgs.h"
+
+void
+sha1_to_lwsgw_hash(unsigned char *hash, lwsgw_hash *shash)
+{
+	static const char *hex = "0123456789abcdef";
+	char *p = shash->id;
+	int n;
+
+	for (n = 0; n < 20; n++) {
+		*p++ = hex[hash[n] >> 4];
+		*p++ = hex[hash[n] & 15];
+	}
+
+	*p = '\0';
+}
+
+int
+lwsgw_check_admin(struct per_vhost_data__gs *vhd,
+		  const char *username, const char *password)
+{
+	lwsgw_hash_bin hash_bin;
+	lwsgw_hash pw_hash;
+
+	if (strcmp(vhd->admin_user, username))
+		return 0;
+
+	lws_SHA1((unsigned char *)password, strlen(password), hash_bin.bin);
+	sha1_to_lwsgw_hash(hash_bin.bin, &pw_hash);
+
+	return !strcmp(vhd->admin_password_sha1.id, pw_hash.id);
+}
+
+/*
+ * secure cookie: it can only be passed over https where it cannot be
+ *		  snooped in transit
+ * HttpOnly:	  it can only be accessed via http[s] transport, it cannot be
+ *		  gotten at by JS
+ */
+void
+lwsgw_cookie_from_session(lwsgw_hash *sid, time_t expires, char **p, char *end)
+{
+	struct tm *tm = gmtime(&expires);
+	time_t n = lws_now_secs();
+
+	*p += snprintf(*p, end - *p, "id=%s;Expires=", sid->id);
+#ifdef WIN32
+	*p += strftime(*p, end - *p, "%Y %H:%M %Z", tm);
+#else
+	*p += strftime(*p, end - *p, "%F %H:%M %Z", tm);
+#endif
+	*p += snprintf(*p, end - *p, ";path=/");
+	*p += snprintf(*p, end - *p, ";Max-Age=%lu", (unsigned long)(expires - n));
+//	*p += snprintf(*p, end - *p, ";secure");
+	*p += snprintf(*p, end - *p, ";HttpOnly");
+}
+
+int
+lwsgw_expire_old_sessions(struct per_vhost_data__gs *vhd)
+{
+	time_t n = lws_now_secs();
+	char s[200];
+
+	if (n - vhd->last_session_expire < 5)
+		return 0;
+
+	vhd->last_session_expire = n;
+
+	snprintf(s, sizeof(s) - 1,
+		 "delete from sessions where "
+		 "expire <= %lu;", (unsigned long)n);
+
+	if (sqlite3_exec(vhd->pdb, s, NULL, NULL, NULL) != SQLITE_OK) {
+		lwsl_err("Unable to expire sessions: %s\n",
+			 sqlite3_errmsg(vhd->pdb));
+		return 1;
+	}
+
+	return 0;
+}
+
+int
+lwsgw_update_session(struct per_vhost_data__gs *vhd,
+		     lwsgw_hash *hash, const char *user)
+{
+	time_t n = lws_now_secs();
+	char s[200], esc[50], esc1[50];
+
+	if (user[0])
+		n += vhd->timeout_absolute_secs;
+	else
+		n += vhd->timeout_anon_absolute_secs;
+
+	snprintf(s, sizeof(s) - 1,
+		 "update sessions set expire=%lu,username='%s' where name='%s';",
+		 (unsigned long)n,
+		 lws_sql_purify(esc, user, sizeof(esc)),
+		 lws_sql_purify(esc1, hash->id, sizeof(esc1)));
+
+	if (sqlite3_exec(vhd->pdb, s, NULL, NULL, NULL) != SQLITE_OK) {
+		lwsl_err("Unable to update session: %s\n",
+			 sqlite3_errmsg(vhd->pdb));
+		return 1;
+	}
+
+	return 0;
+}
+
+static int
+lwsgw_session_from_cookie(const char *cookie, lwsgw_hash *sid)
+{
+	const char *p = cookie;
+	int n;
+
+	while (*p) {
+		if (p[0] == 'i' && p[1] == 'd' && p[2] == '=') {
+			p += 3;
+			break;
+		}
+		p++;
+	}
+	if (!*p) {
+		lwsl_info("no id= in cookie\n");
+		return 1;
+	}
+
+	for (n = 0; n < sizeof(sid->id) - 1 && *p; n++) {
+		/* our SID we issue only has these chars */
+		if ((*p >= '0' && *p <= '9') ||
+		    (*p >= 'a' && *p <= 'f'))
+			sid->id[n] = *p++;
+		else {
+			lwsl_info("bad chars in cookie id %c\n", *p);
+			return 1;
+		}
+	}
+
+	if (n < sizeof(sid->id) - 1) {
+		lwsl_info("cookie id too short\n");
+		return 1;
+	}
+
+	sid->id[sizeof(sid->id) - 1] = '\0';
+
+	return 0;
+}
+
+int
+lwsgs_get_sid_from_wsi(struct lws *wsi, lwsgw_hash *sid)
+{
+	char cookie[1024];
+
+	/* fail it on no cookie */
+	if (!lws_hdr_total_length(wsi, WSI_TOKEN_HTTP_COOKIE)) {
+		lwsl_info("%s: no cookie\n", __func__);
+		return 1;
+	}
+	if (lws_hdr_copy(wsi, cookie, sizeof cookie, WSI_TOKEN_HTTP_COOKIE) < 0) {
+		lwsl_info("cookie copy failed\n");
+		return 1;
+	}
+	/* extract the sid from the cookie */
+	if (lwsgw_session_from_cookie(cookie, sid)) {
+		lwsl_info("session from cookie failed\n");
+		return 1;
+	}
+
+	return 0;
+}
+
+struct lla {
+	char *username;
+	int len;
+	int results;
+};
+
+static int
+lwsgs_lookup_callback(void *priv, int cols, char **col_val, char **col_name)
+{
+	struct lla *lla = (struct lla *)priv;
+
+	//lwsl_err("%s: %d\n", __func__, cols);
+
+	if (cols)
+		lla->results = 0;
+	if (col_val && col_val[0]) {
+		strncpy(lla->username, col_val[0], lla->len);
+		lla->username[lla->len - 1] = '\0';
+		lwsl_info("%s: %s\n", __func__, lla->username);
+	}
+
+	return 0;
+}
+
+int
+lwsgs_lookup_session(struct per_vhost_data__gs *vhd,
+		     const lwsgw_hash *sid, char *username, int len)
+{
+	struct lla lla = { username, len, 1 };
+	char s[150], esc[50];
+
+	lwsgw_expire_old_sessions(vhd);
+
+	snprintf(s, sizeof(s) - 1,
+		 "select username from sessions where name = '%s';",
+		 lws_sql_purify(esc, sid->id, sizeof(esc) - 1));
+
+	if (sqlite3_exec(vhd->pdb, s, lwsgs_lookup_callback, &lla, NULL) != SQLITE_OK) {
+		lwsl_err("Unable to create user table: %s\n",
+			 sqlite3_errmsg(vhd->pdb));
+
+		return 1;
+	}
+
+	/* 0 if found */
+	return lla.results;
+}
+
+int
+lwsgs_lookup_callback_user(void *priv, int cols, char **col_val, char **col_name)
+{
+	struct lwsgs_user *u = (struct lwsgs_user *)priv;
+	int n;
+
+	for (n = 0; n < cols; n++) {
+		if (!strcmp(col_name[n], "username")) {
+			strncpy(u->username, col_val[n], sizeof(u->username) - 1);
+			u->username[sizeof(u->username) - 1] = '\0';
+			continue;
+		}
+		if (!strcmp(col_name[n], "ip")) {
+			strncpy(u->ip, col_val[n], sizeof(u->ip) - 1);
+			u->ip[sizeof(u->ip) - 1] = '\0';
+			continue;
+		}
+		if (!strcmp(col_name[n], "creation_time")) {
+			u->created = atol(col_val[n]);
+			continue;
+		}
+		if (!strcmp(col_name[n], "last_forgot_validated")) {
+			if (col_val[n])
+				u->last_forgot_validated = atol(col_val[n]);
+			else
+				u->last_forgot_validated = 0;
+			continue;
+		}
+		if (!strcmp(col_name[n], "email")) {
+			strncpy(u->email, col_val[n], sizeof(u->email) - 1);
+			u->email[sizeof(u->email) - 1] = '\0';
+			continue;
+		}
+		if (!strcmp(col_name[n], "verified")) {
+			u->verified = atoi(col_val[n]);
+			continue;
+		}
+		if (!strcmp(col_name[n], "pwhash")) {
+			strncpy(u->pwhash.id, col_val[n], sizeof(u->pwhash.id) - 1);
+			u->pwhash.id[sizeof(u->pwhash.id) - 1] = '\0';
+			continue;
+		}
+		if (!strcmp(col_name[n], "pwsalt")) {
+			strncpy(u->pwsalt.id, col_val[n], sizeof(u->pwsalt.id) - 1);
+			u->pwsalt.id[sizeof(u->pwsalt.id) - 1] = '\0';
+			continue;
+		}
+		if (!strcmp(col_name[n], "token")) {
+			strncpy(u->token.id, col_val[n], sizeof(u->token.id) - 1);
+			u->token.id[sizeof(u->token.id) - 1] = '\0';
+			continue;
+		}
+	}
+	return 0;
+}
+
+int
+lwsgs_lookup_user(struct per_vhost_data__gs *vhd,
+		  const char *username, struct lwsgs_user *u)
+{
+	char s[150], esc[50];
+
+	u->username[0] = '\0';
+	snprintf(s, sizeof(s) - 1,
+		 "select username,creation_time,ip,email,verified,pwhash,pwsalt,last_forgot_validated "
+		 "from users where username = '%s';",
+		 lws_sql_purify(esc, username, sizeof(esc) - 1));
+
+	if (sqlite3_exec(vhd->pdb, s, lwsgs_lookup_callback_user, u, NULL) !=
+	    SQLITE_OK) {
+		lwsl_err("Unable to lookup user: %s\n",
+			 sqlite3_errmsg(vhd->pdb));
+
+		return -1;
+	}
+
+	return !u->username[0];
+}
+
+int
+lwsgs_new_session_id(struct per_vhost_data__gs *vhd,
+		     lwsgw_hash *sid, const char *username, int exp)
+{
+	unsigned char sid_rand[20];
+	const char *u;
+	char s[300], esc[50], esc1[50];
+
+	if (username)
+		u = username;
+	else
+		u = "";
+
+	if (!sid)
+		return 1;
+
+	memset(sid, 0, sizeof(*sid));
+
+	if (lws_get_random(vhd->context, sid_rand, sizeof(sid_rand)) !=
+			   sizeof(sid_rand))
+		return 1;
+
+	sha1_to_lwsgw_hash(sid_rand, sid);
+
+	snprintf(s, sizeof(s) - 1,
+		 "insert into sessions(name, username, expire) "
+		 "values ('%s', '%s', %u);",
+		 lws_sql_purify(esc, sid->id, sizeof(esc) - 1),
+		 lws_sql_purify(esc1, u, sizeof(esc1) - 1), exp);
+
+	if (sqlite3_exec(vhd->pdb, s, NULL, NULL, NULL) != SQLITE_OK) {
+		lwsl_err("Unable to insert session: %s\n",
+			 sqlite3_errmsg(vhd->pdb));
+
+		return 1;
+	}
+
+	return 0;
+}
+
+int
+lwsgs_get_auth_level(struct per_vhost_data__gs *vhd,
+		     const char *username)
+{
+	struct lwsgs_user u;
+	int n = 0;
+
+	/* we are logged in as some kind of user */
+	if (username[0]) {
+		n |= LWSGS_AUTH_LOGGED_IN;
+		/* we are logged in as admin */
+		if (!strcmp(username, vhd->admin_user))
+			n |= LWSGS_AUTH_VERIFIED | LWSGS_AUTH_ADMIN; /* automatically verified */
+	}
+
+	if (!lwsgs_lookup_user(vhd, username, &u)) {
+		if ((u.verified & 0xff) == LWSGS_VERIFIED_ACCEPTED)
+			n |= LWSGS_AUTH_VERIFIED;
+
+		if (u.last_forgot_validated > lws_now_secs() - 300)
+			n |= LWSGS_AUTH_FORGOT_FLOW;
+	}
+
+	return n;
+}
+
+int
+lwsgs_check_credentials(struct per_vhost_data__gs *vhd,
+			const char *username, const char *password)
+{
+	unsigned char buffer[300];
+	lwsgw_hash_bin hash_bin;
+	struct lwsgs_user u;
+	lwsgw_hash hash;
+	int n;
+
+	if (lwsgs_lookup_user(vhd, username, &u))
+		return -1;
+
+	lwsl_info("user %s found, salt '%s'\n", username, u.pwsalt.id);
+
+	/* [password in ascii][salt] */
+	n = snprintf((char *)buffer, sizeof(buffer) - 1,
+		     "%s-%s-%s", password, vhd->confounder, u.pwsalt.id);
+
+	/* sha1sum of password + salt */
+	lws_SHA1(buffer, n, hash_bin.bin);
+	sha1_to_lwsgw_hash(&hash_bin.bin[0], &hash);
+
+	return !!strcmp(hash.id, u.pwhash.id);
+}
+
+/* sets u->pwsalt and u->pwhash */
+
+int
+lwsgs_hash_password(struct per_vhost_data__gs *vhd,
+		    const char *password, struct lwsgs_user *u)
+{
+	lwsgw_hash_bin hash_bin;
+	lwsgw_hash hash;
+	unsigned char sid_rand[20];
+	unsigned char buffer[150];
+	int n;
+
+	/* create a random salt as big as the hash */
+
+	if (lws_get_random(vhd->context, sid_rand,
+			   sizeof(sid_rand)) !=
+			   sizeof(sid_rand)) {
+		lwsl_err("Problem getting random for salt\n");
+		return 1;
+	}
+	sha1_to_lwsgw_hash(sid_rand, &u->pwsalt);
+
+	if (lws_get_random(vhd->context, sid_rand,
+			   sizeof(sid_rand)) !=
+			   sizeof(sid_rand)) {
+		lwsl_err("Problem getting random for token\n");
+		return 1;
+	}
+	sha1_to_lwsgw_hash(sid_rand, &hash);
+
+	/* [password in ascii][salt] */
+	n = snprintf((char *)buffer, sizeof(buffer) - 1,
+		    "%s-%s-%s", password, vhd->confounder, u->pwsalt.id);
+
+	/* sha1sum of password + salt */
+	lws_SHA1(buffer, n, hash_bin.bin);
+	sha1_to_lwsgw_hash(&hash_bin.bin[0], &u->pwhash);
+
+	return 0;
+}
diff --git a/plugins/protocol_lws_status.c b/plugins/protocol_lws_status.c
index 817fe76ddb385fedbb91b21908f83280006759c4..912eec85ce7cdbefed91cfec6cab830c8ee6a8ba 100644
--- a/plugins/protocol_lws_status.c
+++ b/plugins/protocol_lws_status.c
@@ -25,6 +25,7 @@
 #include <time.h>
 #include <string.h>
 #ifdef WIN32
+#include <io.h>
 #include <gettimeofday.h>
 #endif
 
diff --git a/plugins/protocol_post_demo.c b/plugins/protocol_post_demo.c
index 9f0cab899cb33d9c625ac26658a34a0aafedcb9f..8520a3153d748361ef84304ec96e7d28f4222013 100644
--- a/plugins/protocol_post_demo.c
+++ b/plugins/protocol_post_demo.c
@@ -23,6 +23,13 @@
 #include "../lib/libwebsockets.h"
 
 #include <string.h>
+#include <fcntl.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#ifdef WIN32
+#include <io.h>
+#endif
+#include <stdio.h>
 
 struct per_session_data__post_demo {
 	struct lws_spa *spa;
@@ -31,7 +38,7 @@ struct per_session_data__post_demo {
 
 	char filename[256];
 	long file_length;
-	int fd;
+	lws_filefd_type fd;
 };
 
 static const char * const param_names[] = {
diff --git a/test-server/test-server-v2.0.c b/test-server/test-server-v2.0.c
index b6feafe7d73df42af296e8bcacb655440ac5646e..d12c0b6ba0436dd7f8849c22bae3ba74da80b318 100644
--- a/test-server/test-server-v2.0.c
+++ b/test-server/test-server-v2.0.c
@@ -82,6 +82,9 @@ static const struct lws_http_mount mount_post = {
 	NULL,	/* default filename if none given */
 	NULL,
 	NULL,
+	NULL,
+	NULL,
+	0,
 	0,
 	0,
 	0,
@@ -104,6 +107,9 @@ static const struct lws_http_mount mount = {
 	"test.html",	/* default filename if none given */
 	NULL,
 	NULL,
+	NULL,
+	NULL,
+	0,
 	0,
 	0,
 	0,
diff --git a/test-server/test-server.h b/test-server/test-server.h
index 954740299ebf6efa5107e8c5187c83ead796c23a..7158abc6898ab821765dd4a6029355878bdc849f 100644
--- a/test-server/test-server.h
+++ b/test-server/test-server.h
@@ -85,7 +85,7 @@ struct per_session_data__http {
 
 	char filename[256];
 	long file_length;
-	int post_fd;
+	lws_filefd_type post_fd;
 };
 
 /*