diff --git a/CMakeLists.txt b/CMakeLists.txt index ad694cbb85..78bfe8069c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -99,7 +99,10 @@ 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") @@ -111,6 +114,10 @@ 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) @@ -121,6 +128,17 @@ 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) + set(LWS_WITH_STATEFUL_URLDECODE 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) @@ -193,6 +211,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) @@ -280,6 +301,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. @@ -865,6 +895,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} ) @@ -1255,6 +1301,17 @@ if (NOT LWS_WITHOUT_CLIENT) "plugins/protocol_client_loopback_test.c") endif(NOT LWS_WITHOUT_CLIENT) +if (LWS_WITH_GENERIC_SESSIONS) + create_plugin(protocol_generic_sessions + "plugins/protocol_generic_sessions.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) # @@ -1455,6 +1512,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-login-example.html + plugins/generic-sessions-register-example.html + plugins/lwsws-logo.png + plugins/failed-login.html + plugins/lwsgs.js + plugins/post-register-fail.html + plugins/post-register-ok.html + plugins/post-verify-ok.html + plugins/post-verify-fail.html + plugins/sent-forgot-ok.html + plugins/sent-forgot-fail.html + plugins/post-forgot-ok.html + plugins/post-forgot-fail.html + plugins/index.html + DESTINATION share/libwebsockets-test-server/generic-sessions + COMPONENT examples) + install(FILES plugins/successful-login.html + DESTINATION share/libwebsockets-test-server/generic-sessions/needauth + COMPONENT examples) + install(FILES plugins/admin-login.html + DESTINATION share/libwebsockets-test-server/generic-sessions/needadmin + COMPONENT examples) +endif() + endif() # Install the LibwebsocketsConfig.cmake and LibwebsocketsConfigVersion.cmake @@ -1526,6 +1608,10 @@ 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("---------------------------------------------------------------------") # These will be available to parent projects including libwebsockets using add_subdirectory() diff --git a/README.generic-sessions.md b/README.generic-sessions.md new file mode 100644 index 0000000000..a9d28f908d --- /dev/null +++ b/README.generic-sessions.md @@ -0,0 +1,198 @@ +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, 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 + + - 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. + + +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 +------------ + +(subject to change...) + +"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-generic-sessions", + "default": "generic-sessions-login-example.html", + "auth-mask": "0", + "interpret": { + ".js": "protocol-generic-sessions" + } + }, { + # 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-generic-sessions", + "default": "generic-sessions-login-example.html", + "auth-mask": "5", # logged in as a verified user + "interpret": { + ".js": "protocol-generic-sessions" + } + }, { + # 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-generic-sessions", + "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-generic-sessions" + } + } +``` + +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 + + +Password Confounder +------------------- + +You can also define a per-vhost confounder 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. + diff --git a/appveyor.yml b/appveyor.yml index b5cb9afcd7..4bbc0f1aef 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 0d3a41c13e..468d7c331a 100644 --- a/lib/context.c +++ b/lib/context.c @@ -165,15 +165,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 +289,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 +385,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 31b03be9c3..66ae2c4986 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[]", @@ -92,7 +95,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, @@ -100,6 +105,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, @@ -123,6 +129,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; @@ -332,8 +339,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; } @@ -393,11 +402,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; @@ -474,6 +490,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 acd11452bc..9b8ac6b9b7 100644 --- a/lib/libwebsockets.c +++ b/lib/libwebsockets.c @@ -208,7 +208,7 @@ lws_close_free_wsi(struct lws *wsi, enum lws_close_status reason) wsi->u.http.fd != LWS_INVALID_FILE) { lws_plat_file_close(wsi, wsi->u.http.fd); wsi->u.http.fd = LWS_INVALID_FILE; - wsi->vhost->protocols[0].callback(wsi, + wsi->vhost->protocols->callback(wsi, LWS_CALLBACK_CLOSED_HTTP, wsi->user_space, NULL, 0); } if (wsi->socket_is_permanently_unusable || @@ -247,9 +247,14 @@ lws_close_free_wsi(struct lws *wsi, enum lws_close_status reason) wsi->mode == LWSCM_WSCL_ISSUE_HANDSHAKE) goto just_kill_connection; - if (wsi->mode == LWSCM_HTTP_SERVING) - wsi->vhost->protocols[0].callback(wsi, LWS_CALLBACK_CLOSED_HTTP, + if (wsi->mode == LWSCM_HTTP_SERVING) { + if (wsi->user_space) + wsi->vhost->protocols->callback(wsi, + LWS_CALLBACK_HTTP_DROP_PROTOCOL, + wsi->user_space, NULL, 0); + wsi->vhost->protocols->callback(wsi, LWS_CALLBACK_CLOSED_HTTP, wsi->user_space, NULL, 0); + } if (wsi->mode == LWSCM_HTTP_CLIENT) wsi->vhost->protocols[0].callback(wsi, LWS_CALLBACK_CLOSED_CLIENT_HTTP, wsi->user_space, NULL, 0); @@ -483,7 +488,7 @@ lws_close_free_wsi(struct lws *wsi, enum lws_close_status reason) wsi->user_space, NULL, 0); } else if (wsi->mode == LWSCM_HTTP_SERVING_ACCEPTED) { lwsl_debug("calling back CLOSED_HTTP\n"); - wsi->vhost->protocols[0].callback(wsi, LWS_CALLBACK_CLOSED_HTTP, + wsi->vhost->protocols->callback(wsi, LWS_CALLBACK_CLOSED_HTTP, wsi->user_space, NULL, 0 ); } else if (wsi->mode == LWSCM_WSCL_WAITING_SERVER_REPLY || wsi->mode == LWSCM_WSCL_WAITING_CONNECT) { @@ -1678,35 +1683,514 @@ lws_socket_bind(struct lws_vhost *vhost, int sockfd, int port, return port; } +static const char *hex = "0123456789ABCDEF"; + +/** + * lws_sql_purify() - like strncpy but with escaping for sql quotes + * + * @escaped: output buffer + * @string: input buffer ('/0' terminated) + * @len: output buffer max length + * + * Because escaping expands the output string, it's not + * possible to do it in-place, ie, with escaped == string + */ + +LWS_VISIBLE LWS_EXTERN const char * +lws_sql_purify(char *escaped, const char *string, int len) +{ + const char *p = string; + char *q = escaped; + + while (*p && len-- > 2) { + if (*p == '\'') { + *q++ = '\\'; + *q++ = '\''; + len --; + p++; + } else + *q++ = *p++; + } + *q = '\0'; + + return escaped; +} + +/** + * lws_urlencode() - like strncpy but with urlencoding + * + * @escaped: output buffer + * @string: input buffer ('/0' terminated) + * @len: output buffer max length + * + * Because urlencoding expands the output string, it's not + * possible to do it in-place, ie, with escaped == string + */ + +LWS_VISIBLE LWS_EXTERN const char * +lws_urlencode(char *escaped, const char *string, int len) +{ + const char *p = string; + char *q = escaped; + + while (*p && len-- > 3) { + if (*p == ' ') { + *q++ = '+'; + p++; + continue; + } + if ((*p >= '0' && *p <= '9') || + (*p >= 'A' && *p <= 'Z') || + (*p >= 'a' && *p <= 'z')) { + *q++ = *p++; + continue; + } + *q++ = '%'; + *q++ = hex[(*p >> 4) & 0xf]; + *q++ = hex[*p & 0xf]; + + len -= 2; + p++; + } + *q = '\0'; + + return escaped; +} + +/** + * lws_urldecode() - like strncpy but with urldecoding + * + * @string: output buffer + * @escaped: input buffer ('\0' terminated) + * @len: output buffer max length + * + * This is only useful for '\0' terminated strings + * + * Since urldecoding only shrinks the output string, it is possible to + * do it in-place, ie, string == escaped + */ + LWS_VISIBLE LWS_EXTERN int -lws_urlencode(const char *in, int inlen, char *out, int outlen) +lws_urldecode(char *string, const char *escaped, int len) { - const char *hex = "0123456789ABCDEF"; - char *start = out, *end = out + outlen; + int state = 0, n; + char sum = 0; + + while (*escaped && len) { + switch (state) { + case 0: + if (*escaped == '%') { + state++; + escaped++; + continue; + } + if (*escaped == '+') { + escaped++; + *string++ = ' '; + len--; + continue; + } + *string++ = *escaped++; + len--; + break; + case 1: + n = char_to_hex(*escaped); + if (n < 0) + return -1; + escaped++; + sum = n << 4; + state++; + break; - while (inlen-- && out < end - 4) { - if ((*in >= 'A' && *in <= 'Z') || - (*in >= 'a' && *in <= 'z') || - (*in >= '0' && *in <= '9') || - *in == '-' || - *in == '_' || - *in == '.' || - *in == '~') - *out++ = *in++; - else { - *out++ = '%'; - *out++ = hex[(*in) >> 4]; - *out++ = hex[(*in++) & 15]; + case 2: + n = char_to_hex(*escaped); + if (n < 0) + return -1; + escaped++; + *string++ = sum | n; + len--; + state = 0; + break; } + } - *out = '\0'; + *string = '\0'; - if (out >= end - 4) + return 0; +} + +#ifdef LWS_WITH_STATEFUL_URLDECODE + +enum urldecode_stateful { + US_NAME, + US_IDLE, + US_PC1, + US_PC2, +}; + +struct lws_urldecode_stateful { + char *out; + void *data; + char name[32]; + int out_len; + int pos; + + enum urldecode_stateful state; + + lws_urldecode_stateful_cb output; +}; + +static struct lws_urldecode_stateful * +lws_urldecode_s_create(char *out, int out_len, void *data, + lws_urldecode_stateful_cb output) +{ + struct lws_urldecode_stateful *s = lws_malloc(sizeof(*s)); + + if (!s) + return NULL; + + s->out = out; + s->out_len = out_len; + s->output = output; + s->pos = 0; + s->state = US_NAME; + s->name[0] = '\0'; + s->data = data; + + return s; +} + +static int +lws_urldecode_s_process(struct lws_urldecode_stateful *s, const char *in, int len) +{ + int n; + char sum = 0; + + while (len--) { + if (s->pos == s->out_len) { + if (s->output(s->data, s->name, &s->out, s->pos, 0)) + return -1; + + s->pos = 0; + } + + switch (s->state) { + case US_NAME: + //lwsl_notice("US_NAME: %c\n", *in); + if (*in == '=') { + s->name[s->pos] = '\0'; + s->pos = 0; + s->state = US_IDLE; + in++; + continue; + } + if (*in == '&') { + s->name[s->pos] = '\0'; + if (s->output(s->data, s->name, &s->out, s->pos, 1)) + return -1; + s->pos = 0; + s->state = US_IDLE; + in++; + continue; + } + if (s->pos >= sizeof(s->name) - 1) { + lwsl_notice("Name too long\n"); + return -1; + } + s->name[s->pos++] = *in++; + break; + case US_IDLE: + //lwsl_notice("US_IDLE: %c\n", *in); + if (*in == '%') { + s->state++; + in++; + continue; + } + if (*in == '&') { + s->out[s->pos] = '\0'; + if (s->output(s->data, s->name, &s->out, s->pos, 1)) + return -1; + s->pos = 0; + s->state = US_NAME; + in++; + continue; + } + if (*in == '+') { + in++; + s->out[s->pos++] = ' '; + continue; + } + s->out[s->pos++] = *in++; + break; + case US_PC1: + n = char_to_hex(*in); + if (n < 0) + return -1; + + in++; + sum = n << 4; + s->state++; + break; + + case US_PC2: + n = char_to_hex(*in); + if (n < 0) + return -1; + + in++; + s->out[s->pos++] = sum | n; + s->state = US_IDLE; + break; + } + + } + + return 0; +} + +static int +lws_urldecode_s_destroy(struct lws_urldecode_stateful *s) +{ + int ret = 0; + + if (s->state != US_IDLE) + ret = -1; + + if (!ret) + if (s->output(s->data, s->name, &s->out, s->pos, 1)) + ret = -1; + + lws_free(s); + + return ret; +} + +struct lws_urldecode_stateful_param_array { + struct lws_urldecode_stateful *s; + lws_urldecode_stateful_cb opt_cb; + const char * const *param_names; + int count_params; + char **params; + int *param_length; + void *opt_data; + + char *storage; + char *end; + int max_storage; +}; + +static int +lws_urldecode_spa_lookup(struct lws_urldecode_stateful_param_array *spa, + const char *name) +{ + int n; + + for (n = 0; n < spa->count_params; n++) + if (!strcmp(spa->param_names[n], name)) + return n; + + return -1; +} + +static int +lws_urldecode_spa_cb(void *data, const char *name, char **buf, int len, + int final) +{ + struct lws_urldecode_stateful_param_array *spa = + (struct lws_urldecode_stateful_param_array *)data; + int n; + + if (spa->opt_cb) { + n = spa->opt_cb(spa->opt_data, name, buf, len, final); + + if (n < 0) + return -1; + if (n > 0) + return 0; + } + n = lws_urldecode_spa_lookup(spa, name); + + lwsl_debug("%s: name %s, buf=%p, len=%d\n", __func__, name, buf, len); + + if (n == -1 || !len) /* unrecognized */ + return 0; + + if (!spa->params[n]) + spa->params[n] = *buf; + + if ((*buf) + len >= spa->end) { + lwsl_notice("%s: exceeded storage\n", __func__); return -1; + } - return out - start; + spa->param_length[n] += len; + + /* move it on inside storage */ + (*buf) += len; + *((*buf)++) = '\0'; + + return 0; +} + +/** + * lws_urldecode_spa_create() - create urldecode parser + * + * @param_names: array of form parameter names, like "username" + * @count_params: count of param_names + * @max_storage: total amount of form parameter values we can store + * @opt_cb: NULL, or callback to filter data. Needed for file transfer case + * @opt_data: NULL, or user pointer provided to opt_cb. + * + * Creates a urldecode parser and initializes it. + * + * @opt_cb can be NULL if you just want normal name=value parsing, however + * if one or more entries in your form are bulk data (file transfer), you + * can provide this callback and filter on the name callback parameter to + * treat that urldecoded data separately. The callback should return -1 + * in case of fatal error, 1 if it handled the data itself and 0 if it + * wants the data to be handled as name=value. + */ + +LWS_VISIBLE LWS_EXTERN struct lws_urldecode_stateful_param_array * +lws_urldecode_spa_create(const char * const *param_names, int count_params, + int max_storage, lws_urldecode_stateful_cb opt_cb, + void *opt_data) +{ + struct lws_urldecode_stateful_param_array *spa = lws_malloc(sizeof(*spa)); + + if (!spa) + return NULL; + + spa->param_names = param_names; + spa->count_params = count_params; + spa->max_storage = max_storage; + spa->opt_cb = opt_cb; + spa->opt_data = opt_data; + + spa->storage = lws_malloc(max_storage); + if (!spa->storage) + goto bail2; + spa->end = spa->storage + max_storage - 1; + + spa->params = lws_zalloc(sizeof(char *) * count_params); + if (!spa->params) + goto bail3; + + spa->s = lws_urldecode_s_create(spa->storage, max_storage, spa, + lws_urldecode_spa_cb); + if (!spa->s) + goto bail4; + + spa->param_length = lws_zalloc(sizeof(int) * count_params); + if (!spa->param_length) + goto bail5; + + return spa; + +bail5: + lws_urldecode_s_destroy(spa->s); +bail4: + lws_free(spa->params); +bail3: + lws_free(spa->storage); +bail2: + lws_free(spa); + + return NULL; +} + +/** + * lws_urldecode_spa_process() - parses a chunk of input data + * + * @ludspa: the parser object previously created + * @in: incoming, urlencoded data + * @len: count of bytes valid at @in + */ + +LWS_VISIBLE LWS_EXTERN int +lws_urldecode_spa_process(struct lws_urldecode_stateful_param_array *ludspa, + const char *in, int len) +{ + return lws_urldecode_s_process(ludspa->s, in, len); +} + +/** + * lws_urldecode_spa_get_length() - return length of parameter value + * + * @ludspa: the parser object previously created + * @n: parameter ordinal to return length of value for + */ + +LWS_VISIBLE LWS_EXTERN int +lws_urldecode_spa_get_length(struct lws_urldecode_stateful_param_array *ludspa, + int n) +{ + if (n >= ludspa->count_params) + return 0; + + return ludspa->param_length[n]; +} + +/** + * lws_urldecode_spa_get_string() - return pointer to parameter value + * + * @ludspa: the parser object previously created + * @n: parameter ordinal to return pointer to value for + */ + +LWS_VISIBLE LWS_EXTERN const char * +lws_urldecode_spa_get_string(struct lws_urldecode_stateful_param_array *ludspa, + int n) +{ + if (n >= ludspa->count_params) + return NULL; + + return ludspa->params[n]; +} + +/** + * lws_urldecode_spa_finalize() - indicate incoming data completed + * + * @ludspa: the parser object previously created + */ + +LWS_VISIBLE LWS_EXTERN int +lws_urldecode_spa_finalize(struct lws_urldecode_stateful_param_array *spa) +{ + if (spa->s) { + lws_urldecode_s_destroy(spa->s); + spa->s = NULL; + } + + return 0; +} + +/** + * lws_urldecode_spa_destroy() - destroy parser object + * + * @ludspa: the parser object previously created + */ + +LWS_VISIBLE LWS_EXTERN int +lws_urldecode_spa_destroy(struct lws_urldecode_stateful_param_array *spa) +{ + int n = 0; + + if (spa->s) + lws_urldecode_s_destroy(spa->s); + + lwsl_debug("%s\n", __func__); + + lws_free(spa->param_length); + lws_free(spa->params); + lws_free(spa->storage); + lws_free(spa); + + return n; } +#endif + LWS_VISIBLE LWS_EXTERN int lws_finalize_startup(struct lws_context *context) { @@ -1733,6 +2217,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) { @@ -1892,7 +2409,7 @@ lws_cgi(struct lws *wsi, const char * const *exec_array, int script_uri_path_len *p++ = *t++; if (*t == '=') *p++ = *t++; - i = lws_urlencode(t, i- (t - tok), p, end - p); + i = urlencode(t, i- (t - tok), p, end - p); if (i > 0) { p += i; *p++ = '&'; diff --git a/lib/libwebsockets.h b/lib/libwebsockets.h index dcb3e6427e..26171c13fb 100644 --- a/lib/libwebsockets.h +++ b/lib/libwebsockets.h @@ -475,6 +475,10 @@ enum lws_callback_reasons { LWS_CALLBACK_RECEIVE_CLIENT_HTTP = 46, 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, /****** add new things just above ---^ ******/ @@ -1337,6 +1341,13 @@ struct lws_protocols { * This is part of the ABI, don't needlessly break compatibility */ }; +struct lws_process_html_args { + char *p; + int len; + int max_len; + int final; +}; + enum lws_ext_options_types { EXTARG_NONE, EXTARG_DEC, @@ -1445,12 +1456,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; @@ -1775,6 +1789,68 @@ lws_add_http_header_status(struct lws *wsi, unsigned int code, unsigned char **p, unsigned char *end); +LWS_VISIBLE LWS_EXTERN const char * +lws_urlencode(char *escaped, const char *string, int len); + +LWS_VISIBLE LWS_EXTERN const char * +lws_sql_purify(char *escaped, const char *string, int len); + + + +/* + * URLDECODE 1 / 2 + * + * This simple urldecode only operates until the first '\0' and requires the + * data to exist all at once + */ + +LWS_VISIBLE LWS_EXTERN int +lws_urldecode(char *string, const char *escaped, int len); + +#ifdef LWS_WITH_STATEFUL_URLDECODE + +/* + * URLDECODE 2 / 2 + * + * These apis let you manage a form data parser that + * + * Since it's stateful, and the decoded area is malloc'd, this is robust + * enough to handle the form data coming in multiple POST_BODY packets without + * having to get into any special code. + */ + +typedef int (*lws_urldecode_stateful_cb)(void *data, + const char *name, char **buf, int len, int final); + +struct lws_urldecode_stateful_param_array; + +LWS_VISIBLE LWS_EXTERN struct lws_urldecode_stateful_param_array * +lws_urldecode_spa_create(const char * const *param_names, int count_params, + int max_storage, lws_urldecode_stateful_cb opt_cb, + void *opt_data); + +LWS_VISIBLE LWS_EXTERN int +lws_urldecode_spa_process(struct lws_urldecode_stateful_param_array *ludspa, + const char *in, int len); + +LWS_VISIBLE LWS_EXTERN int +lws_urldecode_spa_finalize(struct lws_urldecode_stateful_param_array *ludspa); + +LWS_VISIBLE LWS_EXTERN int +lws_urldecode_spa_get_length(struct lws_urldecode_stateful_param_array *ludspa, + int n); + +LWS_VISIBLE LWS_EXTERN const char * +lws_urldecode_spa_get_string(struct lws_urldecode_stateful_param_array *ludspa, + int n); + +LWS_VISIBLE LWS_EXTERN int +lws_urldecode_spa_destroy(struct lws_urldecode_stateful_param_array *ludspa); + +#endif + + + LWS_VISIBLE LWS_EXTERN int LWS_WARN_UNUSED_RESULT lws_http_redirect(struct lws *wsi, int code, const unsigned char *loc, int len, unsigned char **p, unsigned char *end); diff --git a/lib/output.c b/lib/output.c index f14a4a3f52..be0c73c59c 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 @@ LWS_VISIBLE int lws_serve_http_file_fragment(struct lws *wsi) 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 5ce641314f..12719fcbc7 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 ce885de291..18fd4dc9b7 100644 --- a/lib/server.c +++ b/lib/server.c @@ -206,6 +206,18 @@ lws_select_vhost(struct lws_context *context, int port, const char *servername) return NULL; } +static 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 +283,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 +356,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 +377,44 @@ 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_notice("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 +433,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 +462,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 +670,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 +685,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 +738,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 +751,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,44 +776,40 @@ lws_http_action(struct lws *wsi) * For the duration of this http transaction, bind us to the * associated protocol */ - if (hit->origin_protocol == LWSMPRO_CALLBACK) { - - for (n = 0; n < (unsigned int)wsi->vhost->count_protocols; n++) - if (!strcmp(wsi->vhost->protocols[n].name, - hit->origin)) { - if (wsi->protocol != &wsi->vhost->protocols[n]) - if (!wsi->user_space_externally_allocated) - lws_free_set_NULL(wsi->user_space); - wsi->protocol = &wsi->vhost->protocols[n]; - if (lws_ensure_user_space(wsi)) { - lwsl_err("Unable to allocate user space\n"); - - return 1; + if (hit->origin_protocol == LWSMPRO_CALLBACK || + (hit->protocol && lws_hdr_total_length(wsi, WSI_TOKEN_POST_URI))) { + if (! hit->protocol) { + for (n = 0; n < (unsigned int)wsi->vhost->count_protocols; n++) + if (!strcmp(wsi->vhost->protocols[n].name, + hit->origin)) { + + if (wsi->protocol != &wsi->vhost->protocols[n]) + if (!wsi->user_space_externally_allocated) + lws_free_set_NULL(wsi->user_space); + wsi->protocol = &wsi->vhost->protocols[n]; + if (lws_ensure_user_space(wsi)) { + lwsl_err("Unable to allocate user space\n"); + + return 1; + } + break; } - break; - } - if (n == wsi->vhost->count_protocols) { - n = -1; - lwsl_err("Unable to find plugin '%s'\n", - hit->origin); + if (n == wsi->vhost->count_protocols) { + n = -1; + lwsl_err("Unable to find plugin '%s'\n", + hit->origin); + } } - n = wsi->protocol->callback(wsi, LWS_CALLBACK_HTTP, - wsi->user_space, uri_ptr, uri_len); + wsi->user_space, + uri_ptr + hit->mountpoint_len, + uri_len - hit->mountpoint_len); goto after; } - /* deferred cleanup and reset to protocols[0] */ - - if (wsi->protocol != &wsi->vhost->protocols[0]) - if (!wsi->user_space_externally_allocated) - lws_free_set_NULL(wsi->user_space); - - wsi->protocol = &wsi->vhost->protocols[0]; - #ifdef LWS_WITH_CGI /* did we hit something with a cgi:// origin? */ if (hit->origin_protocol == LWSMPRO_CGI) { @@ -773,17 +859,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); @@ -1279,11 +1383,19 @@ lws_http_transaction_completed(struct lws *wsi) return 1; } + n = wsi->protocol->callback(wsi, LWS_CALLBACK_HTTP_DROP_PROTOCOL, + wsi->user_space, NULL, 0); + + if (!wsi->user_space_externally_allocated) + lws_free_set_NULL(wsi->user_space); + + wsi->protocol = &wsi->vhost->protocols[0]; + /* otherwise set ourselves up ready to go again */ wsi->state = LWSS_HTTP; wsi->mode = LWSCM_HTTP_SERVING; - /* reset of non [0] protocols (and freeing of user_space) is deferred */ 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; @@ -1814,8 +1926,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) { diff --git a/libwebsockets-api-doc.html b/libwebsockets-api-doc.html index 3500041f98..bd649732e3 100644 --- a/libwebsockets-api-doc.html +++ b/libwebsockets-api-doc.html @@ -412,6 +412,30 @@

