From 3e23aff1d1b6e4e2f736edd7a8f5465b02c4c6db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Hoguin?= Date: Sat, 5 Oct 2019 17:32:50 +0200 Subject: Add Websocket option validate_utf8 This allows disabling the UTF-8 validation check for text and close frames. --- doc/src/manual/cowboy_websocket.asciidoc | 22 ++++++++++++++++++---- src/cowboy_websocket.erl | 11 ++++++++--- test/handlers/ws_dont_validate_utf8_h.erl | 23 +++++++++++++++++++++++ test/ws_SUITE.erl | 13 ++++++++++++- 4 files changed, 61 insertions(+), 8 deletions(-) create mode 100644 test/handlers/ws_dont_validate_utf8_h.erl diff --git a/doc/src/manual/cowboy_websocket.asciidoc b/doc/src/manual/cowboy_websocket.asciidoc index f8038e5..2f01b8a 100644 --- a/doc/src/manual/cowboy_websocket.asciidoc +++ b/doc/src/manual/cowboy_websocket.asciidoc @@ -151,11 +151,12 @@ Cowboy does it automatically for you. [source,erlang] ---- opts() :: #{ - compress => boolean(), - deflate_opts => cow_ws:deflate_opts() - idle_timeout => timeout(), + compress => boolean(), + deflate_opts => cow_ws:deflate_opts() + idle_timeout => timeout(), max_frame_size => non_neg_integer() | infinity, - req_filter => fun((cowboy_req:req()) -> map()) + req_filter => fun((cowboy_req:req()) -> map()), + validate_utf8 => boolean() } ---- @@ -209,8 +210,21 @@ given back in the `terminate/3` callback. By default it keeps the method, version, URI components and peer information. +validate_utf8 (true):: + +Whether Cowboy should verify that the payload of +`text` and `close` frames is valid UTF-8. This is +required by the protocol specification but in some +cases it may be more interesting to disable it in +order to save resources. ++ +Note that `binary` frames do not have this UTF-8 +requirement and are what should be used under +normal circumstances if necessary. + == Changelog +* *2.7*: The option `validate_utf8` has been added. * *2.6*: Deflate options can now be configured via `deflate_opts`. * *2.0*: The Req object is no longer passed to Websocket callbacks. * *2.0*: The callback `websocket_terminate/3` was removed in favor of `terminate/3`. diff --git a/src/cowboy_websocket.erl b/src/cowboy_websocket.erl index 5cc061a..ad0dad5 100644 --- a/src/cowboy_websocket.erl +++ b/src/cowboy_websocket.erl @@ -73,7 +73,8 @@ deflate_opts => cow_ws:deflate_opts(), idle_timeout => timeout(), max_frame_size => non_neg_integer() | infinity, - req_filter => fun((cowboy_req:req()) -> map()) + req_filter => fun((cowboy_req:req()) -> map()), + validate_utf8 => boolean() }. -export_type([opts/0]). @@ -91,7 +92,7 @@ hibernate = false :: boolean(), frag_state = undefined :: cow_ws:frag_state(), frag_buffer = <<>> :: binary(), - utf8_state = 0 :: cow_ws:utf8_state(), + utf8_state :: cow_ws:utf8_state(), deflate = true :: boolean(), extensions = #{} :: map(), req = #{} :: map() @@ -133,7 +134,11 @@ upgrade(Req0=#{version := Version}, Env, Handler, HandlerState, Opts) -> undefined -> maps:with([method, version, scheme, host, port, path, qs, peer], Req0); FilterFun -> FilterFun(Req0) end, - State0 = #state{opts=Opts, handler=Handler, req=FilteredReq}, + Utf8State = case maps:get(validate_utf8, Opts, true) of + true -> 0; + false -> undefined + end, + State0 = #state{opts=Opts, handler=Handler, utf8_state=Utf8State, req=FilteredReq}, try websocket_upgrade(State0, Req0) of {ok, State, Req} -> websocket_handshake(State, Req, HandlerState, Env); diff --git a/test/handlers/ws_dont_validate_utf8_h.erl b/test/handlers/ws_dont_validate_utf8_h.erl new file mode 100644 index 0000000..6599c78 --- /dev/null +++ b/test/handlers/ws_dont_validate_utf8_h.erl @@ -0,0 +1,23 @@ +%% This module disables UTF-8 validation. + +-module(ws_dont_validate_utf8_h). +-behavior(cowboy_websocket). + +-export([init/2]). +-export([websocket_handle/2]). +-export([websocket_info/2]). + +init(Req, State) -> + {cowboy_websocket, Req, State, #{ + validate_utf8 => false + }}. + +websocket_handle({text, Data}, State) -> + {reply, {text, Data}, State}; +websocket_handle({binary, Data}, State) -> + {reply, {binary, Data}, State}; +websocket_handle(_, State) -> + {ok, State}. + +websocket_info(_, State) -> + {ok, State}. diff --git a/test/ws_SUITE.erl b/test/ws_SUITE.erl index c99830c..64b8561 100644 --- a/test/ws_SUITE.erl +++ b/test/ws_SUITE.erl @@ -67,7 +67,8 @@ init_dispatch() -> {"/ws_timeout_hibernate", ws_timeout_hibernate, []}, {"/ws_timeout_cancel", ws_timeout_cancel, []}, {"/ws_max_frame_size", ws_max_frame_size, []}, - {"/ws_deflate_opts", ws_deflate_opts_h, []} + {"/ws_deflate_opts", ws_deflate_opts_h, []}, + {"/ws_dont_validate_utf8", ws_dont_validate_utf8_h, []} ]} ]). @@ -304,6 +305,16 @@ do_ws_deflate_opts_z(Path, Config) -> {error, closed} = gen_tcp:recv(Socket, 0, 6000), ok. +ws_dont_validate_utf8(Config) -> + doc("Handler is configured with UTF-8 validation disabled."), + {ok, Socket, _} = do_handshake("/ws_dont_validate_utf8", Config), + %% Send an invalid UTF-8 text frame and receive it back. + Mask = 16#37fa213d, + MaskedInvalid = do_mask(<<255, 255, 255, 255>>, Mask, <<>>), + ok = gen_tcp:send(Socket, <<1:1, 0:3, 1:4, 1:1, 4:7, Mask:32, MaskedInvalid/binary>>), + {ok, <<1:1, 0:3, 1:4, 0:1, 4:7, 255, 255, 255, 255>>} = gen_tcp:recv(Socket, 0, 6000), + ok. + ws_first_frame_with_handshake(Config) -> doc("Client sends the first frame immediately with the handshake. " "This is invalid according to the protocol but we still want " -- cgit v1.2.3