All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.servicetalk.http.netty.NettyH2HeadersToHttpHeaders Maven / Gradle / Ivy

The newest version!
/*
 * Copyright © 2019, 2021 Apple Inc. and the ServiceTalk project authors
 *
 * 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.
 */
package io.servicetalk.http.netty;

import io.servicetalk.http.api.HeaderUtils;
import io.servicetalk.http.api.HttpCookiePair;
import io.servicetalk.http.api.HttpHeaders;
import io.servicetalk.http.api.HttpSetCookie;
import io.servicetalk.utils.internal.IllegalCharacterException;

import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http2.Http2Headers;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map.Entry;
import java.util.NoSuchElementException;
import java.util.Set;
import javax.annotation.Nullable;

import static io.servicetalk.buffer.api.CharSequences.contentEquals;
import static io.servicetalk.buffer.api.CharSequences.contentEqualsIgnoreCase;
import static io.servicetalk.buffer.api.CharSequences.forEachByte;
import static io.servicetalk.http.api.DefaultHttpSetCookie.parseSetCookie;
import static io.servicetalk.http.api.HeaderUtils.DEFAULT_HEADER_FILTER;
import static io.servicetalk.http.api.HeaderUtils.domainMatches;
import static io.servicetalk.http.api.HeaderUtils.isSetCookieNameMatches;
import static io.servicetalk.http.api.HeaderUtils.parseCookiePair;
import static io.servicetalk.http.api.HeaderUtils.pathMatches;
import static io.servicetalk.http.api.HeaderUtils.removeCookiePairs;
import static io.servicetalk.http.api.HttpHeaderNames.COOKIE;
import static io.servicetalk.http.api.HttpHeaderNames.SET_COOKIE;
import static java.util.Collections.emptyIterator;

final class NettyH2HeadersToHttpHeaders implements HttpHeaders {

    // ASCII symbols:
    private static final byte HT = 9;
    private static final byte DEL = 127;
    private static final byte CONTROL_CHARS_MASK = (byte) 0xE0;

    private final Http2Headers nettyHeaders;
    private final boolean validateCookies;
    private final boolean validateValues;

    NettyH2HeadersToHttpHeaders(final Http2Headers nettyHeaders, final boolean validateCookies,
                                final boolean validateValues) {
        this.nettyHeaders = nettyHeaders;
        this.validateCookies = validateCookies;
        this.validateValues = validateValues;
    }

    Http2Headers nettyHeaders() {
        return nettyHeaders;
    }

    @Nullable
    @Override
    public CharSequence get(final CharSequence name) {
        return nettyHeaders.get(name);
    }

    @Nullable
    @Override
    public CharSequence getAndRemove(final CharSequence name) {
        return nettyHeaders.getAndRemove(name);
    }

    @Override
    public Iterator valuesIterator(final CharSequence name) {
        return nettyHeaders.valueIterator(name);
    }

    @Override
    public boolean contains(final CharSequence name, final CharSequence value) {
        return nettyHeaders.contains(name, value);
    }

    @Override
    public boolean containsIgnoreCase(final CharSequence name, final CharSequence value) {
        return nettyHeaders.contains(name, value, true);
    }

    @Override
    public int size() {
        return nettyHeaders.size();
    }

    @Override
    public boolean isEmpty() {
        return nettyHeaders.isEmpty();
    }

    @Override
    public Set names() {
        return nettyHeaders.names();
    }

    @Override
    public HttpHeaders add(final CharSequence name, final CharSequence value) {
        nettyHeaders.add(name, validateHeaderValue(value));
        return this;
    }

    @Override
    public HttpHeaders add(final CharSequence name, final Iterable values) {
        nettyHeaders.add(name, validateHeaderValue(values));
        return this;
    }

    @Override
    public HttpHeaders add(final CharSequence name, final CharSequence... values) {
        nettyHeaders.add(name, validateHeaderValue(values));
        return this;
    }

    @Override
    public HttpHeaders add(final HttpHeaders headers) {
        for (Entry entry : headers) {
            nettyHeaders.add(entry.getKey(), validateHeaderValue(entry.getValue()));
        }
        return this;
    }

    @Override
    public HttpHeaders set(final CharSequence name, final CharSequence value) {
        nettyHeaders.set(name, validateHeaderValue(value));
        return this;
    }