Description

You will not need this unless you are doing something special
+

lws_get_urlarg_by_name - return pointer to arg value if present

+LWS_EXTERN const char * +lws_get_urlarg_by_name +(struct lws * wsi, +const char * name, +char * buf, +int len) +

Arguments

+
+
wsi +
the connection to check +
name +
the arg name, like "token=" +
buf +
the buffer to receive the urlarg (including the name= part) +
len +
the length of the buffer to receive the urlarg +
+

Description

+
+Returns NULL if not found or a pointer inside buf to just after the +name= part. +
+

lws_get_peer_simple - Get client address information without RDNS

const char * lws_get_peer_simple @@ -739,6 +763,162 @@

Description

and the leading / on the path is consequently lost
+

lws_sql_purify - like strncpy but with escaping for sql quotes

+LWS_EXTERN const char * +lws_sql_purify +(char * escaped, +const char * string, +int len) +

Arguments

+
+
escaped +
output buffer +
string +
input buffer ('/0' terminated) +
len +
output buffer max length +
+

Description

+
+Because escaping expands the output string, it's not +possible to do it in-place, ie, with escaped == string +
+
+

lws_urlencode - like strncpy but with urlencoding

+LWS_EXTERN const char * +lws_urlencode +(char * escaped, +const char * string, +int len) +

