changeset 8689:6bd8ed493b85 quic

HTTP/3: refactored request body parser. The change reduces diff to the default branch for src/http/ngx_http_request_body.c. Also, client Content-Length, if present, is now checked against the real body size sent by client.
author Roman Arutyunyan <arut@nginx.com>
date Mon, 25 Jan 2021 16:16:47 +0300
parents a346905c359f
children a9034b10dacc
files src/http/ngx_http.h src/http/ngx_http_request_body.c src/http/v3/ngx_http_v3.h src/http/v3/ngx_http_v3_request.c
diffstat 4 files changed, 491 insertions(+), 89 deletions(-) [+]
line wrap: on
line diff
--- a/src/http/ngx_http.h
+++ b/src/http/ngx_http.h
@@ -66,9 +66,6 @@ struct ngx_http_chunked_s {
     ngx_uint_t           state;
     off_t                size;
     off_t                length;
-#if (NGX_HTTP_V3)
-    void                *h3_parse;
-#endif
 };
 
 
--- a/src/http/ngx_http_request_body.c
+++ b/src/http/ngx_http_request_body.c
@@ -87,6 +87,13 @@ ngx_http_read_client_request_body(ngx_ht
     }
 #endif
 
+#if (NGX_HTTP_V3)
+    if (r->http_version == NGX_HTTP_VERSION_30) {
+        rc = ngx_http_v3_read_request_body(r);
+        goto done;
+    }
+#endif
+
     preread = r->header_in->last - r->header_in->pos;
 
     if (preread) {
@@ -229,6 +236,18 @@ ngx_http_read_unbuffered_request_body(ng
     }
 #endif
 
+#if (NGX_HTTP_V3)
+    if (r->http_version == NGX_HTTP_VERSION_30) {
+        rc = ngx_http_v3_read_unbuffered_request_body(r);
+
+        if (rc == NGX_OK) {
+            r->reading_body = 0;
+        }
+
+        return rc;
+    }
+#endif
+
     if (r->connection->read->timedout) {
         r->connection->timedout = 1;
         return NGX_HTTP_REQUEST_TIME_OUT;
@@ -333,10 +352,11 @@ ngx_http_do_read_client_request_body(ngx
             }
 
             if (n == 0) {
-                rb->buf->last_buf = 1;
+                ngx_log_error(NGX_LOG_INFO, c->log, 0,
+                              "client prematurely closed connection");
             }
 
-            if (n == NGX_ERROR) {
+            if (n == 0 || n == NGX_ERROR) {
                 c->error = 1;
                 return NGX_HTTP_BAD_REQUEST;
             }
@@ -583,8 +603,8 @@ ngx_http_discard_request_body(ngx_http_r
     }
 #endif
 
-#if (NGX_HTTP_QUIC)
-    if (r->connection->quic) {
+#if (NGX_HTTP_V3)
+    if (r->http_version == NGX_HTTP_VERSION_30) {
         return NGX_OK;
     }
 #endif
@@ -956,15 +976,6 @@ ngx_http_request_body_length_filter(ngx_
             break;
         }
 
-        size = cl->buf->last - cl->buf->pos;
-
-        if (cl->buf->last_buf && (off_t) size < rb->rest) {
-            ngx_log_error(NGX_LOG_INFO, r->connection->log, 0,
-                          "client prematurely closed connection");
-            r->connection->error = 1;
-            return NGX_HTTP_BAD_REQUEST;
-        }
-
         tl = ngx_chain_get_free_buf(r->pool, &rb->free);
         if (tl == NULL) {
             return NGX_HTTP_INTERNAL_SERVER_ERROR;
@@ -982,6 +993,8 @@ ngx_http_request_body_length_filter(ngx_
         b->end = cl->buf->end;
         b->flush = r->request_body_no_buffering;
 
+        size = cl->buf->last - cl->buf->pos;
+
         if ((off_t) size < rb->rest) {
             cl->buf->pos = cl->buf->last;
             rb->rest -= size;
@@ -1053,16 +1066,7 @@ ngx_http_request_body_chunked_filter(ngx
                            cl->buf->file_pos,
                            cl->buf->file_last - cl->buf->file_pos);
 
-            switch (r->http_version) {
-#if (NGX_HTTP_V3)
-            case NGX_HTTP_VERSION_30:
-                rc = ngx_http_v3_parse_request_body(r, cl->buf, rb->chunked);
-                break;
-#endif
-
-            default: /* HTTP/1.x */
-                rc = ngx_http_parse_chunked(r, cl->buf, rb->chunked);
-            }
+            rc = ngx_http_parse_chunked(r, cl->buf, rb->chunked);
 
             if (rc == NGX_OK) {
 
@@ -1146,20 +1150,6 @@ ngx_http_request_body_chunked_filter(ngx
                 continue;
             }
 
-            if (rc == NGX_AGAIN && cl->buf->last_buf) {
-
-                /* last body buffer */
-
-                if (rb->chunked->length > 0) {
-                    ngx_log_error(NGX_LOG_INFO, r->connection->log, 0,
-                                  "client prematurely closed connection");
-                    r->connection->error = 1;
-                    return NGX_HTTP_BAD_REQUEST;
-                }
-
-                rc = NGX_DONE;
-            }
-
             if (rc == NGX_DONE) {
 
                 /* a whole response has been parsed successfully */
--- a/src/http/v3/ngx_http_v3.h
+++ b/src/http/v3/ngx_http_v3.h
@@ -133,8 +133,8 @@ typedef struct {
 
 
 void ngx_http_v3_init(ngx_connection_t *c);
-ngx_int_t ngx_http_v3_parse_request_body(ngx_http_request_t *r, ngx_buf_t *b,
-    ngx_http_chunked_t *ctx);
+ngx_int_t ngx_http_v3_read_request_body(ngx_http_request_t *r);
+ngx_int_t ngx_http_v3_read_unbuffered_request_body(ngx_http_request_t *r);
 
 uintptr_t ngx_http_v3_encode_varlen_int(u_char *p, uint64_t value);
 uintptr_t ngx_http_v3_encode_prefix_int(u_char *p, uint64_t value,
--- a/src/http/v3/ngx_http_v3_request.c
+++ b/src/http/v3/ngx_http_v3_request.c
@@ -19,6 +19,10 @@ static ngx_int_t ngx_http_v3_process_pse
     ngx_str_t *name, ngx_str_t *value);
 static ngx_int_t ngx_http_v3_init_pseudo_headers(ngx_http_request_t *r);
 static ngx_int_t ngx_http_v3_process_request_header(ngx_http_request_t *r);
+static void ngx_http_v3_read_client_request_body_handler(ngx_http_request_t *r);
+static ngx_int_t ngx_http_v3_do_read_client_request_body(ngx_http_request_t *r);
+static ngx_int_t ngx_http_v3_request_body_filter(ngx_http_request_t *r,
+    ngx_chain_t *in);
 
 
 static const struct {
@@ -625,12 +629,18 @@ failed:
 static ngx_int_t
 ngx_http_v3_process_request_header(ngx_http_request_t *r)
 {
+    ssize_t            n;
+    ngx_buf_t         *b;
+    ngx_connection_t  *c;
+
+    c = r->connection;
+
     if (ngx_http_v3_init_pseudo_headers(r) != NGX_OK) {
         return NGX_ERROR;
     }
 
     if (r->headers_in.server.len == 0) {
-        ngx_log_error(NGX_LOG_INFO, r->connection->log, 0,
+        ngx_log_error(NGX_LOG_INFO, c->log, 0,
                       "client sent neither \":authority\" nor \"Host\" header");
         goto failed;
     }
@@ -642,7 +652,7 @@ ngx_http_v3_process_request_header(ngx_h
                           r->headers_in.server.len)
                != 0)
         {
-            ngx_log_error(NGX_LOG_INFO, r->connection->log, 0,
+            ngx_log_error(NGX_LOG_INFO, c->log, 0,
                           "client sent \":authority\" and \"Host\" headers "
                           "with different values");
             goto failed;
@@ -655,10 +665,32 @@ ngx_http_v3_process_request_header(ngx_h
                                       r->headers_in.content_length->value.len);
 
         if (r->headers_in.content_length_n == NGX_ERROR) {
-            ngx_log_error(NGX_LOG_INFO, r->connection->log, 0,
+            ngx_log_error(NGX_LOG_INFO, c->log, 0,
                           "client sent invalid \"Content-Length\" header");
             goto failed;
         }
+
+    } else {
+        b = r->header_in;
+        n = b->last - b->pos;
+
+        if (n == 0) {
+            n = c->recv(c, b->start, b->end - b->start);
+
+            if (n == NGX_ERROR) {
+                ngx_http_close_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR);
+                return NGX_ERROR;
+            }
+
+            if (n > 0) {
+                b->pos = b->start;
+                b->last = b->start + n;
+            }
+        }
+
+        if (n != 0) {
+            r->headers_in.chunked = 1;
+        }
     }
 
     return NGX_OK;
@@ -671,74 +703,457 @@ failed:
 
 
 ngx_int_t
-ngx_http_v3_parse_request_body(ngx_http_request_t *r, ngx_buf_t *b,
-    ngx_http_chunked_t *ctx)
+ngx_http_v3_read_request_body(ngx_http_request_t *r)
 {
+    size_t                     preread;
     ngx_int_t                  rc;
-    ngx_connection_t          *c;
-    ngx_http_v3_parse_data_t  *st;
-    enum {
-        sw_start = 0,
-        sw_skip
-    };
+    ngx_chain_t               *cl, out;
+    ngx_http_request_body_t   *rb;
+    ngx_http_core_loc_conf_t  *clcf;
+
+    rb = r->request_body;
+
+    preread = r->header_in->last - r->header_in->pos;
+
+    if (preread) {
+
+        /* there is the pre-read part of the request body */
+
+        ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
+                       "http3 client request body preread %uz", preread);
+
+        out.buf = r->header_in;
+        out.next = NULL;
+        cl = &out;
+
+    } else {
+        cl = NULL;
+    }
+
+    rc = ngx_http_v3_request_body_filter(r, cl);
+    if (rc != NGX_OK) {
+        return rc;
+    }
+
+    if (rb->rest == 0) {
+        /* the whole request body was pre-read */
+        r->request_body_no_buffering = 0;
+        rb->post_handler(r);
+        return NGX_OK;
+    }
+
+    if (rb->rest < 0) {
+        ngx_log_error(NGX_LOG_ALERT, r->connection->log, 0,
+                      "negative request body rest");
+        return NGX_HTTP_INTERNAL_SERVER_ERROR;
+    }
 
-    c = r->connection;
-    st = ctx->h3_parse;
+    clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module);
+
+    rb->buf = ngx_create_temp_buf(r->pool, clcf->client_body_buffer_size);
+    if (rb->buf == NULL) {
+        return NGX_HTTP_INTERNAL_SERVER_ERROR;
+    }
 
-    if (st == NULL) {
-        ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0,
-                       "http3 parse request body");
+    r->read_event_handler = ngx_http_v3_read_client_request_body_handler;
+    r->write_event_handler = ngx_http_request_empty_handler;
+
+    return ngx_http_v3_do_read_client_request_body(r);
+}
+
+
+static void
+ngx_http_v3_read_client_request_body_handler(ngx_http_request_t *r)
+{
+    ngx_int_t  rc;
 
-        st = ngx_pcalloc(c->pool, sizeof(ngx_http_v3_parse_data_t));
-        if (st == NULL) {
-            goto failed;
-        }
+    if (r->connection->read->timedout) {
+        r->connection->timedout = 1;
+        ngx_http_finalize_request(r, NGX_HTTP_REQUEST_TIME_OUT);
+        return;
+    }
+
+    rc = ngx_http_v3_do_read_client_request_body(r);
+
+    if (rc >= NGX_HTTP_SPECIAL_RESPONSE) {
+        ngx_http_finalize_request(r, rc);
+    }
+}
+
 
-        ctx->h3_parse = st;
+ngx_int_t
+ngx_http_v3_read_unbuffered_request_body(ngx_http_request_t *r)
+{
+    ngx_int_t  rc;
+
+    if (r->connection->read->timedout) {
+        r->connection->timedout = 1;
+        return NGX_HTTP_REQUEST_TIME_OUT;
+    }
+
+    rc = ngx_http_v3_do_read_client_request_body(r);
+
+    if (rc == NGX_OK) {
+        r->reading_body = 0;
     }
 
-    while (b->pos < b->last && ctx->size == 0) {
+    return rc;
+}
+
+
+static ngx_int_t
+ngx_http_v3_do_read_client_request_body(ngx_http_request_t *r)
+{
+    off_t                      rest;
+    size_t                     size;
+    ssize_t                    n;
+    ngx_int_t                  rc;
+    ngx_chain_t                out;
+    ngx_connection_t          *c;
+    ngx_http_request_body_t   *rb;
+    ngx_http_core_loc_conf_t  *clcf;
+
+    c = r->connection;
+    rb = r->request_body;
+
+    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                   "http3 read client request body");
 
-        rc = ngx_http_v3_parse_data(c, st, *b->pos++);
+    for ( ;; ) {
+        for ( ;; ) {
+            if (rb->buf->last == rb->buf->end) {
+
+                /* update chains */
+
+                rc = ngx_http_v3_request_body_filter(r, NULL);
+
+                if (rc != NGX_OK) {
+                    return rc;
+                }
+
+                if (rb->busy != NULL) {
+                    if (r->request_body_no_buffering) {
+                        if (c->read->timer_set) {
+                            ngx_del_timer(c->read);
+                        }
+
+                        if (ngx_handle_read_event(c->read, 0) != NGX_OK) {
+                            return NGX_HTTP_INTERNAL_SERVER_ERROR;
+                        }
+
+                        return NGX_AGAIN;
+                    }
+
+                    ngx_log_error(NGX_LOG_ALERT, c->log, 0,
+                                  "busy buffers after request body flush");
 
-        if (rc > 0) {
-            ngx_http_v3_finalize_connection(c, rc,
-                                            "could not parse request body");
-            goto failed;
-        }
+                    return NGX_HTTP_INTERNAL_SERVER_ERROR;
+                }
+
+                rb->buf->pos = rb->buf->start;
+                rb->buf->last = rb->buf->start;
+            }
+
+            size = rb->buf->end - rb->buf->last;
+            rest = rb->rest - (rb->buf->last - rb->buf->pos);
+
+            if ((off_t) size > rest) {
+                size = (size_t) rest;
+            }
+
+            n = c->recv(c, rb->buf->last, size);
+
+            ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                           "http3 client request body recv %z", n);
+
+            if (n == NGX_AGAIN) {
+                break;
+            }
 
-        if (rc == NGX_ERROR) {
-            goto failed;
-        }
+            if (n == 0) {
+                rb->buf->last_buf = 1;
+            }
+
+            if (n == NGX_ERROR) {
+                c->error = 1;
+                return NGX_HTTP_BAD_REQUEST;
+            }
+
+            rb->buf->last += n;
+
+            /* pass buffer to request body filter chain */
 
-        if (rc == NGX_AGAIN) {
-            ctx->state = sw_skip;
-            continue;
+            out.buf = rb->buf;
+            out.next = NULL;
+
+            rc = ngx_http_v3_request_body_filter(r, &out);
+
+            if (rc != NGX_OK) {
+                return rc;
+            }
+
+            if (rb->rest == 0) {
+                break;
+            }
+
+            if (rb->buf->last < rb->buf->end) {
+                break;
+            }
         }
 
-        if (rc == NGX_DONE) {
-            return NGX_DONE;
+        ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                       "http3 client request body rest %O", rb->rest);
+
+        if (rb->rest == 0) {
+            break;
         }
 
-        /* rc == NGX_OK */
+        if (!c->read->ready) {
+
+            clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module);
+            ngx_add_timer(c->read, clcf->client_body_timeout);
 
-        ctx->size = st->length;
-        ctx->state = sw_start;
+            if (ngx_handle_read_event(c->read, 0) != NGX_OK) {
+                return NGX_HTTP_INTERNAL_SERVER_ERROR;
+            }
+
+            return NGX_AGAIN;
+        }
     }
 
-    if (ctx->state == sw_skip) {
-        ctx->length = 1;
-        return NGX_AGAIN;
+    if (c->read->timer_set) {
+        ngx_del_timer(c->read);
     }
 
-    if (b->pos == b->last) {
-        ctx->length = ctx->size;
-        return NGX_AGAIN;
+    if (!r->request_body_no_buffering) {
+        r->read_event_handler = ngx_http_block_reading;
+        rb->post_handler(r);
     }
 
     return NGX_OK;
+}
 
-failed:
+
+static ngx_int_t
+ngx_http_v3_request_body_filter(ngx_http_request_t *r, ngx_chain_t *in)
+{
+    off_t                      max;
+    size_t                     size;
+    ngx_int_t                  rc;
+    ngx_buf_t                 *b;
+    ngx_uint_t                 last;
+    ngx_chain_t               *cl, *out, *tl, **ll;
+    ngx_http_request_body_t   *rb;
+    ngx_http_core_loc_conf_t  *clcf;
+    ngx_http_core_srv_conf_t  *cscf;
+    ngx_http_v3_parse_data_t  *st;
+
+    rb = r->request_body;
+    st = r->h3_parse;
+
+    if (rb->rest == -1) {
+
+        ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
+                       "http3 request body filter");
+
+        st = ngx_pcalloc(r->pool, sizeof(ngx_http_v3_parse_data_t));
+        if (st == NULL) {
+            return NGX_HTTP_INTERNAL_SERVER_ERROR;
+        }
+
+        r->h3_parse = st;
+
+        cscf = ngx_http_get_module_srv_conf(r, ngx_http_core_module);
+
+        rb->rest = cscf->large_client_header_buffers.size;
+    }
+
+    clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module);
+
+    max = r->headers_in.content_length_n;
+
+    if (max == -1 && clcf->client_max_body_size) {
+        max = clcf->client_max_body_size;
+    }
+
+    out = NULL;
+    ll = &out;
+    last = 0;
+
+    for (cl = in; cl; cl = cl->next) {
+
+        ngx_log_debug7(NGX_LOG_DEBUG_EVENT, r->connection->log, 0,
+                       "http3 body buf "
+                       "t:%d f:%d %p, pos %p, size: %z file: %O, size: %O",
+                       cl->buf->temporary, cl->buf->in_file,
+                       cl->buf->start, cl->buf->pos,
+                       cl->buf->last - cl->buf->pos,
+                       cl->buf->file_pos,
+                       cl->buf->file_last - cl->buf->file_pos);
+
+        if (cl->buf->last_buf) {
+            last = 1;
+        }
+
+        b = NULL;
+
+        while (cl->buf->pos < cl->buf->last) {
+
+            if (st->length == 0) {
+                r->request_length++;
+
+                rc = ngx_http_v3_parse_data(r->connection, st, *cl->buf->pos++);
+
+                if (rc == NGX_AGAIN) {
+                    continue;
+                }
+
+                if (rc == NGX_DONE) {
+                    last = 1;
+                    goto done;
+                }
+
+                if (rc > 0) {
+                    ngx_http_v3_finalize_connection(r->connection, rc,
+                                                   "client sent invalid body");
+                    ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
+                                  "client sent invalid body");
+                    return NGX_HTTP_BAD_REQUEST;
+                }
+
+                if (rc == NGX_ERROR) {
+                    ngx_http_v3_finalize_connection(r->connection,
+                                                NGX_HTTP_V3_ERR_INTERNAL_ERROR,
+                                                "internal error");
+                    return NGX_HTTP_INTERNAL_SERVER_ERROR;
+                }
+
+                /* rc == NGX_OK */
+            }
+
+            if (max != -1 && (uint64_t) (max - rb->received) < st->length) {
+                ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
+                              "client intended to send too large "
+                              "body: %O+%uL bytes",
+                              rb->received, st->length);
+
+                return NGX_HTTP_REQUEST_ENTITY_TOO_LARGE;
+            }
 
-    return NGX_ERROR;
+            if (b
+                && st->length <= 128
+                && (uint64_t) (cl->buf->last - cl->buf->pos) >= st->length)
+            {
+                rb->received += st->length;
+                r->request_length += st->length;
+
+                if (st->length < 8) {
+
+                    while (st->length) {
+                        *b->last++ = *cl->buf->pos++;
+                        st->length--;
+                    }
+
+                } else {
+                    ngx_memmove(b->last, cl->buf->pos, st->length);
+                    b->last += st->length;
+                    cl->buf->pos += st->length;
+                    st->length = 0;
+                }
+
+                continue;
+            }
+
+            tl = ngx_chain_get_free_buf(r->pool, &rb->free);
+            if (tl == NULL) {
+                return NGX_HTTP_INTERNAL_SERVER_ERROR;
+            }
+
+            b = tl->buf;
+
+            ngx_memzero(b, sizeof(ngx_buf_t));
+
+            b->temporary = 1;
+            b->tag = (ngx_buf_tag_t) &ngx_http_read_client_request_body;
+            b->start = cl->buf->pos;
+            b->pos = cl->buf->pos;
+            b->last = cl->buf->last;
+            b->end = cl->buf->end;
+            b->flush = r->request_body_no_buffering;
+
+            *ll = tl;
+            ll = &tl->next;
+
+            size = cl->buf->last - cl->buf->pos;
+
+            if (size > st->length) {
+                cl->buf->pos += (size_t) st->length;
+                rb->received += st->length;
+                r->request_length += st->length;
+                st->length = 0;
+
+            } else {
+                st->length -= size;
+                rb->received += size;
+                r->request_length += size;
+                cl->buf->pos = cl->buf->last;
+            }
+
+            b->last = cl->buf->pos;
+        }
+    }
+
+done:
+
+    if (last) {
+
+        if (st->length > 0) {
+            ngx_log_error(NGX_LOG_INFO, r->connection->log, 0,
+                          "client prematurely closed stream");
+            r->connection->error = 1;
+            return NGX_HTTP_BAD_REQUEST;
+        }
+
+        if (r->headers_in.content_length_n == -1) {
+            r->headers_in.content_length_n = rb->received;
+
+        } else if (r->headers_in.content_length_n != rb->received) {
+            ngx_log_error(NGX_LOG_INFO, r->connection->log, 0,
+                          "client sent less body data than expected: "
+                          "%O out of %O bytes of request body received",
+                          rb->received, r->headers_in.content_length_n);
+            return NGX_HTTP_BAD_REQUEST;
+        }
+
+        rb->rest = 0;
+
+        tl = ngx_chain_get_free_buf(r->pool, &rb->free);
+        if (tl == NULL) {
+            return NGX_HTTP_INTERNAL_SERVER_ERROR;
+        }
+
+        b = tl->buf;
+
+        ngx_memzero(b, sizeof(ngx_buf_t));
+
+        b->last_buf = 1;
+
+        *ll = tl;
+        ll = &tl->next;
+
+    } else {
+
+        /* set rb->rest, amount of data we want to see next time */
+
+        cscf = ngx_http_get_module_srv_conf(r, ngx_http_core_module);
+
+        rb->rest = (off_t) cscf->large_client_header_buffers.size;
+    }
+
+    rc = ngx_http_top_request_body_filter(r, out);
+
+    ngx_chain_update_chains(r->pool, &rb->free, &rb->busy, &out,
+                            (ngx_buf_tag_t) &ngx_http_read_client_request_body);
+
+    return rc;
 }