    @Override
    public HttpHeaders set(final CharSequence name, final Iterable values) {
        nettyHeaders.set(name, validateHeaderValue(values));
        return this;
    }

    @Override
    public HttpHeaders set(final CharSequence name, final CharSequence... values) {
        nettyHeaders.set(name, validateHeaderValue(values));
        return this;
    }

    @Override
    public boolean remove(final CharSequence name) {
        return nettyHeaders.remove(name);
    }

    @Override
    public boolean remove(final CharSequence name, final CharSequence value) {
        final int sizeBefore = size();
        Iterator valuesItr = nettyHeaders.valueIterator(name);
        while (valuesItr.hasNext()) {
            CharSequence next = valuesItr.next();
            if (contentEquals(next, value)) {
                valuesItr.remove();
            }
        }
        return sizeBefore != size();
    }

    @Override
    public boolean removeIgnoreCase(final CharSequence name, final CharSequence value) {
        final int sizeBefore = size();
        Iterator valuesItr = nettyHeaders.valueIterator(name);
        while (valuesItr.hasNext()) {
            CharSequence next = valuesItr.next();
            if (contentEqualsIgnoreCase(next, value)) {
                valuesItr.remove();
            }
        }
        return sizeBefore != size();
    }

    @Override
    public HttpHeaders clear() {
        nettyHeaders.clear();
        return this;
    }

    @Override
    public Iterator> iterator() {
        return nettyHeaders.iterator();
    }

    @Override
    public String toString() {
        return toString(DEFAULT_HEADER_FILTER);
    }

    @Nullable
    @Override
    public HttpCookiePair getCookie(final CharSequence name) {
        Iterator valueItr = nettyHeaders.valueIterator(HttpHeaderNames.COOKIE);
        while (valueItr.hasNext()) {
            HttpCookiePair cookiePair = parseCookiePair(valueItr.next(), name);
            if (cookiePair != null) {
                return cookiePair;
            }
        }
        return null;
    }

    @Nullable
    @Override
    public HttpSetCookie getSetCookie(final CharSequence name) {
        Iterator valueItr = nettyHeaders.valueIterator(HttpHeaderNames.SET_COOKIE);
        while (valueItr.hasNext()) {
            HttpSetCookie setCookie = HeaderUtils.parseSetCookie(valueItr.next(), name, validateCookies);
            if (setCookie != null) {
                return setCookie;
            }
        }
        return null;
    }

    @Override
    public Iterator getCookiesIterator() {
        Iterator valueItr = nettyHeaders.valueIterator(HttpHeaderNames.COOKIE);
        return valueItr.hasNext() ? new CookiesIterator(valueItr) : emptyIterator();
    }

    @Override
    public Iterator getCookiesIterator(final CharSequence name) {
        return new CookiesByNameIterator(nettyHeaders.valueIterator(HttpHeaderNames.COOKIE), name);
    }

    @Override
    public Iterator getSetCookiesIterator() {
        Iterator valueItr = nettyHeaders.valueIterator(HttpHeaderNames.SET_COOKIE);
        return valueItr.hasNext() ? new SetCookiesIterator(valueItr) : emptyIterator();
    }

    @Override
    public Iterator getSetCookiesIterator(final CharSequence name) {
        Iterator valueItr = nettyHeaders.valueIterator(HttpHeaderNames.SET_COOKIE);
        while (valueItr.hasNext()) {
            HttpSetCookie setCookie = HeaderUtils.parseSetCookie(valueItr.next(), name, validateCookies);
            if (setCookie != null) {
                return new SetCookiesByNameIterator(valueItr, setCookie);
            }
        }
        return emptyIterator();
    }

    @Override
    public Iterator getSetCookiesIterator(final CharSequence name, final CharSequence domain,
                                                                   final CharSequence path) {
        Iterator valueItr = nettyHeaders.valueIterator(HttpHeaderNames.SET_COOKIE);
        while (valueItr.hasNext()) {
            // In the future we could attempt to delay full parsing of the cookie until after the domain/path have
            // been matched, but for simplicity just do the parsing ahead of time.
            HttpSetCookie setCookie = HeaderUtils.parseSetCookie(valueItr.next(), name, validateCookies);
            if (setCookie != null && domainMatches(domain, setCookie.domain()) &&
                    pathMatches(path, setCookie.path())) {
                return new SetCookiesByNameDomainPathIterator(valueItr, setCookie, domain, path);
            }
        }
        return emptyIterator();
    }