Arguments

+
+
escaped +
output buffer +
string +
input buffer ('/0' terminated) +
len +
output buffer max length +
+

Description

+
+Because urlencoding expands the output string, it's not +possible to do it in-place, ie, with escaped == string +
+
+

lws_urldecode - like strncpy but with urldecoding

+LWS_EXTERN int +lws_urldecode +(char * string, +const char * escaped, +int len) +

Arguments

+
+
string +
output buffer +
escaped +
input buffer ('\0' terminated) +
len +
output buffer max length +
+

Description

+
+This is only useful for '\0' terminated strings +

+Since urldecoding only shrinks the output string, it is possible to +do it in-place, ie, string == escaped +

+
+

lws_urldecode_spa_create - create urldecode parser

+LWS_EXTERN struct lws_urldecode_stateful_param_array * +lws_urldecode_spa_create +(const char *const * param_names, +int count_params, +int max_storage, +lws_urldecode_stateful_cb opt_cb, +void * opt_data) +

Arguments

+
+
param_names +
array of form parameter names, like "username" +
count_params +
count of param_names +
max_storage +
total amount of form parameter values we can store +
opt_cb +
NULL, or callback to filter data. Needed for file transfer case +
opt_data +
NULL, or user pointer provided to opt_cb. +
+

Description

