websocket: pause writing and meta data fix
No binary (build failed)
—
(log yok)
| @@ -297,6 +297,7 @@ static void deflate_do_close(struct Curl_easy *data, | |
| static const struct Curl_cwtype deflate_encoding = { | |
| "deflate", | |
| NULL, | |
| + | CURL_CW_FLAG_BLOWUP, |
| deflate_do_init, | |
| deflate_do_write, | |
| Curl_cwriter_def_flush, | |
| @@ -359,6 +360,7 @@ static void gzip_do_close(struct Curl_easy *data, | |
| static const struct Curl_cwtype gzip_encoding = { | |
| "gzip", | |
| "x-gzip", | |
| + | CURL_CW_FLAG_BLOWUP, |
| gzip_do_init, | |
| gzip_do_write, | |
| Curl_cwriter_def_flush, | |
| @@ -493,6 +495,7 @@ static void brotli_do_close(struct Curl_easy *data, | |
| static const struct Curl_cwtype brotli_encoding = { | |
| "br", | |
| NULL, | |
| + | CURL_CW_FLAG_BLOWUP, |
| brotli_do_init, | |
| brotli_do_write, | |
| Curl_cwriter_def_flush, | |
| @@ -607,6 +610,7 @@ static void zstd_do_close(struct Curl_easy *data, | |
| static const struct Curl_cwtype zstd_encoding = { | |
| "zstd", | |
| NULL, | |
| + | CURL_CW_FLAG_BLOWUP, |
| zstd_do_init, | |
| zstd_do_write, | |
| Curl_cwriter_def_flush, | |
| @@ -619,6 +623,7 @@ static const struct Curl_cwtype zstd_encoding = { | |
| static const struct Curl_cwtype identity_encoding = { | |
| "identity", | |
| "none", | |
| + | 0, |
| Curl_cwriter_def_init, | |
| Curl_cwriter_def_write, | |
| Curl_cwriter_def_flush, | |
| @@ -707,6 +712,7 @@ static void error_do_close(struct Curl_easy *data, | |
| static const struct Curl_cwtype error_writer = { | |
| "ce-error", | |
| NULL, | |
| + | 0, |
| error_do_init, | |
| error_do_write, | |
| Curl_cwriter_def_flush, | |
| @@ -190,9 +190,9 @@ static CURLcode cw_out_cb_write(struct Curl_easy *data, | |
| nwritten = wcb((char *)CURL_UNCONST(buf), 1, blen, wcb_data); | |
| CURL_CBAPI_END(&guard); | |
| } | |
| - | CURL_TRC_WRITE(data, "[OUT] wrote %zu %s bytes -> %zu", |
| + | CURL_TRC_WRITE(data, "[OUT] wrote %zu %s bytes, type=%x -> %zu", |
| blen, (otype == CW_OUT_HDS) ? "header" : "body", | |
| - | nwritten); |
| + | (unsigned int)otype, nwritten); |
| if(nwritten == CURL_WRITEFUNC_PAUSE) { | |
| if(data->conn->scheme->flags & PROTOPT_NONETWORK) { | |
| /* Protocols that work without network cannot be paused. This is | |
| @@ -429,8 +429,6 @@ static CURLcode cw_out_write(struct Curl_easy *data, | |
| CURLcode result; | |
| bool flush_all = !!(type & CLIENTWRITE_EOS); | |
| - | CURL_TRC_WRITE(data, "[OUT] write %zu bytes, type=%x", |
| - | blen, (unsigned int)type); |
| if((type & CLIENTWRITE_BODY) || | |
| ((type & CLIENTWRITE_HEADER) && data->set.include_header)) { | |
| cw_out_type otype = (!blen && (type & CLIENTWRITE_0LEN)) ? | |
| @@ -454,32 +452,34 @@ static CURLcode cw_out_do_flush(struct Curl_easy *data, | |
| bool flush_all) | |
| { | |
| struct cw_out_ctx *ctx = (struct cw_out_ctx *)cw_out; | |
| - | CURLcode result = CURLE_OK; |
| if(ctx->errored) | |
| return CURLE_WRITE_ERROR; | |
| - | if(data->req.writer.paused) |
| - | return CURLE_OK; /* not doing it */ |
| - | result = cw_out_flush_chain(ctx, data, &ctx->buf, flush_all); |
| - | if(result) { |
| - | ctx->errored = TRUE; |
| - | cw_out_bufs_free(ctx); |
| - | return result; |
| + | if(!data->req.writer.paused && ctx->buf) { |
| + | CURLcode result; |
| + | |
| + | CURL_TRC_WRITE(data, "[OUT] flush"); |
| + | result = cw_out_flush_chain(ctx, data, &ctx->buf, flush_all); |
| + | if(result) { |
| + | ctx->errored = TRUE; |
| + | cw_out_bufs_free(ctx); |
| + | return result; |
| + | } |
| } | |
| - | return result; |
| + | return CURLE_OK; |
| } | |
| static CURLcode cw_out_flush(struct Curl_easy *data, | |
| struct Curl_cwriter *writer) | |
| { | |
| - | CURL_TRC_WRITE(data, "[OUT] flush"); |
| return cw_out_do_flush(data, writer, FALSE); | |
| } | |
| const struct Curl_cwtype Curl_cwt_out = { | |
| "cw-out", | |
| NULL, | |
| + | 0, |
| cw_out_init, | |
| cw_out_write, | |
| cw_out_flush, | |
| @@ -207,6 +207,7 @@ static CURLcode cw_pause_write(struct Curl_easy *data, | |
| const struct Curl_cwtype Curl_cwt_pause = { | |
| "cw-pause", | |
| NULL, | |
| + | 0, |
| cw_pause_init, | |
| cw_pause_write, | |
| cw_pause_flush, | |
| @@ -441,6 +441,7 @@ static CURLcode ftp_cw_lc_write(struct Curl_easy *data, | |
| static const struct Curl_cwtype ftp_cw_lc = { | |
| "ftp-lineconv", | |
| NULL, | |
| + | 0, |
| Curl_cwriter_def_init, | |
| ftp_cw_lc_write, | |
| Curl_cwriter_def_flush, | |
| @@ -315,6 +315,7 @@ static CURLcode hds_cw_collect_write(struct Curl_easy *data, | |
| static const struct Curl_cwtype hds_cw_collect = { | |
| "hds-collect", | |
| NULL, | |
| + | 0, |
| Curl_cwriter_def_init, | |
| hds_cw_collect_write, | |
| Curl_cwriter_def_flush, | |
| @@ -465,6 +465,7 @@ static CURLcode cw_chunked_write(struct Curl_easy *data, | |
| const struct Curl_cwtype Curl_httpchunk_unencoder = { | |
| "chunked", | |
| NULL, | |
| + | 0, |
| cw_chunked_init, | |
| cw_chunked_write, | |
| Curl_cwriter_def_flush, | |
| @@ -293,6 +293,7 @@ static CURLcode cw_download_write(struct Curl_easy *data, | |
| static const struct Curl_cwtype cw_download = { | |
| "protocol", | |
| NULL, | |
| + | 0, |
| Curl_cwriter_def_init, | |
| cw_download_write, | |
| Curl_cwriter_def_flush, | |
| @@ -315,6 +316,7 @@ static CURLcode cw_raw_write(struct Curl_easy *data, | |
| static const struct Curl_cwtype cw_raw = { | |
| "raw", | |
| NULL, | |
| + | 0, |
| Curl_cwriter_def_init, | |
| cw_raw_write, | |
| Curl_cwriter_def_flush, | |
| @@ -484,9 +486,9 @@ CURLcode Curl_cwriter_add(struct Curl_easy *data, | |
| return result; | |
| } | |
| - | if(writer->phase == CURL_CW_CONTENT_DECODE) { |
| - | /* On adding a content decoder, add the pause writer. Do this |
| - | * BEFORE the given writer as any failure will make the |
| + | if(writer->cwt->flags & CURL_CW_FLAG_BLOWUP) { |
| + | /* On adding a writer that may blow up write sizes, e.g. zip bombs, |
| + | * add the pause writer. Do this first as any failure will make the |
| * caller destroy the writer again. */ | |
| result = cwriter_ensure_pause_writer(data); | |
| if(result) | |
| @@ -110,10 +110,14 @@ typedef enum { | |
| CURL_CW_CLIENT /* data written to client */ | |
| } Curl_cwriter_phase; | |
| + | /* writer may blow up size of write data, e.g. zip bombs */ |
| + | #define CURL_CW_FLAG_BLOWUP (1U << 0) |
| + | |
| /* Client Writer Type, provides the implementation */ | |
| struct Curl_cwtype { | |
| const char *name; /* writer name. */ | |
| const char *alias; /* writer name alias, maybe NULL. */ | |
| + | uint8_t flags; /* flags for writer behaviour */ |
| CURLcode (*do_init)(struct Curl_easy *data, | |
| struct Curl_cwriter *writer); | |
| CURLcode (*do_write)(struct Curl_easy *data, | |
| @@ -695,6 +695,7 @@ static CURLcode ws_cw_dec_next(const uint8_t *buf, size_t buflen, | |
| update_meta(ws, frame_age, frame_flags, payload_offset, | |
| payload_len, buflen); | |
| + | CURL_TRC_WRITE(data, "[WS] pass %zu decoded bytes", buflen); |
| result = Curl_cwriter_write(data, ctx->next_writer, | |
| (ctx->cw_type | CLIENTWRITE_0LEN), | |
| (const char *)buf, buflen); | |
| @@ -713,7 +714,7 @@ static CURLcode ws_cw_write(struct Curl_easy *data, | |
| struct websocket *ws; | |
| CURLcode result = CURLE_OK; | |
| - | CURL_TRC_WRITE(data, "ws_cw_write(len=%zu, type=%d)", nbytes, type); |
| + | CURL_TRC_WRITE(data, "[WS] write(len=%zu, type=%d)", nbytes, type); |
| if(!(type & CLIENTWRITE_BODY) || data->set.ws_raw_mode) | |
| return Curl_cwriter_write(data, writer->next, type, buf, nbytes); | |
| @@ -733,6 +734,10 @@ static CURLcode ws_cw_write(struct Curl_easy *data, | |
| } | |
| } | |
| + | result = Curl_cwriter_flush(data, writer->next); |
| + | if(result) |
| + | goto out; |
| + | |
| while(!Curl_bufq_is_empty(&ctx->buf) && !Curl_cwriter_is_paused(data)) { | |
| struct ws_cw_dec_ctx pass_ctx; | |
| pass_ctx.data = data; | |
| @@ -822,6 +827,7 @@ out: | |
| static const struct Curl_cwtype ws_cw_decode = { | |
| "ws-decode", | |
| NULL, | |
| + | 0, |
| ws_cw_init, | |
| ws_cw_write, | |
| ws_cw_flush, | |
| @@ -325,12 +325,22 @@ class TestWebsockets: | |
| rss2 = r.profile.stats['rss'] / (1024 * 1024) | |
| assert (rss1 * 1.1) >= rss2, 'bad memory increase' | |
| - | # test frame delivery when pausing |
| - | def test_20_12_pause_frames(self, env: Env, ws_4frames): |
| + | # test small frames delivery when pausing |
| + | def test_20_12_pause_frames_small(self, env: Env, ws_4frames): |
| payload = 127 * "x" | |
| client = LocalClient(env=env, name='cli_ws_pause') | |
| if not client.exists(): | |
| pytest.skip(f'example client not built: {client.name}') | |
| - | url = f'ws://localhost:{ws_4frames.port}/' |
| + | url = f'ws://localhost:{ws_4frames.port}/small' |
| + | r = client.run(args=[url, payload]) |
| + | r.check_exit_code(0) |
| + | |
| + | # test small frames delivery when pausing |
| + | def test_20_13_pause_frames_large(self, env: Env, ws_4frames): |
| + | payload = 127 * "x" |
| + | client = LocalClient(env=env, name='cli_ws_pause') |
| + | if not client.exists(): |
| + | pytest.skip(f'example client not built: {client.name}') |
| + | url = f'ws://localhost:{ws_4frames.port}/large' |
| r = client.run(args=[url, payload]) | |
| r.check_exit_code(0) | |
| @@ -29,21 +29,30 @@ import logging | |
| import websockets | |
| - | MESSAGES = [ |
| + | MESSAGES_SMALL = [ |
| "Hello 1", | |
| "Hello 2", | |
| "Hello 3", | |
| "Hello 4", | |
| ] | |
| + | MESSAGES_LARGE = [ |
| + | b"x" * 65536, |
| + | b"x" * 65536, |
| + | b"x" * 65536, |
| + | b"x" * 65536, |
| + | ] |
| + | |
| async def handler(websocket): | |
| peer = websocket.remote_address | |
| print(f"client from {peer[0]}:{peer[1]}", flush=True) | |
| print("handshake complete", flush=True) | |
| + | msgs = MESSAGES_LARGE if websocket.request.path == '/large' else MESSAGES_SMALL |
| + | |
| await asyncio.sleep(0.1) | |
| - | for index, payload in enumerate(MESSAGES, start=1): |
| + | for index, payload in enumerate(msgs, start=1): |
| await websocket.send(payload) | |
| print(f"sent frame {index}: {payload!r}", flush=True) | |
| # await asyncio.sleep(0.2) | |