blob: 65b2b7b625127cf9a10c46f980de0d9689d0541d [file] [log] [blame]
/* Copyright 2015 greenbytes GmbH (https://www.greenbytes.de)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <assert.h>
#include <stdio.h>
#include <apr_strings.h>
#include <apr_lib.h>
#include <httpd.h>
#include <http_core.h>
#include <http_log.h>
#include "h2_private.h"
#include "h2_h2.h"
#include "h2_util.h"
#include "h2_push.h"
#include "h2_request.h"
#include "h2_response.h"
typedef struct {
const h2_request *req;
apr_pool_t *pool;
apr_array_header_t *pushes;
const char *s;
size_t slen;
size_t i;
const char *link;
apr_table_t *params;
char b[4096];
} link_ctx;
static int attr_char(char c)
{
switch (c) {
case '!':
case '#':
case '$':
case '&':
case '+':
case '-':
case '.':
case '^':
case '_':
case '`':
case '|':
case '~':
return 1;
default:
return apr_isalnum(c);
}
}
static int ptoken_char(char c)
{
switch (c) {
case '!':
case '#':
case '$':
case '&':
case '\'':
case '(':
case ')':
case '*':
case '+':
case '-':
case '.':
case '/':
case ':':
case '<':
case '=':
case '>':
case '?':
case '@':
case '[':
case ']':
case '^':
case '_':
case '`':
case '{':
case '|':
case '}':
case '~':
return 1;
default:
return apr_isalnum(c);
}
}
static int skip_ws(link_ctx *ctx)
{
char c;
while (ctx->i < ctx->slen
&& (((c = ctx->s[ctx->i]) == ' ') || (c == '\t'))) {
++ctx->i;
}
return (ctx->i < ctx->slen);
}
static int find_chr(link_ctx *ctx, char c, size_t *pidx)
{
size_t j;
for (j = ctx->i; j < ctx->slen; ++j) {
if (ctx->s[j] == c) {
*pidx = j;
return 1;
}
}
return 0;
}
static int read_chr(link_ctx *ctx, char c)
{
if (ctx->i < ctx->slen && ctx->s[ctx->i] == c) {
++ctx->i;
return 1;
}
return 0;
}
static char *mk_str(link_ctx *ctx, size_t end)
{
if (ctx->i < end) {
return apr_pstrndup(ctx->pool, ctx->s + ctx->i, end - ctx->i);
}
return "";
}
static int read_qstring(link_ctx *ctx, char **ps)
{
if (skip_ws(ctx) && read_chr(ctx, '\"')) {
size_t end;
if (find_chr(ctx, '\"', &end)) {
*ps = mk_str(ctx, end);
ctx->i = end + 1;
return 1;
}
}
return 0;
}
static int read_ptoken(link_ctx *ctx, char **ps)
{
if (skip_ws(ctx)) {
size_t i;
for (i = ctx->i; i < ctx->slen && ptoken_char(ctx->s[i]); ++i) {
/* nop */
}
if (i > ctx->i) {
*ps = mk_str(ctx, i);
ctx->i = i;
return 1;
}
}
return 0;
}
static int read_link(link_ctx *ctx)
{
if (skip_ws(ctx) && read_chr(ctx, '<')) {
size_t end;
if (find_chr(ctx, '>', &end)) {
ctx->link = mk_str(ctx, end);
ctx->i = end + 1;
return 1;
}
}
return 0;
}
static int read_pname(link_ctx *ctx, char **pname)
{
if (skip_ws(ctx)) {
size_t i;
for (i = ctx->i; i < ctx->slen && attr_char(ctx->s[i]); ++i) {
/* nop */
}
if (i > ctx->i) {
*pname = mk_str(ctx, i);
ctx->i = i;
return 1;
}
}
return 0;
}
static int read_pvalue(link_ctx *ctx, char **pvalue)
{
if (skip_ws(ctx) && read_chr(ctx, '=')) {
if (read_qstring(ctx, pvalue) || read_ptoken(ctx, pvalue)) {
return 1;
}
}
return 0;
}
static int read_param(link_ctx *ctx)
{
if (skip_ws(ctx) && read_chr(ctx, ';')) {
char *name, *value = "";
if (read_pname(ctx, &name)) {
read_pvalue(ctx, &value); /* value is optional */
apr_table_setn(ctx->params, name, value);
return 1;
}
}
return 0;
}
static int read_sep(link_ctx *ctx)
{
if (skip_ws(ctx) && read_chr(ctx, ',')) {
return 1;
}
return 0;
}
static void init_params(link_ctx *ctx)
{
if (!ctx->params) {
ctx->params = apr_table_make(ctx->pool, 5);
}
else {
apr_table_clear(ctx->params);
}
}
static int same_authority(const h2_request *req, const apr_uri_t *uri)
{
if (uri->scheme != NULL && strcmp(uri->scheme, req->scheme)) {
return 0;
}
if (uri->hostinfo != NULL && strcmp(uri->hostinfo, req->authority)) {
return 0;
}
return 1;
}
static int set_header(void *ctx, const char *key, const char *value)
{
apr_table_setn(ctx, key, value);
return 1;
}
static int add_push(link_ctx *ctx)
{
/* so, we have read a Link header and need to decide
* if we transform it into a push.
*/
const char *rel = apr_table_get(ctx->params, "rel");
if (rel && !strcmp("preload", rel)) {
apr_uri_t uri;
if (apr_uri_parse(ctx->pool, ctx->link, &uri) == APR_SUCCESS) {
if (uri.path && same_authority(ctx->req, &uri)) {
char *path;
apr_table_t *headers;
h2_request *req;
h2_push *push;
/* We only want to generate pushes for resources in the
* same authority than the original request.
* icing: i think that is wise, otherwise we really need to
* check that the vhost/server is available and uses the same
* TLS (if any) parameters.
*/
path = apr_uri_unparse(ctx->pool, &uri, APR_URI_UNP_OMITSITEPART);
push = apr_pcalloc(ctx->pool, sizeof(*push));
push->initiating_id = ctx->req->id;
headers = apr_table_make(ctx->pool, 5);
apr_table_do(set_header, headers, ctx->req->headers,
"User-Agent",
"Cache-Control",
"Accept-Language",
NULL);
/* TODO: which headers do we add here?
*/
req = h2_request_createn(0, ctx->pool,
ctx->req->method,
ctx->req->scheme,
ctx->req->authority,
path, headers);
h2_request_end_headers(req, ctx->pool, 1);
push->req = req;
push->prio.dependency = H2_DEPENDANT_AFTER;
push->prio.weight = NGHTTP2_DEFAULT_WEIGHT;
if (!ctx->pushes) {
ctx->pushes = apr_array_make(ctx->pool, 5, sizeof(h2_push*));
}
APR_ARRAY_PUSH(ctx->pushes, h2_push*) = push;
}
}
}
return 0;
}
static void inspect_link(link_ctx *ctx, const char *s, size_t slen)
{
/* RFC 5988 <https://tools.ietf.org/html/rfc5988#section-6.2.1>
Link = "Link" ":" #link-value
link-value = "<" URI-Reference ">" *( ";" link-param )
link-param = ( ( "rel" "=" relation-types )
| ( "anchor" "=" <"> URI-Reference <"> )
| ( "rev" "=" relation-types )
| ( "hreflang" "=" Language-Tag )
| ( "media" "=" ( MediaDesc | ( <"> MediaDesc <"> ) ) )
| ( "title" "=" quoted-string )
| ( "title*" "=" ext-value )
| ( "type" "=" ( media-type | quoted-mt ) )
| ( link-extension ) )
link-extension = ( parmname [ "=" ( ptoken | quoted-string ) ] )
| ( ext-name-star "=" ext-value )
ext-name-star = parmname "*" ; reserved for RFC2231-profiled
; extensions. Whitespace NOT
; allowed in between.
ptoken = 1*ptokenchar
ptokenchar = "!" | "#" | "$" | "%" | "&" | "'" | "("
| ")" | "*" | "+" | "-" | "." | "/" | DIGIT
| ":" | "<" | "=" | ">" | "?" | "@" | ALPHA
| "[" | "]" | "^" | "_" | "`" | "{" | "|"
| "}" | "~"
media-type = type-name "/" subtype-name
quoted-mt = <"> media-type <">
relation-types = relation-type
| <"> relation-type *( 1*SP relation-type ) <">
relation-type = reg-rel-type | ext-rel-type
reg-rel-type = LOALPHA *( LOALPHA | DIGIT | "." | "-" )
ext-rel-type = URI
and from <https://tools.ietf.org/html/rfc5987>
parmname = 1*attr-char
attr-char = ALPHA / DIGIT
/ "!" / "#" / "$" / "&" / "+" / "-" / "."
/ "^" / "_" / "`" / "|" / "~"
*/
ctx->s = s;
ctx->slen = slen;
ctx->i = 0;
while (read_link(ctx)) {
init_params(ctx);
while (read_param(ctx)) {
/* nop */
}
add_push(ctx);
if (!read_sep(ctx)) {
break;
}
}
}
static int head_iter(void *ctx, const char *key, const char *value)
{
if (!apr_strnatcasecmp("link", key)) {
inspect_link(ctx, value, strlen(value));
}
return 1;
}
apr_array_header_t *h2_push_collect(apr_pool_t *p, const h2_request *req,
const h2_response *res)
{
/* Collect push candidates from the request/response pair.
*
* One source for pushes are "rel=preload" link headers
* in the response.
*
* TODO: This may be extended in the future by hooks or callbacks
* where other modules can provide push information directly.
*/
if (res->headers) {
link_ctx ctx;
memset(&ctx, 0, sizeof(ctx));
ctx.req = req;
ctx.pool = p;
apr_table_do(head_iter, &ctx, res->headers, NULL);
return ctx.pushes;
}
return NULL;
}