+
+Creates a urldecode parser and initializes it. +

+opt_cb can be NULL if you just want normal name=value parsing, however +if one or more entries in your form are bulk data (file transfer), you +can provide this callback and filter on the name callback parameter to +treat that urldecoded data separately. The callback should return -1 +in case of fatal error, 1 if it handled the data itself and 0 if it +wants the data to be handled as name=value. +

+
+

lws_urldecode_spa_process - parses a chunk of input data

+LWS_EXTERN int +lws_urldecode_spa_process +(struct lws_urldecode_stateful_param_array * ludspa, +const char * in, +int len) +

Arguments

+
+
ludspa +
the parser object previously created +
in +
incoming, urlencoded data +
len +
count of bytes valid at in +
+
+

lws_urldecode_spa_get_length - return length of parameter value

+LWS_EXTERN int +lws_urldecode_spa_get_length +(struct lws_urldecode_stateful_param_array * ludspa, +int n) +

Arguments

+
+
ludspa +
the parser object previously created +
n +
parameter ordinal to return length of value for +
+
+

lws_urldecode_spa_get_string - return pointer to parameter value

+LWS_EXTERN const char * +lws_urldecode_spa_get_string +(struct lws_urldecode_stateful_param_array * ludspa, +int n) +

Arguments

+
+
ludspa +
the parser object previously created +
n +
parameter ordinal to return pointer to value for +
+
+

lws_urldecode_spa_finalize - indicate incoming data completed

+LWS_EXTERN int +lws_urldecode_spa_finalize +(struct lws_urldecode_stateful_param_array * spa) +

Arguments

