From 7a2fc442b61cb08a4ffbcdee0912fd56941e7d46 Mon Sep 17 00:00:00 2001 From: Andy Green Date: Thu, 19 May 2016 15:28:31 +0800 Subject: [PATCH] protocol generic sessions Signed-off-by: Andy Green --- CMakeLists.txt | 122 ++- README.generic-sessions.md | 383 ++++++++ appveyor.yml | 5 +- lib/context.c | 47 +- lib/lejp-conf.c | 36 +- lib/libwebsockets.c | 68 +- lib/libwebsockets.h | 47 + lib/output.c | 49 +- lib/private-libwebsockets.h | 2 + lib/server.c | 193 +++- .../generic-sessions/assets/admin-login.html | 5 + .../generic-sessions/assets/failed-login.html | 3 + plugins/generic-sessions/assets/index.html | 35 + .../generic-sessions/assets/lwsgs-logo.png | Bin 0 -> 9729 bytes plugins/generic-sessions/assets/lwsgs.js | 476 +++++++++ plugins/generic-sessions/assets/md5.min.js | 2 + .../assets/post-forgot-fail.html | 5 + .../assets/post-forgot-ok.html | 6 + .../assets/post-register-fail.html | 1 + .../assets/post-register-ok.html | 27 + .../assets/post-verify-fail.html | 20 + .../assets/post-verify-ok.html | 25 + plugins/generic-sessions/assets/seats.jpg | Bin 0 -> 122754 bytes .../assets/sent-forgot-fail.html | 5 + .../assets/sent-forgot-ok.html | 4 + .../assets/successful-login.html | 4 + plugins/generic-sessions/handlers.c | 598 ++++++++++++ plugins/generic-sessions/private-lwsgs.h | 161 ++++ .../protocol_generic_sessions.c | 901 ++++++++++++++++++ plugins/generic-sessions/utils.c | 450 +++++++++ plugins/protocol_lws_status.c | 1 + plugins/protocol_post_demo.c | 9 +- test-server/test-server-v2.0.c | 6 + test-server/test-server.h | 2 +- 34 files changed, 3596 insertions(+), 102 deletions(-) create mode 100644 README.generic-sessions.md create mode 100644 plugins/generic-sessions/assets/admin-login.html create mode 100644 plugins/generic-sessions/assets/failed-login.html create mode 100644 plugins/generic-sessions/assets/index.html create mode 100644 plugins/generic-sessions/assets/lwsgs-logo.png create mode 100644 plugins/generic-sessions/assets/lwsgs.js create mode 100644 plugins/generic-sessions/assets/md5.min.js create mode 100644 plugins/generic-sessions/assets/post-forgot-fail.html create mode 100644 plugins/generic-sessions/assets/post-forgot-ok.html create mode 100644 plugins/generic-sessions/assets/post-register-fail.html create mode 100644 plugins/generic-sessions/assets/post-register-ok.html create mode 100644 plugins/generic-sessions/assets/post-verify-fail.html create mode 100644 plugins/generic-sessions/assets/post-verify-ok.html create mode 100644 plugins/generic-sessions/assets/seats.jpg create mode 100644 plugins/generic-sessions/assets/sent-forgot-fail.html create mode 100644 plugins/generic-sessions/assets/sent-forgot-ok.html create mode 100644 plugins/generic-sessions/assets/successful-login.html create mode 100644 plugins/generic-sessions/handlers.c create mode 100644 plugins/generic-sessions/private-lwsgs.h create mode 100644 plugins/generic-sessions/protocol_generic_sessions.c create mode 100644 plugins/generic-sessions/utils.c diff --git a/CMakeLists.txt b/CMakeLists.txt index eceddacd0..33642ab15 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 000000000..dab287349 --- /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 + + +``` + + + + + + + + + + + +
+ + +
+
+ + + + + + +``` + +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
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 ", + "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 b5cb9afcd..4bbc0f1ae 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 0d3a41c13..d3c22d6b7 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 1d529a1fd..91429a54c 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 cac78db5b..f8d08fc89 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 b6ec9bbcc..11f6fffcd 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 f14a4a3f5..be0c73c59 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 5ce641314..12719fcbc 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 8aafdbf58..d2398e31f 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 (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"); + 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; + 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 000000000..113df9cd3 --- /dev/null +++ b/plugins/generic-sessions/assets/admin-login.html @@ -0,0 +1,5 @@ + +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. + diff --git a/plugins/generic-sessions/assets/failed-login.html b/plugins/generic-sessions/assets/failed-login.html new file mode 100644 index 000000000..9ab065b53 --- /dev/null +++ b/plugins/generic-sessions/assets/failed-login.html @@ -0,0 +1,3 @@ + +This is an example destination that will appear after a failed login + diff --git a/plugins/generic-sessions/assets/index.html b/plugins/generic-sessions/assets/index.html new file mode 100644 index 000000000..ea970eecf --- /dev/null +++ b/plugins/generic-sessions/assets/index.html @@ -0,0 +1,35 @@ + + + + + + + + + + + + +
+ + +
+
+ + +
+ + + + + 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 GIT binary patch literal 9729 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 ARR910 literal 0 HcmV?d00001 diff --git a/plugins/generic-sessions/assets/lwsgs.js b/plugins/generic-sessions/assets/lwsgs.js new file mode 100644 index 000000000..5362c9a91 --- /dev/null +++ b/plugins/generic-sessions/assets/lwsgs.js @@ -0,0 +1,476 @@ + + +var lwsgs_user = "$lwsgs_user"; +var lwsgs_auth = "$lwsgs_auth"; +var lwsgs_email = "$lwsgs_email"; + +var lwsgs_html = '\ + \ +\ + \ +\ + \ + \ + \ + \ + \ +'; + +/*-- 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<>>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<>>9<<4)+14]=r;var e,i,a,h,d,l=1732584193,g=-271733879,v=-1732584194,m=271733878;for(e=0;e>5]>>>t%32&255);return r}function h(n){var t,r=[];for(r[(n.length>>2)-1]=void 0,t=0;t>5]|=(255&n.charCodeAt(t/8))<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>>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 = + "\u2713"; + 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 = + "\u2718 Passwords do not match"; + } else + document.getElementById('match').innerHTML = + "\u2718 Passwords do not match"; + + 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 = "\u2713"; + 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 = "\u2713"; + else + document.getElementById('uchk').innerHTML = ""; + } else { + document.getElementById('uchk').innerHTML = "\u2718 Already registered"; + en_forgot = 1; + } + + if (lwsgs_email_check === '0') { + if (document.getElementById('email').value) + document.getElementById('echk').innerHTML = "\u2713"; + else + document.getElementById('echk').innerHTML = ""; + } else { + document.getElementById('echk').innerHTML = "\u2718 Already registered"; + 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 = "\u2718"; + } 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 = "\u2713"; + 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 = + "\u2718 Passwords do not match"; + } else + document.getElementById('cmatch').innerHTML = "\u2718 Passwords do not match"; + + 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 = "\u2713"; + 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 = "\u2713"; + else + document.getElementById('cechk').innerHTML = ""; + } else { + document.getElementById('cechk').innerHTML = "\u2718 Already registered"; + 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 = + ""; + //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 000000000..4bd9de1e9 --- /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<>>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<>>9<<4)+14]=r;var e,i,a,h,d,l=1732584193,g=-271733879,v=-1732584194,m=271733878;for(e=0;e>5]>>>t%32&255);return r}function h(n){var t,r=[];for(r[(n.length>>2)-1]=void 0,t=0;t>5]|=(255&n.charCodeAt(t/8))<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>>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 000000000..ead3d13ec --- /dev/null +++ b/plugins/generic-sessions/assets/post-forgot-fail.html @@ -0,0 +1,5 @@ + +Sorry, something went wrong. + +Click here to continue. + 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 000000000..3e8e9cf59 --- /dev/null +++ b/plugins/generic-sessions/assets/post-forgot-ok.html @@ -0,0 +1,6 @@ + +This is a one-time password recovery login. + +Please click here and click your username at the top to reset your password. + + 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 000000000..063c3c50f --- /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 000000000..2d1503581 --- /dev/null +++ b/plugins/generic-sessions/assets/post-register-ok.html @@ -0,0 +1,27 @@ + + + + + + + + + + + + +
+ +
+ Your registration as is accepted,
+ you will receive an email shortly with instructions
+ to verify and enable the account for normal use.

+ The link is only valid for an hour, after that if it has
+ not been verified your account will be deleted. +
+ + + + 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 000000000..d1d89ca56 --- /dev/null +++ b/plugins/generic-sessions/assets/post-verify-fail.html @@ -0,0 +1,20 @@ + + + + + + + + + + + + +
+ +
+ Sorry, the link was invalid. +
+ + + 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 000000000..e968f6a75 --- /dev/null +++ b/plugins/generic-sessions/assets/post-verify-ok.html @@ -0,0 +1,25 @@ + + + + + + + + + + + + +
+ +
+ Thanks for signing up, your registration as is verified.
+
+ Click here to continue. +
+ + + + diff --git a/plugins/generic-sessions/assets/seats.jpg b/plugins/generic-sessions/assets/seats.jpg new file mode 100644 index 0000000000000000000000000000000000000000..5bed40d919872359f2fcc00a8430e7dcfabaab4a GIT binary patch literal 122754 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 literal 0 HcmV?d00001 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 000000000..ead3d13ec --- /dev/null +++ b/plugins/generic-sessions/assets/sent-forgot-fail.html @@ -0,0 +1,5 @@ + +Sorry, something went wrong. + +Click here to continue. + 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 000000000..83df7510a --- /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 000000000..dfc25cf74 --- /dev/null +++ b/plugins/generic-sessions/assets/successful-login.html @@ -0,0 +1,4 @@ + +This is an example destination that will appear after successful non-Admin login + + diff --git a/plugins/generic-sessions/handlers.c b/plugins/generic-sessions/handlers.c new file mode 100644 index 000000000..bd00c2b4c --- /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 + * + * 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 000000000..cb408ae8c --- /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 + * + * 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 +#include + +#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 000000000..6bf5d2b2d --- /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 + * + * 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 000000000..0d458d7b0 --- /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 + * + * 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 817fe76dd..912eec85c 100644 --- a/plugins/protocol_lws_status.c +++ b/plugins/protocol_lws_status.c @@ -25,6 +25,7 @@ #include #include #ifdef WIN32 +#include #include #endif diff --git a/plugins/protocol_post_demo.c b/plugins/protocol_post_demo.c index 9f0cab899..8520a3153 100644 --- a/plugins/protocol_post_demo.c +++ b/plugins/protocol_post_demo.c @@ -23,6 +23,13 @@ #include "../lib/libwebsockets.h" #include +#include +#include +#include +#ifdef WIN32 +#include +#endif +#include 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 b6feafe7d..d12c0b6ba 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 954740299..7158abc68 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; }; /*