    @Override
    public HttpHeaders addCookie(final HttpCookiePair cookie) {
        add(COOKIE, cookie.encoded());
        return this;
    }

    @Override
    public HttpHeaders addSetCookie(final HttpSetCookie cookie) {
        add(SET_COOKIE, cookie.encoded());
        return this;
    }

    @Override
    public boolean removeCookies(final CharSequence name) {
        Iterator valuesItr = nettyHeaders.valueIterator(HttpHeaderNames.COOKIE);
        List cookiesToAdd = null;
        final int sizeBefore = size();
        while (valuesItr.hasNext()) {
            CharSequence newHeaderValue = removeCookiePairs(valuesItr.next(), name);
            if (newHeaderValue != null) {
                if (newHeaderValue.length() != 0) {
                    if (cookiesToAdd == null) {
                        cookiesToAdd = new ArrayList<>(4);
                    }
                    cookiesToAdd.add(newHeaderValue);
                }
                valuesItr.remove();
            }
        }
        if (cookiesToAdd != null) {
            for (CharSequence cookies : cookiesToAdd) {
                nettyHeaders.add(HttpHeaderNames.COOKIE, cookies);
            }
            return true;
        }
        return sizeBefore != size();
    }

    @Override
    public boolean removeSetCookies(final CharSequence name) {
        final int sizeBefore = size();
        Iterator valueItr = nettyHeaders.valueIterator(HttpHeaderNames.SET_COOKIE);
        while (valueItr.hasNext()) {
            if (isSetCookieNameMatches(valueItr.next(), name)) {
                valueItr.remove();
            }
        }
        return sizeBefore != size();
    }

    @Override
    public boolean removeSetCookies(final CharSequence name, final CharSequence domain, final CharSequence path) {
        final int sizeBefore = size();
        Iterator valueItr = nettyHeaders.valueIterator(HttpHeaderNames.SET_COOKIE);
        while (valueItr.hasNext()) {
            // In the future we could attempt to delay full parsing of the cookie until after the domain/path have
            // been matched, but for simplicity just do the parsing ahead of time.
            HttpSetCookie setCookie = HeaderUtils.parseSetCookie(valueItr.next(), name, false);
            if (setCookie != null && domainMatches(domain, setCookie.domain()) &&
                    pathMatches(path, setCookie.path())) {
                valueItr.remove();
            }
        }
        return sizeBefore != size();
    }

    @SuppressWarnings("ClassNameSameAsAncestorName")
    private static final class CookiesIterator extends HeaderUtils.CookiesIterator {
        private final Iterator valueItr;
        @Nullable
        private CharSequence headerValue;

        CookiesIterator(final Iterator valueItr) {
            this.valueItr = valueItr;
            if (valueItr.hasNext()) {
                headerValue = valueItr.next();
                initNext(headerValue);
            }
        }

        @Nullable
        @Override
        protected CharSequence cookieHeaderValue() {
            return headerValue;
        }

        @Override
        protected void advanceCookieHeaderValue() {
            headerValue = valueItr.hasNext() ? valueItr.next() : null;
        }
    }

    @SuppressWarnings("ClassNameSameAsAncestorName")
    private static final class CookiesByNameIterator extends HeaderUtils.CookiesByNameIterator {
        private final Iterator valueItr;
        @Nullable
        private CharSequence headerValue;

        CookiesByNameIterator(final Iterator valueItr, final CharSequence name) {
            super(name);
            this.valueItr = valueItr;
            if (valueItr.hasNext()) {
                headerValue = valueItr.next();
                initNext(headerValue);
            }
        }

        @Nullable
        @Override
        protected CharSequence cookieHeaderValue() {
            return headerValue;
        }

        @Override
        protected void advanceCookieHeaderValue() {
            headerValue = valueItr.hasNext() ? valueItr.next() : null;
        }
    }

    private final class SetCookiesIterator implements Iterator {
        private final Iterator valueItr;