+
+
+
+

lws_urldecode_spa_destroy - destroy parser object

+LWS_EXTERN int +lws_urldecode_spa_destroy +(struct lws_urldecode_stateful_param_array * spa) +

Arguments

+
+
+

lws_cgi - connected cgi process

LWS_EXTERN int lws_cgi @@ -1231,6 +1411,55 @@

Description

nothing is pending, or as soon as it services whatever was pending.
+

lws_email_init - Initialize a struct lws_email

+LWS_EXTERN int +lws_email_init +(struct lws_email * email, +uv_loop_t * loop, +int max_content) +

Arguments

+
+
email +
struct lws_email to init +
loop +
libuv loop to use +
max_content +
max email content size +
+

Description

+
+Prepares a struct lws_email for use ending SMTP +
+
+

lws_email_check - Request check for new email

+LWS_EXTERN void +lws_email_check +(struct lws_email * email) +

Arguments

+
+
email +
struct lws_email context to check +
+

Description

+
+Schedules a check for new emails in 1s... call this when you have queued an +email for send. +
+
+

lws_email_destroy - stop using the struct lws_email

+LWS_EXTERN void +lws_email_destroy +(struct lws_email * email) +

Arguments

+
+
email +
the struct lws_email context +
+

Description

+
+Stop sending email using email and free allocations +
+

struct lws_plat_file_ops - Platform-specific file operations

struct lws_plat_file_ops {
    lws_filefd_type (*open) (struct lws *wsi, const char *filename,unsigned long *filelen, int flags);
@@ -1860,6 +2089,7 @@

struct lws_context_creation_info - parameters to create context with

    const struct lws_http_mount * mounts;
    const char * server_string;
    unsigned int pt_serv_buf_size;
+    unsigned int max_http_header_data2;
};

Members

@@ -1987,6 +2217,11 @@

Members

defines the max chunk of file that can be sent at once. At the risk of lws having to buffer failed large sends, it can be increased to, eg, 128KiB to improve throughput. +
max_http_header_data2 +
if max_http_header_data is 0 and this +is nonzero, this will be used in place of the default. It's +like this for compatibility with the original short version, +this is unsigned int length.

Description

