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 000000000..723a12443 Binary files /dev/null and b/plugins/generic-sessions/assets/lwsgs-logo.png differ diff --git a/plugins/generic-sessions/assets/lwsgs.js b/plugins/generic-sessions/assets/lwsgs.js new file mode 100644 index 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 000000000..5bed40d91 Binary files /dev/null and b/plugins/generic-sessions/assets/seats.jpg differ diff --git a/plugins/generic-sessions/assets/sent-forgot-fail.html b/plugins/generic-sessions/assets/sent-forgot-fail.html new file mode 100644 index 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; }; /*