static int on_frame_recv(nghttp2_session *session, const nghttp2_frame *frame, void *userp) { struct connectdata *conn = (struct connectdata *)userp; struct http_conn *c = &conn->proto.httpc; int rv; (void)session; (void)frame; infof(conn->data, "on_frame_recv() was called with header %x\n", frame->hd.type); switch(frame->hd.type) { case NGHTTP2_HEADERS: if(frame->headers.cat != NGHTTP2_HCAT_RESPONSE) break; c->bodystarted = TRUE; Curl_add_buffer(c->header_recvbuf, "\r\n", 2); c->nread_header_recvbuf = c->len < c->header_recvbuf->size_used ? c->len : c->header_recvbuf->size_used; memcpy(c->mem, c->header_recvbuf->buffer, c->nread_header_recvbuf); c->mem += c->nread_header_recvbuf; c->len -= c->nread_header_recvbuf; break; case NGHTTP2_PUSH_PROMISE: rv = nghttp2_submit_rst_stream(session, NGHTTP2_FLAG_NONE, frame->hd.stream_id, NGHTTP2_CANCEL); if(nghttp2_is_fatal(rv)) { return rv; } break; } return 0; }
/* Start submitting the response to a stream request. This is possible * once we have all the response headers. The response body will be * read by the session using the callback we supply. */ apr_status_t h2_session_handle_response(h2_session *session, h2_stream *stream) { apr_status_t status = APR_SUCCESS; int rv = 0; AP_DEBUG_ASSERT(session); AP_DEBUG_ASSERT(stream); AP_DEBUG_ASSERT(stream->response); if (stream->response->ngheader) { rv = submit_response(session, stream->response); } else { rv = nghttp2_submit_rst_stream(session->ngh2, NGHTTP2_FLAG_NONE, stream->id, NGHTTP2_PROTOCOL_ERROR); } if (nghttp2_is_fatal(rv)) { status = APR_EGENERAL; h2_session_abort_int(session, rv); ap_log_cerror(APLOG_MARK, APLOG_ERR, status, session->c, APLOGNO(02940) "submit_response: %s", nghttp2_strerror(rv)); } return status; }
/* h2_io_on_read_cb implementation that offers the data read * directly to the session for consumption. */ static apr_status_t session_receive(const char *data, apr_size_t len, apr_size_t *readlen, int *done, void *puser) { h2_session *session = (h2_session *)puser; AP_DEBUG_ASSERT(session); if (len > 0) { ssize_t n = nghttp2_session_mem_recv(session->ngh2, (const uint8_t *)data, len); if (n < 0) { ap_log_cerror(APLOG_MARK, APLOG_DEBUG, APR_EGENERAL, session->c, "h2_session: nghttp2_session_mem_recv error %d", (int)n); if (nghttp2_is_fatal((int)n)) { *done = 1; h2_session_abort_int(session, (int)n); return APR_EGENERAL; } } else { *readlen = n; } } return APR_SUCCESS; }
apr_status_t h2_session_write(h2_session *session, apr_interval_time_t timeout) { apr_status_t status = APR_EAGAIN; h2_response *response = NULL; int have_written = 0; assert(session); /* Check that any pending window updates are sent. */ status = h2_session_update_windows(session); if (status == APR_SUCCESS) { have_written = 1; } else if (status != APR_EAGAIN) { return status; } /* If we have responses ready, submit them now. */ while ((response = h2_session_pop_response(session)) != NULL) { h2_stream *stream = h2_session_get_stream(session, response->stream_id); if (stream) { status = h2_session_handle_response(session, stream, response); have_written = 1; } h2_response_destroy(response); response = NULL; } h2_session_resume_streams_with_data(session); if (!have_written && timeout > 0 && !h2_session_want_write(session)) { status = h2_mplx_out_trywait(session->mplx, timeout, session->iowait); } h2_session_resume_streams_with_data(session); if (h2_session_want_write(session)) { status = APR_SUCCESS; int rv = nghttp2_session_send(session->ngh2); if (rv != 0) { ap_log_cerror( APLOG_MARK, APLOG_INFO, 0, session->c, "h2_session: send: %s", nghttp2_strerror(rv)); if (nghttp2_is_fatal(rv)) { h2_session_abort_int(session, rv); status = APR_ECONNABORTED; } } have_written = 1; } if (have_written) { h2_conn_io_flush(&session->io); } reap_zombies(session); return status; }
/* * If the read would block (EWOULDBLOCK) we return -1. Otherwise we return * a regular CURLcode value. */ static ssize_t http2_recv(struct connectdata *conn, int sockindex, char *mem, size_t len, CURLcode *err) { CURLcode rc; ssize_t rv; ssize_t nread; char inbuf[H2_BUFSIZE]; (void)sockindex; /* we always do HTTP2 on sockindex 0 */ conn->proto.httpc.mem = mem; conn->proto.httpc.len = len; infof(conn->data, "http2_recv: %d bytes buffer\n", conn->proto.httpc.len); for(;;) { rc = Curl_read_plain(conn->sock[FIRSTSOCKET], inbuf, H2_BUFSIZE, &nread); if(rc == CURLE_AGAIN) { if(len == conn->proto.httpc.len) { *err = rc; return 0; } return len - conn->proto.httpc.len; } if(rc) { failf(conn->data, "Failed receiving HTTP2 data"); *err = CURLE_RECV_ERROR; return 0; } if(!nread) { *err = CURLE_RECV_ERROR; return 0; /* TODO EOF? */ } rv = nghttp2_session_mem_recv(conn->proto.httpc.h2, (const uint8_t *)inbuf, nread); if(nghttp2_is_fatal((int)rv)) { failf(conn->data, "nghttp2_session_mem_recv() returned %d:%s\n", rv, nghttp2_strerror((int)rv)); *err = CURLE_RECV_ERROR; return 0; } if(rv < nread) { /* Happens when NGHTTP2_ERR_PAUSE is returned from user callback */ break; } break; } return len - conn->proto.httpc.len; }
static int on_data_chunk_recv_cb(nghttp2_session *ngh2, uint8_t flags, int32_t stream_id, const uint8_t *data, size_t len, void *userp) { int rv; h2_session *session = (h2_session *)userp; h2_stream * stream; apr_status_t status; (void)flags; if (session->aborted) { return NGHTTP2_ERR_CALLBACK_FAILURE; } stream = h2_stream_set_get(session->streams, stream_id); if (!stream) { ap_log_cerror(APLOG_MARK, APLOG_ERR, 0, session->c, APLOGNO(02919) "h2_session: stream(%ld-%d): on_data_chunk for unknown stream", session->id, (int)stream_id); rv = nghttp2_submit_rst_stream(ngh2, NGHTTP2_FLAG_NONE, stream_id, NGHTTP2_INTERNAL_ERROR); if (nghttp2_is_fatal(rv)) { return NGHTTP2_ERR_CALLBACK_FAILURE; } return 0; } status = h2_stream_write_data(stream, (const char *)data, len); ap_log_cerror(APLOG_MARK, APLOG_TRACE1, status, session->c, "h2_stream(%ld-%d): written DATA, length %d", session->id, stream_id, (int)len); if (status != APR_SUCCESS) { rv = nghttp2_submit_rst_stream(ngh2, NGHTTP2_FLAG_NONE, stream_id, NGHTTP2_INTERNAL_ERROR); if (nghttp2_is_fatal(rv)) { return NGHTTP2_ERR_CALLBACK_FAILURE; } } return 0; }
/* Start submitting the response to a stream request. This is possible * once we have all the response headers. The response body will be * read by the session using the callback we supply. */ apr_status_t h2_session_handle_response(h2_session *session, h2_stream *stream, h2_response *head) { assert(session); apr_status_t status = APR_SUCCESS; int rv = 0; if (head->http_status) { ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, session->c, "h2_stream(%ld-%d): submitting response %s with %d headers", session->id, stream->id, head->http_status, (int)head->nvlen); assert(head->nvlen); nghttp2_data_provider provider = { stream->id, stream_data_cb }; rv = nghttp2_submit_response(session->ngh2, stream->id, &head->nv, head->nvlen, &provider); if (rv != 0) { ap_log_cerror(APLOG_MARK, APLOG_ERR, status, session->c, "h2_stream(%ld-%d): submit_response: %s", session->id, stream->id, nghttp2_strerror(rv)); } else { ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, session->c, "h2_stream(%ld-%d): submitted response %s with %d " "headers, rv=%d", session->id, stream->id, head->http_status, (int)head->nvlen, rv); } } else { rv = nghttp2_submit_rst_stream(session->ngh2, 0, stream->id, NGHTTP2_ERR_INVALID_STATE); } if (nghttp2_is_fatal(rv)) { status = APR_EGENERAL; h2_session_abort_int(session, rv); ap_log_cerror(APLOG_MARK, APLOG_ERR, status, session->c, "submit_response: %s", nghttp2_strerror(rv)); } return status; }
static h2_stream *resume_on_data(void *ctx, h2_stream *stream) { h2_session *session = (h2_session *)ctx; assert(session); assert(stream); if (h2_stream_is_suspended(stream)) { ap_log_perror(APLOG_MARK, APLOG_DEBUG, 0, stream->pool, "h2_stream(%ld-%d): suspended, checking for DATA", h2_mplx_get_id(stream->m), stream->id); if (h2_mplx_out_has_data_for(stream->m, h2_stream_get_id(stream))) { h2_stream_set_suspended(stream, 0); int rv = nghttp2_session_resume_data(session->ngh2, h2_stream_get_id(stream)); ap_log_cerror(APLOG_MARK, nghttp2_is_fatal(rv)? APLOG_ERR : APLOG_DEBUG, 0, session->c, "h2_stream(%ld-%d): resuming stream %s", session->id, stream->id, nghttp2_strerror(rv)); } } return NULL; }
static int resume_on_data(void *ctx, h2_stream *stream) { resume_ctx *rctx = (resume_ctx*)ctx; h2_session *session = rctx->session; AP_DEBUG_ASSERT(session); AP_DEBUG_ASSERT(stream); if (h2_stream_is_suspended(stream)) { if (h2_mplx_out_has_data_for(stream->m, stream->id)) { int rv; h2_stream_set_suspended(stream, 0); ++rctx->resume_count; rv = nghttp2_session_resume_data(session->ngh2, stream->id); ap_log_cerror(APLOG_MARK, nghttp2_is_fatal(rv)? APLOG_ERR : APLOG_DEBUG, 0, session->c, APLOGNO(02936) "h2_stream(%ld-%d): resuming stream %s", session->id, stream->id, nghttp2_strerror(rv)); } } return 1; }
/* return number of received (decrypted) bytes */ static ssize_t http2_send(struct connectdata *conn, int sockindex, const void *mem, size_t len, CURLcode *err) { /* * BIG TODO: Currently, we send request in this function, but this * function is also used to send request body. It would be nice to * add dedicated function for request. */ int rv; struct http_conn *httpc = &conn->proto.httpc; struct HTTP *stream = conn->data->req.protop; nghttp2_nv *nva; size_t nheader; size_t i; size_t authority_idx; char *hdbuf = (char*)mem; char *end; nghttp2_data_provider data_prd; int32_t stream_id; nghttp2_session *h2 = httpc->h2; (void)sockindex; DEBUGF(infof(conn->data, "http2_send len=%zu\n", len)); if(stream->stream_id != -1) { /* If stream_id != -1, we have dispatched request HEADERS, and now are going to send or sending request body in DATA frame */ stream->upload_mem = mem; stream->upload_len = len; nghttp2_session_resume_data(h2, stream->stream_id); rv = nghttp2_session_send(h2); if(nghttp2_is_fatal(rv)) { *err = CURLE_SEND_ERROR; return -1; } len -= stream->upload_len; /* Nullify here because we call nghttp2_session_send() and they might refer to the old buffer. */ stream->upload_mem = NULL; stream->upload_len = 0; if(stream->upload_left) { /* we are sure that we have more data to send here. Calling the following API will make nghttp2_session_want_write() return nonzero if remote window allows it, which then libcurl checks socket is writable or not. See http2_perform_getsock(). */ nghttp2_session_resume_data(h2, stream->stream_id); } DEBUGF(infof(conn->data, "http2_send returns %zu for stream %u\n", len, stream->stream_id)); return len; } /* Calculate number of headers contained in [mem, mem + len) */ /* Here, we assume the curl http code generate *correct* HTTP header field block */ nheader = 0; for(i = 0; i < len; ++i) { if(hdbuf[i] == 0x0a) { ++nheader; } } /* We counted additional 2 \n in the first and last line. We need 3 new headers: :method, :path and :scheme. Therefore we need one more space. */ nheader += 1; nva = malloc(sizeof(nghttp2_nv) * nheader); if(nva == NULL) { *err = CURLE_OUT_OF_MEMORY; return -1; } /* Extract :method, :path from request line */ end = strchr(hdbuf, ' '); nva[0].name = (unsigned char *)":method"; nva[0].namelen = (uint16_t)strlen((char *)nva[0].name); nva[0].value = (unsigned char *)hdbuf; nva[0].valuelen = (uint16_t)(end - hdbuf); nva[0].flags = NGHTTP2_NV_FLAG_NONE; hdbuf = end + 1; end = strchr(hdbuf, ' '); nva[1].name = (unsigned char *)":path"; nva[1].namelen = (uint16_t)strlen((char *)nva[1].name); nva[1].value = (unsigned char *)hdbuf; nva[1].valuelen = (uint16_t)(end - hdbuf); nva[1].flags = NGHTTP2_NV_FLAG_NONE; nva[2].name = (unsigned char *)":scheme"; nva[2].namelen = (uint16_t)strlen((char *)nva[2].name); if(conn->handler->flags & PROTOPT_SSL) nva[2].value = (unsigned char *)"https"; else nva[2].value = (unsigned char *)"http"; nva[2].valuelen = (uint16_t)strlen((char *)nva[2].value); nva[2].flags = NGHTTP2_NV_FLAG_NONE; hdbuf = strchr(hdbuf, 0x0a); ++hdbuf; authority_idx = 0; for(i = 3; i < nheader; ++i) { end = strchr(hdbuf, ':'); assert(end); if(end - hdbuf == 4 && Curl_raw_nequal("host", hdbuf, 4)) { authority_idx = i; nva[i].name = (unsigned char *)":authority"; nva[i].namelen = (uint16_t)strlen((char *)nva[i].name); } else { nva[i].name = (unsigned char *)hdbuf; nva[i].namelen = (uint16_t)(end - hdbuf); } hdbuf = end + 1; for(; *hdbuf == ' '; ++hdbuf); end = strchr(hdbuf, 0x0d); assert(end); nva[i].value = (unsigned char *)hdbuf; nva[i].valuelen = (uint16_t)(end - hdbuf); nva[i].flags = NGHTTP2_NV_FLAG_NONE; hdbuf = end + 2; /* Inspect Content-Length header field and retrieve the request entity length so that we can set END_STREAM to the last DATA frame. */ if(nva[i].namelen == 14 && Curl_raw_nequal("content-length", (char*)nva[i].name, 14)) { size_t j; stream->upload_left = 0; for(j = 0; j < nva[i].valuelen; ++j) { stream->upload_left *= 10; stream->upload_left += nva[i].value[j] - '0'; } DEBUGF(infof(conn->data, "request content-length=%" CURL_FORMAT_CURL_OFF_T "\n", stream->upload_left)); } } /* :authority must come before non-pseudo header fields */ if(authority_idx != 0 && authority_idx != AUTHORITY_DST_IDX) { nghttp2_nv authority = nva[authority_idx]; for(i = authority_idx; i > AUTHORITY_DST_IDX; --i) { nva[i] = nva[i - 1]; } nva[i] = authority; } switch(conn->data->set.httpreq) { case HTTPREQ_POST: case HTTPREQ_POST_FORM: case HTTPREQ_PUT: data_prd.read_callback = data_source_read_callback; data_prd.source.ptr = NULL; stream_id = nghttp2_submit_request(h2, NULL, nva, nheader, &data_prd, NULL); break; default: stream_id = nghttp2_submit_request(h2, NULL, nva, nheader, NULL, NULL); } free(nva); if(stream_id < 0) { DEBUGF(infof(conn->data, "http2_send() send error\n")); *err = CURLE_SEND_ERROR; return -1; } infof(conn->data, "Using Stream ID: %x (easy handle %p)\n", stream_id, conn->data); stream->stream_id = stream_id; /* put the SessionHandle in the hash with the stream_id as key */ if(!Curl_hash_add(&httpc->streamsh, &stream->stream_id, sizeof(stream_id), conn->data)) { failf(conn->data, "Couldn't add stream to hash!"); *err = CURLE_OUT_OF_MEMORY; return -1; } rv = nghttp2_session_send(h2); if(rv != 0) { *err = CURLE_SEND_ERROR; return -1; } if(stream->stream_id != -1) { /* If whole HEADERS frame was sent off to the underlying socket, the nghttp2 library calls data_source_read_callback. But only it found that no data available, so it deferred the DATA transmission. Which means that nghttp2_session_want_write() returns 0 on http2_perform_getsock(), which results that no writable socket check is performed. To workaround this, we issue nghttp2_session_resume_data() here to bring back DATA transmission from deferred state. */ nghttp2_session_resume_data(h2, stream->stream_id); } return len; }
/* * If the read would block (EWOULDBLOCK) we return -1. Otherwise we return * a regular CURLcode value. */ static ssize_t http2_recv(struct connectdata *conn, int sockindex, char *mem, size_t len, CURLcode *err) { CURLcode result = CURLE_OK; ssize_t rv; ssize_t nread; struct http_conn *httpc = &conn->proto.httpc; struct SessionHandle *data = conn->data; struct HTTP *stream = data->req.protop; (void)sockindex; /* we always do HTTP2 on sockindex 0 */ /* If stream is closed, return 0 to signal the http routine to close the connection. We need to handle stream closure here, otherwise, we may be going to read from underlying connection, and gets EAGAIN, and we will get stuck there. */ if(stream->memlen == 0 && stream->closed) { return http2_handle_stream_close(httpc, data, stream, err); } /* Nullify here because we call nghttp2_session_send() and they might refer to the old buffer. */ stream->upload_mem = NULL; stream->upload_len = 0; /* * At this point 'stream' is just in the SessionHandle the connection * identifies as its owner at this time. */ if(stream->bodystarted && stream->nread_header_recvbuf < stream->header_recvbuf->size_used) { /* If there is body data pending for this stream to return, do that */ size_t left = stream->header_recvbuf->size_used - stream->nread_header_recvbuf; size_t ncopy = MIN(len, left); memcpy(mem, stream->header_recvbuf->buffer + stream->nread_header_recvbuf, ncopy); stream->nread_header_recvbuf += ncopy; infof(data, "http2_recv: Got %d bytes from header_recvbuf\n", (int)ncopy); return ncopy; } infof(data, "http2_recv: %d bytes buffer at %p (stream %u)\n", len, mem, stream->stream_id); if((data->state.drain) && stream->memlen) { DEBUGF(infof(data, "http2_recv: DRAIN %zu bytes stream %u!! (%p => %p)\n", stream->memlen, stream->stream_id, stream->mem, mem)); if(mem != stream->mem) { /* if we didn't get the same buffer this time, we must move the data to the beginning */ memmove(mem, stream->mem, stream->memlen); stream->len = len - stream->memlen; stream->mem = mem; } } else if(stream->pausedata) { nread = MIN(len, stream->pauselen); memcpy(mem, stream->pausedata, nread); stream->pausedata += nread; stream->pauselen -= nread; infof(data, "%zu data bytes written\n", nread); if(stream->pauselen == 0) { DEBUGF(infof(data, "Unpaused by stream %u\n", stream->stream_id)); assert(httpc->pause_stream_id == stream->stream_id); httpc->pause_stream_id = 0; stream->pausedata = NULL; stream->pauselen = 0; } infof(data, "http2_recv: returns unpaused %zd bytes on stream %u\n", nread, stream->stream_id); return nread; } else if(httpc->pause_stream_id) { /* If a stream paused nghttp2_session_mem_recv previously, and has not processed all data, it still refers to the buffer in nghttp2_session. If we call nghttp2_session_mem_recv(), we may overwrite that buffer. To avoid that situation, just return here with CURLE_AGAIN. This could be busy loop since data in socket is not read. But it seems that usually streams are notified with its drain property, and socket is read again quickly. */ *err = CURLE_AGAIN; return -1; } else { char *inbuf; /* remember where to store incoming data for this stream and how big the buffer is */ stream->mem = mem; stream->len = len; stream->memlen = 0; if(httpc->inbuflen == 0) { nread = ((Curl_recv *)httpc->recv_underlying)( conn, FIRSTSOCKET, httpc->inbuf, H2_BUFSIZE, &result); if(result == CURLE_AGAIN) { *err = result; return -1; } if(nread == -1) { failf(data, "Failed receiving HTTP2 data"); *err = result; return 0; } if(nread == 0) { failf(data, "Unexpected EOF"); *err = CURLE_RECV_ERROR; return -1; } DEBUGF(infof(data, "nread=%zd\n", nread)); httpc->inbuflen = nread; inbuf = httpc->inbuf; } else { nread = httpc->inbuflen - httpc->nread_inbuf; inbuf = httpc->inbuf + httpc->nread_inbuf; DEBUGF(infof(data, "Use data left in connection buffer, nread=%zd\n", nread)); } rv = nghttp2_session_mem_recv(httpc->h2, (const uint8_t *)inbuf, nread); if(nghttp2_is_fatal((int)rv)) { failf(data, "nghttp2_session_mem_recv() returned %d:%s\n", rv, nghttp2_strerror((int)rv)); *err = CURLE_RECV_ERROR; return 0; } DEBUGF(infof(data, "nghttp2_session_mem_recv() returns %zd\n", rv)); if(nread == rv) { DEBUGF(infof(data, "All data in connection buffer processed\n")); httpc->inbuflen = 0; httpc->nread_inbuf = 0; } else { httpc->nread_inbuf += rv; DEBUGF(infof(data, "%zu bytes left in connection buffer\n", httpc->inbuflen - httpc->nread_inbuf)); } /* Always send pending frames in nghttp2 session, because nghttp2_session_mem_recv() may queue new frame */ rv = nghttp2_session_send(httpc->h2); if(rv != 0) { *err = CURLE_SEND_ERROR; return 0; } } if(stream->memlen) { ssize_t retlen = stream->memlen; infof(data, "http2_recv: returns %zd for stream %u\n", retlen, stream->stream_id); stream->memlen = 0; if(httpc->pause_stream_id == stream->stream_id) { /* data for this stream is returned now, but this stream caused a pause already so we need it called again asap */ DEBUGF(infof(data, "Data returned for PAUSED stream %u\n", stream->stream_id)); } else data->state.drain = 0; /* this stream is hereby drained */ return retlen; } /* If stream is closed, return 0 to signal the http routine to close the connection */ if(stream->closed) { return http2_handle_stream_close(httpc, data, stream, err); } *err = CURLE_AGAIN; DEBUGF(infof(data, "http2_recv returns AGAIN for stream %u\n", stream->stream_id)); return -1; }
static int on_frame_recv(nghttp2_session *session, const nghttp2_frame *frame, void *userp) { struct connectdata *conn = (struct connectdata *)userp; struct http_conn *httpc = &conn->proto.httpc; struct SessionHandle *data_s = NULL; struct HTTP *stream = NULL; int rv; size_t left, ncopy; int32_t stream_id = frame->hd.stream_id; (void)session; (void)frame; DEBUGF(infof(conn->data, "on_frame_recv() header %x stream %x\n", frame->hd.type, stream_id)); if(stream_id) { /* get the stream from the hash based on Stream ID, stream ID zero is for connection-oriented stuff */ data_s = Curl_hash_pick(&httpc->streamsh, &stream_id, sizeof(stream_id)); if(!data_s) { /* Receiving a Stream ID not in the hash should not happen, this is an internal error more than anything else! */ failf(conn->data, "Received frame on Stream ID: %x not in stream hash!", stream_id); return NGHTTP2_ERR_CALLBACK_FAILURE; } stream = data_s->req.protop; } switch(frame->hd.type) { case NGHTTP2_DATA: /* If body started on this stream, then receiving DATA is illegal. */ if(!stream->bodystarted) { rv = nghttp2_submit_rst_stream(session, NGHTTP2_FLAG_NONE, stream_id, NGHTTP2_PROTOCOL_ERROR); if(nghttp2_is_fatal(rv)) { return NGHTTP2_ERR_CALLBACK_FAILURE; } } break; case NGHTTP2_HEADERS: if(frame->headers.cat == NGHTTP2_HCAT_REQUEST) break; if(stream->bodystarted) { /* Only valid HEADERS after body started is trailer HEADERS. We ignores trailer HEADERS for now. nghttp2 guarantees that it has END_STREAM flag set. */ break; } /* nghttp2 guarantees that :status is received, and we store it to stream->status_code */ DEBUGASSERT(stream->status_code != -1); /* Only final status code signals the end of header */ if(stream->status_code / 100 != 1) { stream->bodystarted = TRUE; stream->status_code = -1; } Curl_add_buffer(stream->header_recvbuf, "\r\n", 2); left = stream->header_recvbuf->size_used - stream->nread_header_recvbuf; ncopy = MIN(stream->len, left); memcpy(&stream->mem[stream->memlen], stream->header_recvbuf->buffer + stream->nread_header_recvbuf, ncopy); stream->nread_header_recvbuf += ncopy; DEBUGF(infof(data_s, "Store %zu bytes headers from stream %u at %p\n", ncopy, stream_id, stream->mem)); stream->len -= ncopy; stream->memlen += ncopy; data_s->state.drain++; Curl_expire(data_s, 1); break; case NGHTTP2_PUSH_PROMISE: DEBUGF(infof(data_s, "Got PUSH_PROMISE, RST_STREAM it!\n")); rv = nghttp2_submit_rst_stream(session, NGHTTP2_FLAG_NONE, frame->push_promise.promised_stream_id, NGHTTP2_CANCEL); if(nghttp2_is_fatal(rv)) { return rv; } break; case NGHTTP2_SETTINGS: { uint32_t max_conn = httpc->settings.max_concurrent_streams; DEBUGF(infof(conn->data, "Got SETTINGS for stream %u!\n", stream_id)); httpc->settings.max_concurrent_streams = nghttp2_session_get_remote_settings( session, NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS); httpc->settings.enable_push = nghttp2_session_get_remote_settings( session, NGHTTP2_SETTINGS_ENABLE_PUSH); DEBUGF(infof(conn->data, "MAX_CONCURRENT_STREAMS == %d\n", httpc->settings.max_concurrent_streams)); DEBUGF(infof(conn->data, "ENABLE_PUSH == %s\n", httpc->settings.enable_push?"TRUE":"false")); if(max_conn != httpc->settings.max_concurrent_streams) { /* only signal change if the value actually changed */ infof(conn->data, "Connection state changed (MAX_CONCURRENT_STREAMS updated)!\n"); Curl_multi_connchanged(conn->data->multi); } } break; default: DEBUGF(infof(conn->data, "Got frame type %x for stream %u!\n", frame->hd.type, stream_id)); break; } return 0; }
CURLcode Curl_http2_switched(struct connectdata *conn, const char *mem, size_t nread) { CURLcode result; struct http_conn *httpc = &conn->proto.httpc; int rv; ssize_t nproc; struct SessionHandle *data = conn->data; struct HTTP *stream = conn->data->req.protop; result = Curl_http2_setup(conn); if(result) return result; httpc->recv_underlying = (recving)conn->recv[FIRSTSOCKET]; httpc->send_underlying = (sending)conn->send[FIRSTSOCKET]; conn->recv[FIRSTSOCKET] = http2_recv; conn->send[FIRSTSOCKET] = http2_send; if(conn->data->req.upgr101 == UPGR101_RECEIVED) { /* stream 1 is opened implicitly on upgrade */ stream->stream_id = 1; /* queue SETTINGS frame (again) */ rv = nghttp2_session_upgrade(httpc->h2, httpc->binsettings, httpc->binlen, NULL); if(rv != 0) { failf(data, "nghttp2_session_upgrade() failed: %s(%d)", nghttp2_strerror(rv), rv); return CURLE_HTTP2; } nghttp2_session_set_stream_user_data(httpc->h2, stream->stream_id, conn->data); } else { /* stream ID is unknown at this point */ stream->stream_id = -1; rv = nghttp2_submit_settings(httpc->h2, NGHTTP2_FLAG_NONE, NULL, 0); if(rv != 0) { failf(data, "nghttp2_submit_settings() failed: %s(%d)", nghttp2_strerror(rv), rv); return CURLE_HTTP2; } } /* we are going to copy mem to httpc->inbuf. This is required since mem is part of buffer pointed by stream->mem, and callbacks called by nghttp2_session_mem_recv() will write stream specific data into stream->mem, overwriting data already there. */ if(H2_BUFSIZE < nread) { failf(data, "connection buffer size is too small to store data following " "HTTP Upgrade response header: buflen=%zu, datalen=%zu", H2_BUFSIZE, nread); return CURLE_HTTP2; } infof(conn->data, "Copying HTTP/2 data in stream buffer to connection buffer" " after upgrade: len=%zu\n", nread); memcpy(httpc->inbuf, mem, nread); httpc->inbuflen = nread; nproc = nghttp2_session_mem_recv(httpc->h2, (const uint8_t *)httpc->inbuf, httpc->inbuflen); if(nghttp2_is_fatal((int)nproc)) { failf(data, "nghttp2_session_mem_recv() failed: %s(%d)", nghttp2_strerror((int)nproc), (int)nproc); return CURLE_HTTP2; } DEBUGF(infof(data, "nghttp2_session_mem_recv() returns %zd\n", nproc)); if((ssize_t)nread == nproc) { httpc->inbuflen = 0; httpc->nread_inbuf = 0; } else { httpc->nread_inbuf += nproc; } /* Try to send some frames since we may read SETTINGS already. */ rv = nghttp2_session_send(httpc->h2); if(rv != 0) { failf(data, "nghttp2_session_send() failed: %s(%d)", nghttp2_strerror(rv), rv); return CURLE_HTTP2; } return CURLE_OK; }
/* * If the read would block (EWOULDBLOCK) we return -1. Otherwise we return * a regular CURLcode value. */ static ssize_t http2_recv(struct connectdata *conn, int sockindex, char *mem, size_t len, CURLcode *err) { CURLcode rc; ssize_t rv; ssize_t nread; struct http_conn *httpc = &conn->proto.httpc; (void)sockindex; /* we always do HTTP2 on sockindex 0 */ if(httpc->closed) { /* Reset to FALSE to prevent infinite loop in readwrite_data function. */ httpc->closed = FALSE; return 0; } /* Nullify here because we call nghttp2_session_send() and they might refer to the old buffer. */ httpc->upload_mem = NULL; httpc->upload_len = 0; if(httpc->bodystarted && httpc->nread_header_recvbuf < httpc->header_recvbuf->size_used) { size_t left = httpc->header_recvbuf->size_used - httpc->nread_header_recvbuf; size_t ncopy = len < left ? len : left; memcpy(mem, httpc->header_recvbuf->buffer + httpc->nread_header_recvbuf, ncopy); httpc->nread_header_recvbuf += ncopy; return ncopy; } if(httpc->data) { nread = len < httpc->datalen ? len : httpc->datalen; memcpy(mem, httpc->data, nread); httpc->data += nread; httpc->datalen -= nread; infof(conn->data, "%zu data written\n", nread); if(httpc->datalen == 0) { httpc->data = NULL; httpc->datalen = 0; } return nread; } conn->proto.httpc.mem = mem; conn->proto.httpc.len = len; infof(conn->data, "http2_recv: %d bytes buffer\n", conn->proto.httpc.len); rc = 0; nread = ((Curl_recv*)httpc->recv_underlying)(conn, FIRSTSOCKET, httpc->inbuf, H2_BUFSIZE, &rc); if(rc == CURLE_AGAIN) { *err = rc; return -1; } if(nread == -1) { failf(conn->data, "Failed receiving HTTP2 data"); *err = rc; return 0; } infof(conn->data, "nread=%zd\n", nread); rv = nghttp2_session_mem_recv(httpc->h2, (const uint8_t *)httpc->inbuf, nread); if(nghttp2_is_fatal((int)rv)) { failf(conn->data, "nghttp2_session_mem_recv() returned %d:%s\n", rv, nghttp2_strerror((int)rv)); *err = CURLE_RECV_ERROR; return 0; } infof(conn->data, "nghttp2_session_mem_recv() returns %zd\n", rv); /* Always send pending frames in nghttp2 session, because nghttp2_session_mem_recv() may queue new frame */ rv = nghttp2_session_send(httpc->h2); if(rv != 0) { *err = CURLE_SEND_ERROR; return 0; } if(len != httpc->len) { return len - conn->proto.httpc.len; } /* If stream is closed, return 0 to signal the http routine to close the connection */ if(httpc->closed) { /* Reset to FALSE to prevent infinite loop in readwrite_data function. */ httpc->closed = FALSE; return 0; } *err = CURLE_AGAIN; return -1; }
/** * nghttp2 session has received a complete frame. Most, it uses * for processing of internal state. HEADER and DATA frames however * we need to handle ourself. */ static int on_frame_recv_cb(nghttp2_session *ng2s, const nghttp2_frame *frame, void *userp) { int rv; h2_session *session = (h2_session *)userp; apr_status_t status = APR_SUCCESS; if (session->aborted) { return NGHTTP2_ERR_CALLBACK_FAILURE; } ++session->frames_received; ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, session->c, "h2_session(%ld): on_frame_rcv #%ld, type=%d", session->id, (long)session->frames_received, frame->hd.type); switch (frame->hd.type) { case NGHTTP2_HEADERS: { int eos; h2_stream * stream = h2_stream_set_get(session->streams, frame->hd.stream_id); if (stream == NULL) { ap_log_cerror(APLOG_MARK, APLOG_ERR, 0, session->c, APLOGNO(02921) "h2_session: stream(%ld-%d): HEADERS frame " "for unknown stream", session->id, (int)frame->hd.stream_id); rv = nghttp2_submit_rst_stream(ng2s, NGHTTP2_FLAG_NONE, frame->hd.stream_id, NGHTTP2_INTERNAL_ERROR); if (nghttp2_is_fatal(rv)) { return NGHTTP2_ERR_CALLBACK_FAILURE; } return 0; } eos = (frame->hd.flags & NGHTTP2_FLAG_END_STREAM); status = stream_end_headers(session, stream, eos); break; } case NGHTTP2_DATA: { h2_stream * stream = h2_stream_set_get(session->streams, frame->hd.stream_id); if (stream == NULL) { ap_log_cerror(APLOG_MARK, APLOG_ERR, 0, session->c, APLOGNO(02922) "h2_session: stream(%ld-%d): DATA frame " "for unknown stream", session->id, (int)frame->hd.stream_id); rv = nghttp2_submit_rst_stream(ng2s, NGHTTP2_FLAG_NONE, frame->hd.stream_id, NGHTTP2_INTERNAL_ERROR); if (nghttp2_is_fatal(rv)) { return NGHTTP2_ERR_CALLBACK_FAILURE; } return 0; } break; } case NGHTTP2_PRIORITY: { ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, session->c, "h2_session: stream(%ld-%d): PRIORITY frame " " weight=%d, dependsOn=%d, exclusive=%d", session->id, (int)frame->hd.stream_id, frame->priority.pri_spec.weight, frame->priority.pri_spec.stream_id, frame->priority.pri_spec.exclusive); break; } default: if (APLOGctrace2(session->c)) { char buffer[256]; frame_print(frame, buffer, sizeof(buffer)/sizeof(buffer[0])); ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, session->c, "h2_session: on_frame_rcv %s", buffer); } break; } /* only DATA and HEADERS frame can bear END_STREAM flag. Other frame types may have other flag which has the same value, so we have to check the frame type first. */ if ((frame->hd.type == NGHTTP2_DATA || frame->hd.type == NGHTTP2_HEADERS) && frame->hd.flags & NGHTTP2_FLAG_END_STREAM) { h2_stream * stream = h2_stream_set_get(session->streams, frame->hd.stream_id); if (stream != NULL) { status = h2_stream_write_eos(stream); ap_log_cerror(APLOG_MARK, APLOG_DEBUG, status, session->c, "h2_stream(%ld-%d): input closed", session->id, (int)frame->hd.stream_id); } } if (status != APR_SUCCESS) { ap_log_cerror(APLOG_MARK, APLOG_ERR, status, session->c, APLOGNO(02923) "h2_session: stream(%ld-%d): error handling frame", session->id, (int)frame->hd.stream_id); rv = nghttp2_submit_rst_stream(ng2s, NGHTTP2_FLAG_NONE, frame->hd.stream_id, NGHTTP2_INTERNAL_ERROR); if (nghttp2_is_fatal(rv)) { return NGHTTP2_ERR_CALLBACK_FAILURE; } return 0; } return 0; }
static int on_frame_recv(nghttp2_session *session, const nghttp2_frame *frame, void *userp) { struct connectdata *conn = (struct connectdata *)userp; struct http_conn *c = &conn->proto.httpc; int rv; size_t left, ncopy; (void)session; (void)frame; infof(conn->data, "on_frame_recv() was called with header %x\n", frame->hd.type); switch(frame->hd.type) { case NGHTTP2_DATA: /* If body started, then receiving DATA is illegal. */ if(!c->bodystarted) { rv = nghttp2_submit_rst_stream(session, NGHTTP2_FLAG_NONE, frame->hd.stream_id, NGHTTP2_PROTOCOL_ERROR); if(nghttp2_is_fatal(rv)) { return NGHTTP2_ERR_CALLBACK_FAILURE; } } break; case NGHTTP2_HEADERS: if(frame->headers.cat == NGHTTP2_HCAT_REQUEST) break; if(c->bodystarted) { /* Only valid HEADERS after body started is trailer header, which is not fully supported in this code. If HEADERS is not trailer, then it is a PROTOCOL_ERROR. */ if((frame->hd.flags & NGHTTP2_FLAG_END_STREAM) == 0) { rv = nghttp2_submit_rst_stream(session, NGHTTP2_FLAG_NONE, frame->hd.stream_id, NGHTTP2_PROTOCOL_ERROR); if(nghttp2_is_fatal(rv)) { return NGHTTP2_ERR_CALLBACK_FAILURE; } } break; } if(c->status_code == -1) { /* No :status header field means PROTOCOL_ERROR. */ rv = nghttp2_submit_rst_stream(session, NGHTTP2_FLAG_NONE, frame->hd.stream_id, NGHTTP2_PROTOCOL_ERROR); if(nghttp2_is_fatal(rv)) { return NGHTTP2_ERR_CALLBACK_FAILURE; } break; } /* Only final status code signals the end of header */ if(c->status_code / 100 != 1) { c->bodystarted = TRUE; } c->status_code = -1; Curl_add_buffer(c->header_recvbuf, "\r\n", 2); left = c->header_recvbuf->size_used - c->nread_header_recvbuf; ncopy = c->len < left ? c->len : left; memcpy(c->mem, c->header_recvbuf->buffer + c->nread_header_recvbuf, ncopy); c->nread_header_recvbuf += ncopy; c->mem += ncopy; c->len -= ncopy; break; case NGHTTP2_PUSH_PROMISE: rv = nghttp2_submit_rst_stream(session, NGHTTP2_FLAG_NONE, frame->hd.stream_id, NGHTTP2_CANCEL); if(nghttp2_is_fatal(rv)) { return rv; } break; } return 0; }
/* frame->hd.type is either NGHTTP2_HEADERS or NGHTTP2_PUSH_PROMISE */ static int on_header(nghttp2_session *session, const nghttp2_frame *frame, const uint8_t *name, size_t namelen, const uint8_t *value, size_t valuelen, uint8_t flags, void *userp) { struct connectdata *conn = (struct connectdata *)userp; struct http_conn *c = &conn->proto.httpc; int rv; (void)session; (void)frame; (void)flags; if(frame->hd.stream_id != c->stream_id) { return 0; } if(c->bodystarted) { /* Ignore trailer or HEADERS not mapped to HTTP semantics. The consequence is handled in on_frame_recv(). */ return 0; } if(!nghttp2_check_header_name(name, namelen) || !nghttp2_check_header_value(value, valuelen)) { rv = nghttp2_submit_rst_stream(session, NGHTTP2_FLAG_NONE, frame->hd.stream_id, NGHTTP2_PROTOCOL_ERROR); if(nghttp2_is_fatal(rv)) { return NGHTTP2_ERR_CALLBACK_FAILURE; } return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; } if(namelen == sizeof(":status") - 1 && memcmp(STATUS, name, namelen) == 0) { /* :status must appear exactly once. */ if(c->status_code != -1 || (c->status_code = decode_status_code(value, valuelen)) == -1) { rv = nghttp2_submit_rst_stream(session, NGHTTP2_FLAG_NONE, frame->hd.stream_id, NGHTTP2_PROTOCOL_ERROR); if(nghttp2_is_fatal(rv)) { return NGHTTP2_ERR_CALLBACK_FAILURE; } return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; } Curl_add_buffer(c->header_recvbuf, "HTTP/2.0 ", 9); Curl_add_buffer(c->header_recvbuf, value, valuelen); Curl_add_buffer(c->header_recvbuf, "\r\n", 2); return 0; } else { /* Here we are sure that namelen > 0 because of nghttp2_check_header_name(). Pseudo header other than :status is illegal. */ if(c->status_code == -1 || name[0] == ':') { rv = nghttp2_submit_rst_stream(session, NGHTTP2_FLAG_NONE, frame->hd.stream_id, NGHTTP2_PROTOCOL_ERROR); if(nghttp2_is_fatal(rv)) { return NGHTTP2_ERR_CALLBACK_FAILURE; } return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; } /* convert to a HTTP1-style header */ infof(conn->data, "got header\n"); Curl_add_buffer(c->header_recvbuf, name, namelen); Curl_add_buffer(c->header_recvbuf, ":", 1); Curl_add_buffer(c->header_recvbuf, value, valuelen); Curl_add_buffer(c->header_recvbuf, "\r\n", 2); } return 0; /* 0 is successful */ }
apr_status_t h2_session_write(h2_session *session, apr_interval_time_t timeout) { apr_status_t status = APR_EAGAIN; h2_stream *stream = NULL; int flush_output = 0; AP_DEBUG_ASSERT(session); /* Check that any pending window updates are sent. */ status = h2_session_update_windows(session); if (status == APR_SUCCESS) { flush_output = 1; } else if (status != APR_EAGAIN) { return status; } if (h2_session_want_write(session)) { int rv; status = APR_SUCCESS; rv = nghttp2_session_send(session->ngh2); if (rv != 0) { ap_log_cerror( APLOG_MARK, APLOG_DEBUG, 0, session->c, "h2_session: send: %s", nghttp2_strerror(rv)); if (nghttp2_is_fatal(rv)) { h2_session_abort_int(session, rv); status = APR_ECONNABORTED; } } flush_output = 1; } /* If we have responses ready, submit them now. */ while ((stream = h2_mplx_next_submit(session->mplx, session->streams)) != NULL) { status = h2_session_handle_response(session, stream); flush_output = 1; } if (h2_session_resume_streams_with_data(session) > 0) { flush_output = 1; } if (!flush_output && timeout > 0 && !h2_session_want_write(session)) { status = h2_mplx_out_trywait(session->mplx, timeout, session->iowait); if (status != APR_TIMEUP && h2_session_resume_streams_with_data(session) > 0) { flush_output = 1; } else { /* nothing happened to ongoing streams, do some house-keeping */ } } if (h2_session_want_write(session)) { int rv; status = APR_SUCCESS; rv = nghttp2_session_send(session->ngh2); if (rv != 0) { ap_log_cerror( APLOG_MARK, APLOG_DEBUG, 0, session->c, "h2_session: send2: %s", nghttp2_strerror(rv)); if (nghttp2_is_fatal(rv)) { h2_session_abort_int(session, rv); status = APR_ECONNABORTED; } } flush_output = 1; } if (flush_output) { h2_conn_io_flush(&session->io); } return status; }
/* return number of received (decrypted) bytes */ static ssize_t http2_send(struct connectdata *conn, int sockindex, const void *mem, size_t len, CURLcode *err) { /* * BIG TODO: Currently, we send request in this function, but this * function is also used to send request body. It would be nice to * add dedicated function for request. */ int rv; struct http_conn *httpc = &conn->proto.httpc; nghttp2_nv *nva; size_t nheader; size_t i; char *hdbuf = (char*)mem; char *end; nghttp2_data_provider data_prd; int32_t stream_id; (void)sockindex; infof(conn->data, "http2_send len=%zu\n", len); if(httpc->stream_id != -1) { /* If stream_id != -1, we have dispatched request HEADERS, and now are going to send or sending request body in DATA frame */ httpc->upload_mem = mem; httpc->upload_len = len; nghttp2_session_resume_data(httpc->h2, httpc->stream_id); rv = nghttp2_session_send(httpc->h2); if(nghttp2_is_fatal(rv)) { *err = CURLE_SEND_ERROR; return -1; } return len - httpc->upload_len; } /* Calculate number of headers contained in [mem, mem + len) */ /* Here, we assume the curl http code generate *correct* HTTP header field block */ nheader = 0; for(i = 0; i < len; ++i) { if(hdbuf[i] == 0x0a) { ++nheader; } } /* We counted additional 2 \n in the first and last line. We need 3 new headers: :method, :path and :scheme. Therefore we need one more space. */ nheader += 1; nva = malloc(sizeof(nghttp2_nv) * nheader); if(nva == NULL) { *err = CURLE_OUT_OF_MEMORY; return -1; } /* Extract :method, :path from request line */ end = strchr(hdbuf, ' '); nva[0].name = (unsigned char *)":method"; nva[0].namelen = (uint16_t)strlen((char *)nva[0].name); nva[0].value = (unsigned char *)hdbuf; nva[0].valuelen = (uint16_t)(end - hdbuf); nva[0].flags = NGHTTP2_NV_FLAG_NONE; hdbuf = end + 1; end = strchr(hdbuf, ' '); nva[1].name = (unsigned char *)":path"; nva[1].namelen = (uint16_t)strlen((char *)nva[1].name); nva[1].value = (unsigned char *)hdbuf; nva[1].valuelen = (uint16_t)(end - hdbuf); nva[1].flags = NGHTTP2_NV_FLAG_NONE; nva[2].name = (unsigned char *)":scheme"; nva[2].namelen = (uint16_t)strlen((char *)nva[2].name); if(conn->handler->flags & PROTOPT_SSL) nva[2].value = (unsigned char *)"https"; else nva[2].value = (unsigned char *)"http"; nva[2].valuelen = (uint16_t)strlen((char *)nva[2].value); nva[2].flags = NGHTTP2_NV_FLAG_NONE; hdbuf = strchr(hdbuf, 0x0a); ++hdbuf; for(i = 3; i < nheader; ++i) { end = strchr(hdbuf, ':'); assert(end); if(end - hdbuf == 4 && Curl_raw_nequal("host", hdbuf, 4)) { nva[i].name = (unsigned char *)":authority"; nva[i].namelen = (uint16_t)strlen((char *)nva[i].name); } else { nva[i].name = (unsigned char *)hdbuf; nva[i].namelen = (uint16_t)(end - hdbuf); } hdbuf = end + 1; for(; *hdbuf == ' '; ++hdbuf); end = strchr(hdbuf, 0x0d); assert(end); nva[i].value = (unsigned char *)hdbuf; nva[i].valuelen = (uint16_t)(end - hdbuf); nva[i].flags = NGHTTP2_NV_FLAG_NONE; hdbuf = end + 2; /* Inspect Content-Length header field and retrieve the request entity length so that we can set END_STREAM to the last DATA frame. */ if(nva[i].namelen == 14 && Curl_raw_nequal("content-length", (char*)nva[i].name, 14)) { size_t j; for(j = 0; j < nva[i].valuelen; ++j) { httpc->upload_left *= 10; httpc->upload_left += nva[i].value[j] - '0'; } infof(conn->data, "request content-length=%zu\n", httpc->upload_left); } } switch(conn->data->set.httpreq) { case HTTPREQ_POST: case HTTPREQ_POST_FORM: case HTTPREQ_PUT: data_prd.read_callback = data_source_read_callback; data_prd.source.ptr = NULL; stream_id = nghttp2_submit_request(httpc->h2, NULL, nva, nheader, &data_prd, NULL); break; default: stream_id = nghttp2_submit_request(httpc->h2, NULL, nva, nheader, NULL, NULL); } Curl_safefree(nva); if(stream_id < 0) { *err = CURLE_SEND_ERROR; return -1; } httpc->stream_id = stream_id; rv = nghttp2_session_send(httpc->h2); if(rv != 0) { *err = CURLE_SEND_ERROR; return -1; } if(httpc->stream_id != -1) { /* If whole HEADERS frame was sent off to the underlying socket, the nghttp2 library calls data_source_read_callback. But only it found that no data available, so it deferred the DATA transmission. Which means that nghttp2_session_want_write() returns 0 on http2_perform_getsock(), which results that no writable socket check is performed. To workaround this, we issue nghttp2_session_resume_data() here to bring back DATA transmission from deferred state. */ nghttp2_session_resume_data(httpc->h2, httpc->stream_id); } return len; }
static int on_frame_recv(nghttp2_session *session, const nghttp2_frame *frame, void *userp) { struct connectdata *conn = (struct connectdata *)userp; struct http_conn *httpc = NULL; struct SessionHandle *data_s = NULL; struct HTTP *stream = NULL; static int lastStream = -1; int rv; size_t left, ncopy; int32_t stream_id = frame->hd.stream_id; if(!stream_id) { /* stream ID zero is for connection-oriented stuff */ return 0; } data_s = nghttp2_session_get_stream_user_data(session, frame->hd.stream_id); if(lastStream != frame->hd.stream_id) { lastStream = frame->hd.stream_id; } if(!data_s) { DEBUGF(infof(conn->data, "No SessionHandle associated with stream: %x\n", stream_id)); return 0; } stream = data_s->req.protop; if(!stream) return NGHTTP2_ERR_CALLBACK_FAILURE; DEBUGF(infof(data_s, "on_frame_recv() header %x stream %x\n", frame->hd.type, stream_id)); conn = data_s->easy_conn; assert(conn); assert(conn->data == data_s); httpc = &conn->proto.httpc; switch(frame->hd.type) { case NGHTTP2_DATA: /* If body started on this stream, then receiving DATA is illegal. */ if(!stream->bodystarted) { rv = nghttp2_submit_rst_stream(session, NGHTTP2_FLAG_NONE, stream_id, NGHTTP2_PROTOCOL_ERROR); if(nghttp2_is_fatal(rv)) { return NGHTTP2_ERR_CALLBACK_FAILURE; } } break; case NGHTTP2_HEADERS: if(frame->headers.cat == NGHTTP2_HCAT_REQUEST) break; if(stream->bodystarted) { /* Only valid HEADERS after body started is trailer HEADERS. We ignores trailer HEADERS for now. nghttp2 guarantees that it has END_STREAM flag set. */ break; } /* nghttp2 guarantees that :status is received, and we store it to stream->status_code */ DEBUGASSERT(stream->status_code != -1); /* Only final status code signals the end of header */ if(stream->status_code / 100 != 1) { stream->bodystarted = TRUE; stream->status_code = -1; } Curl_add_buffer(stream->header_recvbuf, "\r\n", 2); left = stream->header_recvbuf->size_used - stream->nread_header_recvbuf; ncopy = MIN(stream->len, left); memcpy(&stream->mem[stream->memlen], stream->header_recvbuf->buffer + stream->nread_header_recvbuf, ncopy); stream->nread_header_recvbuf += ncopy; DEBUGF(infof(data_s, "Store %zu bytes headers from stream %u at %p\n", ncopy, stream_id, stream->mem)); stream->len -= ncopy; stream->memlen += ncopy; data_s->state.drain++; { /* get the pointer from userp again since it was re-assigned above */ struct connectdata *conn_s = (struct connectdata *)userp; /* if we receive data for another handle, wake that up */ if(conn_s->data != data_s) Curl_expire(data_s, 1); } break; case NGHTTP2_PUSH_PROMISE: rv = push_promise(data_s, conn, &frame->push_promise); if(rv) { /* deny! */ rv = nghttp2_submit_rst_stream(session, NGHTTP2_FLAG_NONE, frame->push_promise.promised_stream_id, NGHTTP2_CANCEL); if(nghttp2_is_fatal(rv)) { return rv; } } break; case NGHTTP2_SETTINGS: { uint32_t max_conn = httpc->settings.max_concurrent_streams; DEBUGF(infof(conn->data, "Got SETTINGS for stream %u!\n", stream_id)); httpc->settings.max_concurrent_streams = nghttp2_session_get_remote_settings( session, NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS); httpc->settings.enable_push = nghttp2_session_get_remote_settings( session, NGHTTP2_SETTINGS_ENABLE_PUSH); DEBUGF(infof(conn->data, "MAX_CONCURRENT_STREAMS == %d\n", httpc->settings.max_concurrent_streams)); DEBUGF(infof(conn->data, "ENABLE_PUSH == %s\n", httpc->settings.enable_push?"TRUE":"false")); if(max_conn != httpc->settings.max_concurrent_streams) { /* only signal change if the value actually changed */ infof(conn->data, "Connection state changed (MAX_CONCURRENT_STREAMS updated)!\n"); Curl_multi_connchanged(conn->data->multi); } } break; default: DEBUGF(infof(conn->data, "Got frame type %x for stream %u!\n", frame->hd.type, stream_id)); break; } return 0; }