diff --git a/lws_config.h.in b/lws_config.h.in index 21f1b536c7..9e61d6f137 100644 --- a/lws_config.h.in +++ b/lws_config.h.in @@ -96,6 +96,8 @@ #cmakedefine LWS_WITH_ACCESS_LOG #cmakedefine LWS_WITH_SERVER_STATUS +#cmakedefine LWS_WITH_STATEFUL_URLDECODE + /* Maximum supported service threads */ #define LWS_MAX_SMP ${LWS_MAX_SMP} diff --git a/plugins/admin-login.html b/plugins/admin-login.html new file mode 100644 index 0000000000..113df9cd38 --- /dev/null +++ b/plugins/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/failed-login.html b/plugins/failed-login.html new file mode 100644 index 0000000000..9ab065b53c --- /dev/null +++ b/plugins/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-login-example.html b/plugins/generic-sessions-login-example.html new file mode 100644 index 0000000000..a84f179e0b --- /dev/null +++ b/plugins/generic-sessions-login-example.html @@ -0,0 +1,176 @@ + + + + + + + + + + + + + + + + diff --git a/plugins/generic-sessions-register-example.html b/plugins/generic-sessions-register-example.html new file mode 100644 index 0000000000..6125548ce6 --- /dev/null +++ b/plugins/generic-sessions-register-example.html @@ -0,0 +1,181 @@ + + + + + + + + + + + + + + diff --git a/plugins/index.html b/plugins/index.html new file mode 100644 index 0000000000..5c27ba4c0d --- /dev/null +++ b/plugins/index.html @@ -0,0 +1,192 @@ + + + + + + + + + + + + +
+ + + Example website with top
session management ribbon +
+ + + + + + + + + + + +
+ + + + + + + diff --git a/plugins/lwsgs.js b/plugins/lwsgs.js new file mode 100644 index 0000000000..f814537a16 --- /dev/null +++ b/plugins/lwsgs.js @@ -0,0 +1,294 @@ + + +var lwsgs_user = "$lwsgs_user"; +var lwsgs_auth = "$lwsgs_auth"; +var lwsgs_email = "$lwsgs_email"; + +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; + + document.getElementById('change').disabled = !en_change; + document.getElementById('cpassword').disabled = pwok === 0; + document.getElementById('cpassword2').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", "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", "check?email="+document.getElementById(id).value, true); + xmlHttp.send(null); +} + +function lwsgs_initial() +{ + //if (lwsgs_email) + //document.getElementById('cemail').placeholder = lwsgs_email; + document.getElementById('cusername').value = lwsgs_user; + lwsgs_update(); + lwsgs_cupdate(); +} diff --git a/plugins/post-forgot-fail.html b/plugins/post-forgot-fail.html new file mode 100644 index 0000000000..ead3d13ecb --- /dev/null +++ b/plugins/post-forgot-fail.html @@ -0,0 +1,5 @@ + +Sorry, something went wrong. + +Click here to continue. + diff --git a/plugins/post-forgot-ok.html b/plugins/post-forgot-ok.html new file mode 100644 index 0000000000..3e8e9cf599 --- /dev/null +++ b/plugins/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/post-register-fail.html b/plugins/post-register-fail.html new file mode 100644 index 0000000000..063c3c50fa --- /dev/null +++ b/plugins/post-register-fail.html @@ -0,0 +1 @@ +Registration failed, sorry diff --git a/plugins/post-register-ok.html b/plugins/post-register-ok.html new file mode 100644 index 0000000000..f38e299f13 --- /dev/null +++ b/plugins/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/post-verify-fail.html b/plugins/post-verify-fail.html new file mode 100644 index 0000000000..d1d89ca56b --- /dev/null +++ b/plugins/post-verify-fail.html @@ -0,0 +1,20 @@ + + + + + + + + + + + + +
+ +
+ Sorry, the link was invalid. +
+ + + diff --git a/plugins/post-verify-ok.html b/plugins/post-verify-ok.html new file mode 100644 index 0000000000..eeb48df9ab --- /dev/null +++ b/plugins/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/protocol_generic_sessions.c b/plugins/protocol_generic_sessions.c new file mode 100644 index 0000000000..3a6a790a96 --- /dev/null +++ b/plugins/protocol_generic_sessions.c @@ -0,0 +1,1992 @@ +/* + * 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_EMAIL_CONTENT_SIZE 16384 +#define LWSGS_VERIFIED_ACCEPTED 100 + +/* 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, +}; + +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__generic_sessions { + 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; +}; + +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" +}; + +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, +}; + +struct per_session_data__generic_sessions { + struct lws_urldecode_stateful_param_array *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 *start; + char swallow[16]; + char ip[46]; + int pos; + int spos; + + unsigned int logging_out:1; +}; + +static 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'; +} + +static unsigned int +lwsgs_now_secs(void) +{ + struct timeval tv; + + gettimeofday(&tv, NULL); + + return tv.tv_sec; +} + +static int +lwsgw_check_admin(struct per_vhost_data__generic_sessions *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 + */ +static void +lwsgw_cookie_from_session(lwsgw_hash *sid, time_t expires, + char **p, char *end) +{ + struct tm *tm = gmtime(&expires); + time_t n = lwsgs_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"); +} + +static int +lwsgw_expire_old_sessions(struct per_vhost_data__generic_sessions *vhd) +{ + time_t n = lwsgs_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; +} + +static int +lwsgw_update_session(struct per_vhost_data__generic_sessions *vhd, + lwsgw_hash *hash, const char *user) +{ + time_t n = lwsgs_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; +} + +static 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; +} + +static int +lwsgs_lookup_session(struct per_vhost_data__generic_sessions *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; +} + +static 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; +} + +static int +lwsgs_lookup_user(struct per_vhost_data__generic_sessions *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]; +} + +static int +lwsgs_new_session_id(struct per_vhost_data__generic_sessions *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; +} + +static int +lwsgs_get_auth_level(struct per_vhost_data__generic_sessions *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 > lwsgs_now_secs() - 300) + n |= LWSGS_AUTH_FORGOT_FLOW; + } + + return n; +} + +struct lwsgs_fill_args { + char *buf; + int len; +}; + +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__generic_sessions *vhd = + (struct per_vhost_data__generic_sessions *)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__generic_sessions *vhd = + (struct per_vhost_data__generic_sessions *)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__generic_sessions *vhd = lws_container_of(email, + struct per_vhost_data__generic_sessions, email); + char s[LWSGS_EMAIL_CONTENT_SIZE]; + time_t now = lwsgs_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;", + vhd->u.username); + + 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; +} + + +static int +lwsgs_check_credentials(struct per_vhost_data__generic_sessions *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 */ + +static int +lwsgs_hash_password(struct per_vhost_data__generic_sessions *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; +} + +static int +callback_generic_sessions(struct lws *wsi, enum lws_callback_reasons reason, + void *user, void *in, size_t len) +{ + struct per_session_data__generic_sessions *pss = + (struct per_session_data__generic_sessions *)user; + const struct lws_protocol_vhost_options *pvo; + struct per_vhost_data__generic_sessions *vhd = + (struct per_vhost_data__generic_sessions *) + lws_protocol_vh_priv_get(lws_get_vhost(wsi), + lws_get_protocol(wsi)); + + const char *cp; + unsigned char buffer[LWS_PRE + LWSGS_EMAIL_CONTENT_SIZE]; + char cookie[1024], username[32], *pc = cookie, *sp; + char esc[50], esc1[50], esc2[50], esc3[50], esc4[50]; + struct lws_process_html_args *args; + unsigned char *p, *start, *end; + sqlite3_stmt *sm; + lwsgw_hash sid; + int n, old_len; + struct lwsgs_user u; + char s[LWSGS_EMAIL_CONTENT_SIZE]; + + switch (reason) { + case LWS_CALLBACK_PROTOCOL_INIT: /* per vhost */ + vhd = lws_protocol_vh_priv_zalloc(lws_get_vhost(wsi), + lws_get_protocol(wsi), + sizeof(struct per_vhost_data__generic_sessions)); + 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_notice("LWS_CALLBACK_HTTP: %s\n", in); + + pss->login_session.id[0] = '\0'; + pss->pos = 0; + strncpy(pss->onward, (char *)in, sizeof(pss->onward)); + + if (!strcmp((const char *)in, "/forgot")) { + 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)lwsgs_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 = lwsgs_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); + + goto redirect_with_cookie; + +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); + + goto redirect_with_cookie; + } + + if (!strcmp((const char *)in, "/confirm")) { + + if (lws_hdr_copy_fragment(wsi, cookie, sizeof(cookie), + WSI_TOKEN_HTTP_URI_ARGS, 0) < 0) { + lwsl_notice("copy failed\n"); + goto verf_fail; + } + + if (strncmp(cookie, "token=", 6)) { + lwsl_notice("not token=\n"); + goto verf_fail; + } + + u.username[0] = '\0'; + snprintf(s, sizeof(s) - 1, + "select username,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; + } + + snprintf(pss->onward, sizeof(pss->onward), + "%s/post-verify-ok.html", + vhd->email_confirm_url); + + pss->login_expires = lwsgs_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); + + goto redirect_with_cookie; + +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); + + goto redirect_with_cookie; + } + if (!strcmp((const char *)in, "/check")) { + /* + * either /check?email=xxx@yyy + * + * or, /check?username=xxx + * + * returns '0' if not already registered, else '1' + */ + + static const char * const colname[] = { + "username", "email" + }; + + u.username[0] = '\0'; + if (lws_hdr_copy_fragment(wsi, cookie, sizeof(cookie), + WSI_TOKEN_HTTP_URI_ARGS, 0) < 0) + goto nope; + + n = !strncmp(cookie, "email=", 6); + pc = strchr(cookie, '='); + if (!pc) { + lwsl_notice("cookie has no =\n"); + goto nope; + } + pc++; + + 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 nope; + } +nope: + 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; + goto try_to_reuse; + } + + if (!strcmp((const char *)in, "/login")) + break; + if (!strcmp((const char *)in, "/logout")) + break; + if (!strcmp((const char *)in, "/forgot")) + break; + if (!strcmp((const char *)in, "/change")) + break; + + lwsl_err("http doing 404 on %s\n", in); + lws_return_http_status(wsi, HTTP_STATUS_NOT_FOUND, NULL); + break; + + 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_PROCESS_HTML: + /* + * replace placeholders with session data and prepare the + * preamble to send chunked, p is already at +10 from the + * real buffer start to allow us to add the chunk header + * + * struct lws_process_html_args { + * char *p; + * int len; + * int max_len; + * int final; + * }; + */ + + args = (struct lws_process_html_args *)in; + + username[0] = '\0'; + u.email[0] = '\0'; + if (!lwsgs_get_sid_from_wsi(wsi, &sid)) { + if (lwsgs_lookup_session(vhd, &sid, username, + sizeof(username))) { + lwsl_notice("sid lookup for %s failed\n", sid.id); + pss->delete_session = sid; + return 1; + } + snprintf(s, sizeof(s) - 1, + "select username,email " + "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 token: %s\n", + sqlite3_errmsg(vhd->pdb)); + pss->delete_session = sid; + return 1; + } + } else + lwsl_notice("no sid\n"); + + /* do replacements */ + sp = args->p; + old_len = args->len; + args->len = 0; + pss->start = sp; + while (sp < args->p + old_len) { + + if (args->len + 7 >= args->max_len) { + lwsl_err("Used up interpret padding\n"); + return -1; + } + + if ((!pss->pos && *sp == '$') || + pss->pos) { + static const char * const vars[] = { + "$lwsgs_user", + "$lwsgs_auth", + "$lwsgs_email" + }; + int hits = 0, hit; + + if (!pss->pos) + pss->start = sp; + pss->swallow[pss->pos++] = *sp; + if (pss->pos == sizeof(pss->swallow)) + goto skip; + for (n = 0; n < ARRAY_SIZE(vars); n++) + if (!strncmp(pss->swallow, vars[n], pss->pos)) { + hits++; + hit = n; + } + if (!hits) { +skip: + pss->swallow[pss->pos] = '\0'; + memcpy(pss->start, pss->swallow, pss->pos); + args->len++; + pss->pos = 0; + sp = pss->start + 1; + continue; + } + if (hits == 1 && pss->pos == strlen(vars[hit])) { + switch (hit) { + case 0: + pc = username; + break; + case 1: + pc = cookie; + n = lwsgs_get_auth_level(vhd, username); + sprintf(cookie, "%d", n); + break; + case 2: + pc = u.email; + break; + } + + n = strlen(pc); + pss->swallow[pss->pos] = '\0'; + if (n != pss->pos) { + memmove(pss->start + n, + pss->start + pss->pos, + old_len - + (sp - args->p)); + old_len += (n - pss->pos) + 1; + } + memcpy(pss->start, pc, n); + args->len++; + sp = pss->start + 1; + + pss->pos = 0; + } + sp++; + continue; + } + + args->len++; + sp++; + } + + /* no space left for final chunk trailer */ + if (args->final && args->len + 7 >= args->max_len) + return -1; + + n = sprintf((char *)buffer, "%X\x0d\x0a", args->len); + + args->p -= n; + memcpy(args->p, buffer, n); + args->len += n; + + if (args->final) { + sp = args->p + args->len; + *sp++ = '\x0d'; + *sp++ = '\x0a'; + *sp++ = '0'; + *sp++ = '\x0d'; + *sp++ = '\x0a'; + *sp++ = '\x0d'; + *sp++ = '\x0a'; + args->len += 7; + } else { + sp = args->p + args->len; + *sp++ = '\x0d'; + *sp++ = '\x0a'; + args->len += 2; + } + break; + + case LWS_CALLBACK_HTTP_BODY: + lwsl_notice("LWS_CALLBACK_HTTP_BODY: %s %d\n", pss->onward, len); + + if (len < 2) + break; + + if (!pss->spa) { + pss->spa = lws_urldecode_spa_create(param_names, + ARRAY_SIZE(param_names), 1024, + NULL, NULL); + if (!pss->spa) + return -1; + } + + if (lws_urldecode_spa_process(pss->spa, in, len)) { + lwsl_notice("spa process blew\n"); + return -1; + } + + break; + + case LWS_CALLBACK_HTTP_WRITEABLE: + break; + + case LWS_CALLBACK_HTTP_BODY_COMPLETION: + + if (!pss->spa) + break; + + lwsl_notice("LWS_CALLBACK_HTTP_BODY_COMPLETION: %s\n", pss->onward); + + lws_urldecode_spa_finalize(pss->spa); + + /* + * change password + */ + + if (!strcmp((char *)pss->onward, "/change")) { + 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)) + goto change_fail; + + /* did a forgot pw ? */ + if (u.last_forgot_validated > + lwsgs_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_urldecode_spa_get_string(pss->spa, FGS_USERNAME), + lws_urldecode_spa_get_string(pss->spa, FGS_CURPW))) { + lwsl_notice("credentials bad\n"); + goto change_fail; + } + + strcpy(u.username, lws_urldecode_spa_get_string(pss->spa, FGS_USERNAME)); + } + + if (lwsgs_hash_password(vhd, lws_urldecode_spa_get_string(pss->spa, FGS_PASSWORD), + &u)) { + lwsl_err("Password hash failed\n"); + goto change_fail; + } + + 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)); + if (sqlite3_exec(vhd->pdb, s, NULL, NULL, NULL) != + SQLITE_OK) { + lwsl_err("Unable to update pw hash: %s\n", + sqlite3_errmsg(vhd->pdb)); + + goto change_fail; + } + + cp = lws_urldecode_spa_get_string(pss->spa, FGS_GOOD); + goto pass; +change_fail: + cp = lws_urldecode_spa_get_string(pss->spa, FGS_BAD); + lwsl_notice("user/password no good %s\n", + lws_urldecode_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, "/login")) { + lwsgw_hash hash; + unsigned char sid_rand[20]; + + if (lws_urldecode_spa_get_string(pss->spa, FGS_FORGOT) && + lws_urldecode_spa_get_string(pss->spa, FGS_FORGOT)[0]) { + + lwsl_notice("FORGOT %s %s\n", + lws_urldecode_spa_get_string(pss->spa, FGS_USERNAME), + lws_urldecode_spa_get_string(pss->spa, FGS_EMAIL)); + + if (!lws_urldecode_spa_get_string(pss->spa, FGS_USERNAME) && + !lws_urldecode_spa_get_string(pss->spa, FGS_EMAIL)) { + lwsl_err("Form must provide either " + "username or email\n"); + return -1; + } + + if (!lws_urldecode_spa_get_string(pss->spa, FGS_FORGOT_GOOD) || + !lws_urldecode_spa_get_string(pss->spa, FGS_FORGOT_BAD) || + !lws_urldecode_spa_get_string(pss->spa, FGS_FORGOT_POST_GOOD) || + !lws_urldecode_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_urldecode_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_urldecode_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_urldecode_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)); + + n = FGS_FORGOT_BAD; + goto reg_done; + } + if (!u.username[0]) { + lwsl_err("No match found %s\n", s); + n = FGS_FORGOT_BAD; + goto reg_done; + } + + 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"); + n = FGS_BAD; + goto reg_done; + } + 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/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_urldecode_spa_get_string(pss->spa, FGS_FORGOT_POST_GOOD), + sizeof(esc1) - 1), + lws_urlencode(esc3, + lws_urldecode_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)); + + n = FGS_FORGOT_BAD; + goto reg_done; + } + + snprintf(s, sizeof(s) - 1, + "update users set token='%s',token_time='%ld' where username='%s';", + hash.id, (long)lwsgs_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)); + + 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_urldecode_spa_get_string(pss->spa, FGS_USERNAME) || + !lws_urldecode_spa_get_string(pss->spa, FGS_PASSWORD)) { + lwsl_notice("username '%s' or pw '%s' missing\n", + lws_urldecode_spa_get_string(pss->spa, FGS_USERNAME), + lws_urldecode_spa_get_string(pss->spa, FGS_PASSWORD)); + return -1; + } + + if (lws_urldecode_spa_get_string(pss->spa, FGS_REGISTER) && + lws_urldecode_spa_get_string(pss->spa, FGS_REGISTER)[0]) { + unsigned char sid_rand[20]; + + lwsl_notice("REGISTER %s %s %s\n", + lws_urldecode_spa_get_string(pss->spa, FGS_USERNAME), + lws_urldecode_spa_get_string(pss->spa, FGS_PASSWORD), + lws_urldecode_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_urldecode_spa_get_string(pss->spa, FGS_REG_GOOD) || + !lws_urldecode_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_urldecode_spa_get_string(pss->spa, FGS_USERNAME))) { + n = FGS_REG_BAD; + goto reg_done; + } + + if (!lwsgs_lookup_user(vhd, + lws_urldecode_spa_get_string(pss->spa, FGS_USERNAME), &u)) { + lwsl_notice("user %s already registered\n", + lws_urldecode_spa_get_string(pss->spa, FGS_USERNAME)); + n = FGS_REG_BAD; + goto reg_done; + } + + u.username[0] = '\0'; + snprintf(s, sizeof(s) - 1, + "select username, email " + "from users where email = '%s';", + lws_sql_purify(esc, lws_urldecode_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)); + n = FGS_REG_BAD; + goto reg_done; + } + + if (u.username[0]) { + lwsl_notice("email %s already in use\n", + lws_urldecode_spa_get_string(pss->spa, FGS_USERNAME)); + n = FGS_REG_BAD; + goto reg_done; + } + + if (lwsgs_hash_password(vhd, + lws_urldecode_spa_get_string(pss->spa, FGS_PASSWORD), + &u)) { + lwsl_err("Password hash failed\n"); + n = FGS_REG_BAD; + goto reg_done; + } + + 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_urldecode_spa_get_string(pss->spa, FGS_USERNAME), sizeof(esc) - 1), + (unsigned long)lwsgs_now_secs(), + lws_sql_purify(esc1, pss->ip, sizeof(esc1) - 1), + lws_sql_purify(esc2, lws_urldecode_spa_get_string(pss->spa, FGS_EMAIL), sizeof(esc2) - 1), + u.pwhash.id, u.pwsalt.id, hash.id); + + n = FGS_REG_GOOD; + if (sqlite3_exec(vhd->pdb, (char *)buffer, NULL, + NULL, NULL) != SQLITE_OK) { + lwsl_err("Unable to insert user: %s\n", + sqlite3_errmsg(vhd->pdb)); + + n = FGS_REG_BAD; + goto reg_done; + } + + 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/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_urldecode_spa_get_string(pss->spa, FGS_USERNAME), sizeof(esc1) - 1), + lws_sql_purify(esc2, lws_urldecode_spa_get_string(pss->spa, FGS_EMAIL), sizeof(esc2) - 1), + lws_sql_purify(esc3, lws_urldecode_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_urldecode_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)); + + n = FGS_REG_BAD; + goto reg_done; + } + + /* get the email monitor to take a look */ + lws_email_check(&vhd->email); + +reg_done: + strncpy(pss->onward, lws_urldecode_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_urldecode_spa_get_string(pss->spa, FGS_USERNAME), + lws_urldecode_spa_get_string(pss->spa, FGS_PASSWORD))) { + if (lws_urldecode_spa_get_string(pss->spa, FGS_ADMIN)) + cp = lws_urldecode_spa_get_string(pss->spa, FGS_ADMIN); + else + if (lws_urldecode_spa_get_string(pss->spa, FGS_GOOD)) + cp = lws_urldecode_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_urldecode_spa_get_string(pss->spa, FGS_USERNAME), + lws_urldecode_spa_get_string(pss->spa, FGS_PASSWORD))) { + lwsl_info("pw hash check met\n"); + cp = lws_urldecode_spa_get_string(pss->spa, FGS_GOOD); + goto pass; + } else + lwsl_notice("user/password no good %s\n", + lws_urldecode_spa_get_string(pss->spa, FGS_USERNAME)); + + if (!lws_urldecode_spa_get_string(pss->spa, FGS_BAD)) { + lwsl_info("No admin or good target url in form\n"); + return -1; + } + + strncpy(pss->onward, lws_urldecode_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, "/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_urldecode_spa_get_string(pss->spa, FGS_GOOD)) { + lwsl_info("No admin or good target url in form\n"); + return -1; + } + + strncpy(pss->onward, lws_urldecode_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 = lwsgs_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_urldecode_spa_get_string(pss->spa, FGS_USERNAME), + pss->login_expires)) + goto try_to_reuse; + + lwsl_notice("Creating new session: %s\n", + pss->login_session.id); + } else { + /* + * we can just update the existing session to be + * authorized + */ + lwsl_notice("Authorizing current session %s", sid.id); + lwsgw_update_session(vhd, &sid, lws_urldecode_spa_get_string(pss->spa, FGS_USERNAME)); + pss->login_session = sid; + } + +completion_flow: + + lwsl_notice("LWS_CALLBACK_HTTP_BODY_COMPLETION: onward=%s\n", pss->onward); + + lwsgw_expire_old_sessions(vhd); + +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 = lwsgs_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 = lwsgs_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_notice("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; + goto try_to_reuse; + + case LWS_CALLBACK_HTTP_DROP_PROTOCOL: + if (pss->spa) { + lws_urldecode_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_notice("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 = lwsgs_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_notice("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; + +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__generic_sessions), + 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/protocol_post_demo.c b/plugins/protocol_post_demo.c index e2e0b7326e..57b531e8cf 100644 --- a/plugins/protocol_post_demo.c +++ b/plugins/protocol_post_demo.c @@ -25,11 +25,19 @@ #include struct per_session_data__post_demo { - char post_string[256]; + struct lws_urldecode_stateful_param_array *spa; char result[500 + LWS_PRE]; int result_len; }; +static const char * const param_names[] = { + "Text", +}; + +enum enum_param_names { + EPN_TEXT, +}; + static int callback_post_demo(struct lws *wsi, enum lws_callback_reasons reason, void *user, void *in, size_t len) @@ -43,16 +51,23 @@ callback_post_demo(struct lws *wsi, enum lws_callback_reasons reason, switch (reason) { case LWS_CALLBACK_HTTP_BODY: - lwsl_debug("LWS_CALLBACK_HTTP_BODY: len %d\n", (int)len); - strncpy(pss->post_string, in, sizeof (pss->post_string) -1); - pss->post_string[sizeof(pss->post_string) - 1] = '\0'; - - if (len < sizeof(pss->post_string) - 1) - pss->post_string[len] = '\0'; + /* create the POST argument parser if not already existing */ + if (!pss->spa) { + pss->spa = lws_urldecode_spa_create(param_names, + ARRAY_SIZE(param_names), 1024, + NULL, NULL); + if (!pss->spa) + return -1; + } + + /* let it parse the POST data */ + if (lws_urldecode_spa_process(pss->spa, in, len)) + return -1; break; case LWS_CALLBACK_HTTP_WRITEABLE: - lwsl_debug("LWS_CALLBACK_HTTP_WRITEABLE: sending %d\n", pss->result_len); + lwsl_debug("LWS_CALLBACK_HTTP_WRITEABLE: sending %d\n", + pss->result_len); n = lws_write(wsi, (unsigned char *)pss->result + LWS_PRE, pss->result_len, LWS_WRITE_HTTP); if (n < 0) @@ -66,9 +81,20 @@ callback_post_demo(struct lws *wsi, enum lws_callback_reasons reason, * respond to the client with a redirect to show the * results */ + + /* call to inform no more payload data coming */ + lws_urldecode_spa_finalize(pss->spa); + pss->result_len = sprintf((char *)pss->result + LWS_PRE, - "

Form results

'%s'
" - "", pss->post_string); + "

Form results

" + "Text (after urldecoding):'%s' (len %d)
" + "", + lws_urldecode_spa_get_string(pss->spa, EPN_TEXT), + lws_urldecode_spa_get_length(pss->spa, EPN_TEXT)); + + /* finished with the POST argument parser */ +// lws_urldecode_spa_destroy(pss->spa); +// pss->spa = NULL; p = buffer + LWS_PRE; start = p; @@ -96,6 +122,13 @@ callback_post_demo(struct lws *wsi, enum lws_callback_reasons reason, lws_callback_on_writable(wsi); break; + case LWS_CALLBACK_HTTP_DROP_PROTOCOL: + if (pss->spa) { + lws_urldecode_spa_destroy(pss->spa); + pss->spa = NULL; + } + break; + default: break; } diff --git a/plugins/sent-forgot-fail.html b/plugins/sent-forgot-fail.html new file mode 100644 index 0000000000..ead3d13ecb --- /dev/null +++ b/plugins/sent-forgot-fail.html @@ -0,0 +1,5 @@ + +Sorry, something went wrong. + +Click here to continue. + diff --git a/plugins/sent-forgot-ok.html b/plugins/sent-forgot-ok.html new file mode 100644 index 0000000000..83df7510af --- /dev/null +++ b/plugins/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/successful-login.html b/plugins/successful-login.html new file mode 100644 index 0000000000..dfc25cf74a --- /dev/null +++ b/plugins/successful-login.html @@ -0,0 +1,4 @@ + +This is an example destination that will appear after successful non-Admin login + + diff --git a/test-server/test-server-v2.0.c b/test-server/test-server-v2.0.c index b6feafe7d7..d12c0b6ba0 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,