diff --git a/READMEs/README.lwsws.md b/READMEs/README.lwsws.md index 8893c86b6..d6943b20b 100644 --- a/READMEs/README.lwsws.md +++ b/READMEs/README.lwsws.md @@ -418,7 +418,7 @@ Content-Type: header. 7) A mount can be protected by HTTP Basic Auth. This only makes sense when using https, since otherwise the password can be sniffed. -You can add a `basic-auth` entry on a mount like this +You can add a `basic-auth` entry on an http mount like this ``` { @@ -447,6 +447,12 @@ a mount. After successful authentication, `WSI_TOKEN_HTTP_AUTHORIZATION` contains the authenticated username. +In the case you want to also protect being able to connect to a ws protocol on +a particular vhost by requiring the http part can authenticate using Basic +Auth before the ws upgrade, this is also possible. In this case, the +"basic-auth": and filepath to the credentials file is passed as a pvo in the +"ws-protocols" section of the vhost definition. + @section lwswscc Requiring a Client Cert on a vhost You can make a vhost insist to get a client certificate from the peer before diff --git a/lib/core/context.c b/lib/core/context.c index 94b2d8633..baff84917 100644 --- a/lib/core/context.c +++ b/lib/core/context.c @@ -209,7 +209,7 @@ lws_protocol_vh_priv_get(struct lws_vhost *vhost, return vhost->protocol_vh_privs[n]; } -static const struct lws_protocol_vhost_options * +const struct lws_protocol_vhost_options * lws_vhost_protocol_options(struct lws_vhost *vh, const char *name) { const struct lws_protocol_vhost_options *pvo = vh->pvo; diff --git a/lib/core/private.h b/lib/core/private.h index c1a0a661b..aa5d3a216 100644 --- a/lib/core/private.h +++ b/lib/core/private.h @@ -1374,6 +1374,9 @@ int lws_bind_protocol(struct lws *wsi, const struct lws_protocols *p, const char *reason); +const struct lws_protocol_vhost_options * +lws_vhost_protocol_options(struct lws_vhost *vh, const char *name); + const struct lws_http_mount * lws_find_mount(struct lws *wsi, const char *uri_ptr, int uri_len); diff --git a/lib/roles/http/private.h b/lib/roles/http/private.h index ff8b0581c..ad5496aaf 100644 --- a/lib/roles/http/private.h +++ b/lib/roles/http/private.h @@ -267,6 +267,18 @@ enum lws_parse_urldecode_results { LPUR_EXCESSIVE, }; +enum lws_check_basic_auth_results { + LCBA_CONTINUE, + LCBA_FAILED_AUTH, + LCBA_END_TRANSACTION, +}; + +enum lws_check_basic_auth_results +lws_check_basic_auth(struct lws *wsi, const char *basic_auth_login_file); + +int +lws_unauthorised_basic_auth(struct lws *wsi); + int lws_read_h1(struct lws *wsi, unsigned char *buf, lws_filepos_t len); diff --git a/lib/roles/http/server/server.c b/lib/roles/http/server/server.c index 729315602..7dae16f8d 100644 --- a/lib/roles/http/server/server.c +++ b/lib/roles/http/server/server.c @@ -808,7 +808,7 @@ lws_find_string_in_file(const char *filename, const char *string, int stringlen) } #endif -static int +int lws_unauthorised_basic_auth(struct lws *wsi) { struct lws_context_per_thread *pt = &wsi->context->pt[(int)wsi->tsi]; @@ -912,6 +912,76 @@ lws_http_get_uri_and_method(struct lws *wsi, char **puri_ptr, int *puri_len) return -1; } +enum lws_check_basic_auth_results +lws_check_basic_auth(struct lws *wsi, const char *basic_auth_login_file) +{ + char b64[160], plain[(sizeof(b64) * 3) / 4], *pcolon; + int m, ml, fi; + + if (!basic_auth_login_file) + return LCBA_CONTINUE; + + /* Did he send auth? */ + ml = lws_hdr_total_length(wsi, WSI_TOKEN_HTTP_AUTHORIZATION); + if (!ml) + return LCBA_FAILED_AUTH; + + /* Disallow fragmentation monkey business */ + + fi = wsi->http.ah->frag_index[WSI_TOKEN_HTTP_AUTHORIZATION]; + if (wsi->http.ah->frags[fi].nfrag) { + lwsl_err("fragmented basic auth header not allowed\n"); + return LCBA_FAILED_AUTH; + } + + m = lws_hdr_copy(wsi, b64, sizeof(b64), + WSI_TOKEN_HTTP_AUTHORIZATION); + if (m < 7) { + lwsl_err("b64 auth too long\n"); + return LCBA_END_TRANSACTION; + } + + b64[5] = '\0'; + if (strcasecmp(b64, "Basic")) { + lwsl_err("auth missing basic: %s\n", b64); + return LCBA_END_TRANSACTION; + } + + /* It'll be like Authorization: Basic QWxhZGRpbjpPcGVuU2VzYW1l */ + + m = lws_b64_decode_string(b64 + 6, plain, sizeof(plain) - 1); + if (m < 0) { + lwsl_err("plain auth too long\n"); + return LCBA_END_TRANSACTION; + } + + plain[m] = '\0'; + pcolon = strchr(plain, ':'); + if (!pcolon) { + lwsl_err("basic auth format broken\n"); + return LCBA_END_TRANSACTION; + } + if (!lws_find_string_in_file(basic_auth_login_file, plain, m)) { + lwsl_err("basic auth lookup failed\n"); + return LCBA_FAILED_AUTH; + } + + /* + * Rewrite WSI_TOKEN_HTTP_AUTHORIZATION so it is just the + * authorized username + */ + + *pcolon = '\0'; + wsi->http.ah->frags[fi].len = lws_ptr_diff(pcolon, plain); + pcolon = lws_hdr_simple_ptr(wsi, WSI_TOKEN_HTTP_AUTHORIZATION); + strncpy(pcolon, plain, ml - 1); + pcolon[ml - 1] = '\0'; + lwsl_info("%s: basic auth accepted for %s\n", __func__, + lws_hdr_simple_ptr(wsi, WSI_TOKEN_HTTP_AUTHORIZATION)); + + return LCBA_CONTINUE; +} + static const char * const oprot[] = { "http://", "https://" }; @@ -1144,70 +1214,14 @@ lws_http_action(struct lws *wsi) /* basic auth? */ - if (hit->basic_auth_login_file) { - char b64[160], plain[(sizeof(b64) * 3) / 4], *pcolon; - int m, ml, fi; - - /* Did he send auth? */ - ml = lws_hdr_total_length(wsi, WSI_TOKEN_HTTP_AUTHORIZATION); - if (!ml) - return lws_unauthorised_basic_auth(wsi); - - /* Disallow fragmentation monkey business */ - - fi = wsi->http.ah->frag_index[WSI_TOKEN_HTTP_AUTHORIZATION]; - if (wsi->http.ah->frags[fi].nfrag) { - lwsl_err("fragmented basic auth header not allowed\n"); - return lws_unauthorised_basic_auth(wsi); - } - - n = HTTP_STATUS_FORBIDDEN; - - m = lws_hdr_copy(wsi, b64, sizeof(b64), - WSI_TOKEN_HTTP_AUTHORIZATION); - if (m < 7) { - lwsl_err("b64 auth too long\n"); - goto transaction_result_n; - } - - b64[5] = '\0'; - if (strcasecmp(b64, "Basic")) { - lwsl_err("auth missing basic: %s\n", b64); - goto transaction_result_n; - } - - /* It'll be like Authorization: Basic QWxhZGRpbjpPcGVuU2VzYW1l */ - - m = lws_b64_decode_string(b64 + 6, plain, sizeof(plain) - 1); - if (m < 0) { - lwsl_err("plain auth too long\n"); - goto transaction_result_n; - } - - plain[m] = '\0'; - pcolon = strchr(plain, ':'); - if (!pcolon) { - lwsl_err("basic auth format broken\n"); - return lws_unauthorised_basic_auth(wsi); - } - if (!lws_find_string_in_file(hit->basic_auth_login_file, - plain, m)) { - lwsl_err("basic auth lookup failed\n"); - return lws_unauthorised_basic_auth(wsi); - } - - /* - * Rewrite WSI_TOKEN_HTTP_AUTHORIZATION so it is just the - * authorized username - */ - - *pcolon = '\0'; - wsi->http.ah->frags[fi].len = lws_ptr_diff(pcolon, plain); - pcolon = lws_hdr_simple_ptr(wsi, WSI_TOKEN_HTTP_AUTHORIZATION); - strncpy(pcolon, plain, ml - 1); - pcolon[ml - 1] = '\0'; - lwsl_info("%s: basic auth accepted for %s\n", __func__, - lws_hdr_simple_ptr(wsi, WSI_TOKEN_HTTP_AUTHORIZATION)); + switch(lws_check_basic_auth(wsi, hit->basic_auth_login_file)) { + case LCBA_CONTINUE: + break; + case LCBA_FAILED_AUTH: + return lws_unauthorised_basic_auth(wsi); + case LCBA_END_TRANSACTION: + lws_return_http_status(wsi, HTTP_STATUS_FORBIDDEN, NULL); + return lws_http_transaction_completed(wsi); } #if defined(LWS_WITH_HTTP_PROXY) @@ -1557,11 +1571,6 @@ bail_nuke_ah: lws_header_table_detach(wsi, 1); return 1; - -transaction_result_n: - lws_return_http_status(wsi, n, NULL); - - return lws_http_transaction_completed(wsi); } int diff --git a/lib/roles/ws/server-ws.c b/lib/roles/ws/server-ws.c index bdc45367d..ba7db0833 100644 --- a/lib/roles/ws/server-ws.c +++ b/lib/roles/ws/server-ws.c @@ -252,7 +252,9 @@ int lws_process_ws_upgrade(struct lws *wsi) { struct lws_context_per_thread *pt = &wsi->context->pt[(int)wsi->tsi]; + const struct lws_protocol_vhost_options *pvos = NULL; const struct lws_protocols *pcol = NULL; + const char *ws_prot_basic_auth = NULL; char buf[128], name[64]; struct lws_tokenize ts; lws_tokenize_elem e; @@ -260,23 +262,6 @@ lws_process_ws_upgrade(struct lws *wsi) if (!wsi->protocol) lwsl_err("NULL protocol at lws_read\n"); - /* - * We are upgrading to ws, so http/1.1 + h2 and keepalive + pipelined - * header considerations about keeping the ah around no longer apply. - * - * However it's common for the first ws protocol data to have been - * coalesced with the browser upgrade request and to already be in the - * ah rx buffer. - */ - - lws_pt_lock(pt, __func__); - - if (!wsi->h2_stream_carries_ws) - lws_role_transition(wsi, LWSIFR_SERVER, LRS_ESTABLISHED, - &role_ops_ws); - - lws_pt_unlock(pt); - /* * It's either websocket or h2->websocket * @@ -389,6 +374,47 @@ check_protocol: alloc_ws: + /* + * Allow basic auth a look-in now we bound the wsi to the protocol. + * + * For vhost ws basic auth, it is "basic-auth": "path" as usual but + * applied to the protocol's entry in the vhost's "ws-protocols": + * section, as a pvo. + */ + + pvos = lws_vhost_protocol_options(wsi->vhost, wsi->protocol->name); + if (pvos && pvos->options && + !lws_pvo_get_str((void *)pvos->options, "basic-auth", + &ws_prot_basic_auth)) { + lwsl_notice("%s: ws upgrade requires basic auth\n", __func__); + switch(lws_check_basic_auth(wsi, ws_prot_basic_auth)) { + case LCBA_CONTINUE: + break; + case LCBA_FAILED_AUTH: + return lws_unauthorised_basic_auth(wsi); + case LCBA_END_TRANSACTION: + lws_return_http_status(wsi, HTTP_STATUS_FORBIDDEN, NULL); + return lws_http_transaction_completed(wsi); + } + } + + /* + * We are upgrading to ws, so http/1.1 + h2 and keepalive + pipelined + * header considerations about keeping the ah around no longer apply. + * + * However it's common for the first ws protocol data to have been + * coalesced with the browser upgrade request and to already be in the + * ah rx buffer. + */ + + lws_pt_lock(pt, __func__); + + if (!wsi->h2_stream_carries_ws) + lws_role_transition(wsi, LWSIFR_SERVER, LRS_ESTABLISHED, + &role_ops_ws); + + lws_pt_unlock(pt); + /* allocate the ws struct for the wsi */ wsi->ws = lws_zalloc(sizeof(*wsi->ws), "ws struct");