         SetCookiesIterator(final Iterator valueItr) {
            this.valueItr = valueItr;
        }

        @Override
        public boolean hasNext() {
            return valueItr.hasNext();
        }

        @Override
        public HttpSetCookie next() {
            return parseSetCookie(valueItr.next(), validateCookies);
        }

        @Override
        public void remove() {
            valueItr.remove();
        }
    }

    private final class SetCookiesByNameIterator implements Iterator {
        private final Iterator valueItr;
        @Nullable
        private HttpSetCookie next;

        SetCookiesByNameIterator(final Iterator valueItr, final HttpSetCookie next) {
            this.valueItr = valueItr;
            this.next = next;
        }

        @Override
        public boolean hasNext() {
            return next != null;
        }

        @Override
        public HttpSetCookie next() {
            if (next == null) {
                throw new NoSuchElementException();
            }
            HttpSetCookie currentCookie = next;
            next = null;
            while (valueItr.hasNext()) {
                next = HeaderUtils.parseSetCookie(valueItr.next(), currentCookie.name(), validateCookies);
                if (next != null) {
                    break;
                }
            }
            return currentCookie;
        }

        @Override
        public void remove() {
            valueItr.remove();
        }
    }

    private final class SetCookiesByNameDomainPathIterator implements Iterator {
        private final Iterator valueItr;
        private final CharSequence domain;
        private final CharSequence path;
        @Nullable
        private HttpSetCookie next;

        SetCookiesByNameDomainPathIterator(final Iterator valueItr,
                                           final HttpSetCookie next, final CharSequence domain,
                                           final CharSequence path) {
            this.valueItr = valueItr;
            this.domain = domain;
            this.path = path;
            this.next = next;
        }

        @Override
        public boolean hasNext() {
            return next != null;
        }

        @Override
        public HttpSetCookie next() {
            if (next == null) {
                throw new NoSuchElementException();
            }
            HttpSetCookie currentCookie = next;
            next = null;
            while (valueItr.hasNext()) {
                // In the future we could attempt to delay full parsing of the cookie until after the domain/path have
                // been matched, but for simplicity just do the parsing ahead of time.
                HttpSetCookie setCookie = HeaderUtils.parseSetCookie(valueItr.next(), currentCookie.name(),
                        validateCookies);
                if (setCookie != null && domainMatches(domain, setCookie.domain()) &&
                        pathMatches(path, setCookie.path())) {
                    next = setCookie;
                    break;
                }
            }
            return currentCookie;
        }

        @Override
        public void remove() {
            valueItr.remove();
        }
    }

    private CharSequence validateHeaderValue(final CharSequence value) {
        if (validateValues) {
            forEachByte(value, NettyH2HeadersToHttpHeaders::validateHeaderValue);
        }
        return value;
    }

    private Iterable validateHeaderValue(final Iterable values) {
        if (validateValues) {
            for (CharSequence v : values) {
                forEachByte(v, NettyH2HeadersToHttpHeaders::validateHeaderValue);
            }
        }
        return values;
    }

    private CharSequence[] validateHeaderValue(final CharSequence... values) {
        if (validateValues) {
            for (CharSequence v : values) {
                forEachByte(v, NettyH2HeadersToHttpHeaders::validateHeaderValue);
            }
        }
        return values;
    }

    /**
     * Validate char is a valid field-value character.
     *
     * @param value the character to validate.
     */
    private static boolean validateHeaderValue(final byte value) {
        // HEADER
        // header-field   = field-name ":" OWS field-value OWS
        //
        // field-value    = *( field-content / obs-fold )
        // field-content  = field-vchar [ 1*( SP / HTAB ) field-vchar ]
        // field-vchar    = VCHAR / obs-text
        //
        // obs-fold       = CRLF 1*( SP / HTAB )
        //                ; obsolete line folding
        //                ; see Section 3.2.4
        //
        // Note: we do not support obs-fold.
        // Illegal chars are control chars (0-31) except HT (9), and DEL (127):
        if (((value & CONTROL_CHARS_MASK) == 0 && value != HT) || value == DEL) {
            throw new IllegalCharacterException(value,
                    "(VCHAR / obs-text) [ 1*(SP / HTAB) (VCHAR / obs-text) ]");
        }
        return true;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy