/*
 * Pound - the reverse-proxy load-balancer
 * Copyright (C) 2002-2020 Apsis GmbH
 *
 * This file is part of Pound.
 *
 * Pound is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * Pound is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/> .
 *
 * Contact information:
 * Apsis GmbH
 * P.O.Box
 * 8707 Uetikon am See
 * Switzerland
 * EMail: roseg@apsis.ch
 */

#include    "pound.h"

/* frame types */
#define F_DATA      0x0
#define F_HEADERS   0x1
#define F_PRIORITY  0x2
#define F_RST       0x3
#define F_SETTINGS  0x4
#define F_PUSH      0x5
#define F_PING      0x6
#define F_GOAWAY    0x7
#define F_WINUPD    0x8
#define F_CONT      0x9

/* error flags */
#define E_NO_ERROR              0x0
#define E_PROTOCOL_ERROR        0x1
#define E_INTERNAL_ERROR        0x2
#define E_FLOW_CONTROL_ERROR    0x3
#define E_SETTINGS_TIMEOUT      0x4
#define E_STREAM_CLOSED         0x5
#define E_FRAME_SIZE_ERROR      0x6
#define E_REFUSED_STREAM        0x7
#define E_CANCEL                0x8
#define E_COMPRESSION_ERROR     0x9
#define E_CONNECT_ERROR         0xa
#define E_ENHANCE_YOUR_CALM     0xb
#define E_INADEQUATE_SECURITY   0xc
#define E_HTTP_1_1_REQUIRED     0xd

/* settings types */
#define S_TABSIZE       0x1
#define S_MAXSTREAMS    0x3
#define S_MAXFRAME      0x5

typedef struct {
    int             length;
    unsigned char   type;
    unsigned char   flags;
    unsigned int    stream_id;
} FRAME;

typedef struct {
    unsigned int    id;
    unsigned int    val;
} SETTINGS;

typedef struct {
    int             stream_id;
    UT_hash_handle  hh;
} CLOSED_STREAM;

typedef enum { HEADERS, DATA, TRAILERS, REPLY }   STREAM_STATE;

typedef struct {
    int                         stream_id;
    STREAM_STATE                state;
    int                         at_eos;
    struct hpack_table          *h_tab;
    struct hpack_headerblock    *h_headers, *h_trailers;
    int                         s_be;
    UT_hash_handle              hh;
} ACTIVE_STREAM;

static int
read_int(FILE *f, int n, jmp_buf *jmp_err)
{
    int cin, res;

    for(res = 0; n > 0; n--) {
        if((cin = getc(f)) == EOF)
            longjmp(*jmp_err, 1);
        res = (res << 8) | (cin & 0xFF);
    }
    return res;
}

static void
write_int(FILE *f, int val, int n, jmp_buf *jmp_err)
{
    int byte;

    while(n > 0) {
        byte = (val >> ((n - 1) * 8)) & 0xFF;
        if(putc(byte, f) == EOF)
            longjmp(*jmp_err, 1);
        n--;
    }
    return;
}

#define get_frame(FH, F, E)     { memset((FH), '\0', sizeof(FRAME)); (FH)->length = read_int((F), 3, (E)); (FH)->type = read_int((F), 1, (E)); (FH)->flags = read_int((F), 1, (E)); (FH)->stream_id = read_int((F), 4, (E)); }
#define put_frame(FH, F, E)     { write_int((F), (FH)->length, 3, (E)); write_int((F), (FH)->type, 1, (E)); write_int((F), (FH)->flags, 1, (E)); write_int((F), (FH)->stream_id, 4, (E)); }
#define get_settings(S, F, E)   { memset((S), '\0', sizeof(SETTINGS)); (S)->id = read_int((F), 2, (E)); (S)->val = read_int((F), 4, (E)); }
#define put_settings(S, F, E)   { write_int((F), (S)->id, 2, (E)); write_int((F), (S)->val, 4, (E)); }
#define get_rst(C, F, E)        { *(C) = read_int((F), 4, (E)); }
#define put_rst(C, F, E)        { write_int((F), *(C), 4, (E)); }

static void
close_be(int s_be)
{
    struct timespec t_wait;

    if(s_be < 0)
        return;
    nn_send(s_be, "", 0, 0);
    /* sleep to make sure all messages are through before closing the channel */
    t_wait.tv_sec = 0;
    t_wait.tv_nsec = 1000000;
    nanosleep(&t_wait, NULL);
    nn_close(s_be);
    return;
}

static int
put_msg(FILE *f_client, int stream_id, char *reason, char *body, int TABSIZE, int FRAMESIZE, jmp_buf *jmp_err)
{
    FRAME   rep;
    unsigned char   *content;
    struct hpack_table *tab;
    struct hpack_headerblock *headers;
    int total;
    size_t  length;

    memset(&rep, '\0', sizeof(rep));
    rep.stream_id = stream_id;
    rep.type = F_HEADERS;
    rep.flags = 0x04;
    if(body == NULL)
        rep.flags |= 0x01;
    headers = hpack_headerblock_new();
    if(hpack_header_add(headers, ":status", reason, HPACK_INDEX) == NULL) {
        hpack_headerblock_free(headers);
        return 0;
    }
    tab = hpack_table_new(TABSIZE);
    if((content = hpack_encode(headers, &length, tab)) == NULL) {
        hpack_table_free(tab);
        hpack_headerblock_free(headers);
        return 0;
    }
    rep.length = length;
    put_frame(&rep, f_client, jmp_err);
    fwrite(content, 1, rep.length, f_client);
    total = 9 + rep.length;
    hpack_table_free(tab);
    hpack_headerblock_free(headers);
    free(content);
    if(body != NULL) {
        memset(&rep, '\0', sizeof(rep));
        rep.stream_id = stream_id + 1;
        rep.type = F_DATA;
        rep.flags = 0x01;
        rep.length = strlen(body);
        put_frame(&rep, f_client, jmp_err);
        fwrite(body, 1, rep.length, f_client);
        total += 9 + rep.length;
    }

    return total;
}

static void
put_reject(FILE *f_client, int stream_id, int code, jmp_buf *jmp_err)
{
    FRAME   rep;

    rep.stream_id = stream_id;
    rep.length = 0;
    rep.flags = 0;
    rep.type = F_RST;
    put_frame(&rep, f_client, jmp_err);
    write_int(f_client, code, 4, jmp_err);
    return;
}

static ACTIVE_STREAM *
add_active(ACTIVE_STREAM **as, int stream_id, int TABSIZE)
{
    ACTIVE_STREAM   *s;

    if((s = malloc(sizeof(ACTIVE_STREAM))) == NULL)
        return NULL;
    s->stream_id = stream_id;
    s->h_headers = s->h_trailers = NULL;
    s->h_tab = hpack_table_new(TABSIZE);
    s->s_be = -1;
    s->state = HEADERS;
    s->at_eos = 0;
    HASH_ADD_INT(*as, stream_id, s);
    logmsg(3, "%lX added %d to active %s:%d", pthread_self(), stream_id, __FILE__, __LINE__);
    return s;
}

static void
add_closed(CLOSED_STREAM **cs, int stream_id)
{
    CLOSED_STREAM   *s;

    if((s = malloc(sizeof(CLOSED_STREAM))) == NULL)
        return;
    s->stream_id = stream_id;
    HASH_ADD_INT(*cs, stream_id, s);
    logmsg(3, "%lX added %d to closed %s:%d", pthread_self(), stream_id, __FILE__, __LINE__);
    return;
}

static void
del_active(ACTIVE_STREAM **active_streams, CLOSED_STREAM **closed_streams, int stream_id)
{
    ACTIVE_STREAM   *s;

    HASH_FIND_INT(*active_streams, &stream_id, s);
    if(s != NULL) {
        if(s->h_headers != NULL)
            hpack_headerblock_free(s->h_headers);
        if(s->h_trailers != NULL)
            hpack_headerblock_free(s->h_trailers);
        hpack_table_free(s->h_tab);
        close_be(s->s_be);
        HASH_DEL(*active_streams, s);
        logmsg(3, "%lX removed %d from active %s:%d", pthread_self(), stream_id, __FILE__, __LINE__);
        free(s);
    }
    add_closed(closed_streams, stream_id);
    return;
}

#define cleanup()   {\
    if(active_streams != NULL) \
        HASH_ITER(hh, active_streams, as, tas) { \
            hpack_headerblock_free(as->h_headers); \
            hpack_table_free(as->h_tab); \
            close_be(as->s_be); \
            HASH_DEL(active_streams, as); \
            free(as); \
        } \
    if(closed_streams != NULL) \
        HASH_ITER(hh, closed_streams, cs, tcs) { \
            HASH_DEL(closed_streams, cs); \
            free(cs); \
        } \
    if(content != NULL) \
        free(content); \
}

void
do_http2(HTTP_LISTENER *http, FILE *f_client, char *peer_name, char *crt_buf, int upgrade_h2)
{
    FRAME           header;
    SETTINGS        settings;
    unsigned char   *content, *buf;
    char            *msg;
    int             n, err, last_stream, v1, v2, h_size;
    int             TABSIZE, FRAMESIZE, MAXSTREAMS;
    jmp_buf         err_jmp;
    ACTIVE_STREAM   *active_streams, *as, *tas;
    CLOSED_STREAM   *closed_streams, *cs, *tcs;
    struct hpack_headerblock    *h_cont;
    struct timespec t_wait;

    TABSIZE = H2TABSIZE;
    FRAMESIZE = H2FRAMESIZE;
    MAXSTREAMS = 0;
    active_streams = NULL;
    closed_streams = NULL;
    content = NULL;

    logmsg(1, "%lX start do_http2 %s:%d", pthread_self(), __FILE__, __LINE__);
    if(setjmp(err_jmp)) {
        logmsg(0, "%lX Unexpected error %s:%d", pthread_self(), __FILE__, __LINE__);
        cleanup();
        return;
    }

    get_frame(&header, f_client, &err_jmp);
    if(header.type == F_SETTINGS && !(header.flags & 0x01)) {
        for(n = header.length; n > 0; n -= 6) {
            get_settings(&settings, f_client, &err_jmp);
            if(settings.id == S_TABSIZE)
                TABSIZE = settings.val;
            else if(settings.id == S_MAXFRAME)
                FRAMESIZE = settings.val;
            else if(settings.id == S_MAXSTREAMS)
                MAXSTREAMS = settings.val;
            else
                logmsg(4, "%lX ignored settings %d/%d %s:%d", pthread_self(), settings.id, settings.val, __FILE__, __LINE__);
        }
    }
    logmsg(3, "%lX TABSIZE %d FRAMESIZE %d MAXSTREAMS %d %s:%d", pthread_self(), TABSIZE, FRAMESIZE, MAXSTREAMS, __FILE__, __LINE__);
    if(MAXSTREAMS <= 0)
        MAXSTREAMS = 4;
    if((content = malloc(FRAMESIZE)) == NULL) {
        logmsg(0, "%lX HTTP2 content: out of memory %s:%d", pthread_self(), __FILE__, __LINE__);
        return;
    }

    /* my settings - same as client */
    header.length = 18;
    header.type = 0x04;
    header.flags = 0;
    header.stream_id = 0;
    put_frame(&header, f_client, &err_jmp);
    settings.id = S_TABSIZE;
    settings.val = TABSIZE;
    put_settings(&settings, f_client, &err_jmp);
    settings.id = S_MAXFRAME;
    settings.val = FRAMESIZE;
    put_settings(&settings, f_client, &err_jmp);
    settings.id = S_MAXSTREAMS;
    settings.val = MAXSTREAMS;
    put_settings(&settings, f_client, &err_jmp);
    logmsg(3, "%lX sent my SETTINGS %s:%d", pthread_self(), __FILE__, __LINE__);

    /* ACK */
    header.length = 0;
    header.type = F_SETTINGS;
    header.flags = 0x01;
    header.stream_id = 0;
    put_frame(&header, f_client, &err_jmp);
    logmsg(3, "%lX ACK SETTINGS %s:%d", pthread_self(), __FILE__, __LINE__);

    /* close stream 1 if needed - only for plain upgraded connections */
    if(upgrade_h2) {
        if((cs = malloc(sizeof(CLOSED_STREAM))) == NULL) {
            logmsg(2, "%lX http2: out of memory for closed streams %s:%d", pthread_self(), __FILE__, __LINE__);
            cleanup();
            return;
        }
        cs->stream_id = 1;
        HASH_ADD_INT(closed_streams, stream_id, cs);
        put_reject(f_client, cs->stream_id, E_REFUSED_STREAM, &err_jmp);
        logmsg(3, "%lX REJECT stream 1 %s:%d", pthread_self(), __FILE__, __LINE__);
    }

    /* Frames we care about:
        HEADERS
        CONTINUATION
        DATA
        RST_STREAM
        SETTINGS        ==> only changes in the table size
        PING
        GOAWAY

        Frames we must react to but otherwise ignore:
        PUSH_PROMISE    ==> RST_STREAM, set as closed, ignore further frames

        Frames we can ignore:
        WINDOW_UPDATE
        PRIORITY

        Frames we send:
        HEADERS
        CONTINUATION
        DATA
        GOAWAY
    */
    for(;;) {
        get_frame(&header, f_client, &err_jmp);
        switch(header.type) {
        case F_HEADERS:
            logmsg(4, "%lX received HEADER %d length %d flags %x %s:%d", pthread_self(), header.stream_id, header.length, header.flags, __FILE__, __LINE__);
            HASH_FIND_INT(closed_streams, &header.stream_id, cs);
            if(cs != NULL) {
                /* header for a closed stream -> RST/PRTOCOL_ERROR */
                put_reject(f_client, header.stream_id, E_PROTOCOL_ERROR, &err_jmp);
                logmsg(2, "%lX stream %d already closed %s:%d", pthread_self(), header.stream_id, __FILE__, __LINE__);
                continue;
            }
            HASH_FIND_INT(active_streams, &header.stream_id, as);
            if(as == NULL) {
                if((as = add_active(&active_streams, header.stream_id, TABSIZE)) == NULL) {
                    logmsg(2, "%lX stream %d failed to add to active %s:%d", pthread_self(), header.stream_id, __FILE__, __LINE__);
                    put_msg(f_client, header.stream_id + 1, "500", global.err500, TABSIZE, FRAMESIZE, &err_jmp);
                    add_closed(&closed_streams, header.stream_id);
                    continue;
                }
            } else {
                if(as->state == DATA) {
                    as->state = TRAILERS;
                } else {
                    /* second HEADER frame for an already open stream -> RST/PRTOCOL_ERROR */
                    logmsg(2, "%lX stream %d repeated HEADERS %s:%d", pthread_self(), header.stream_id, __FILE__, __LINE__);
                    del_active(&active_streams, &closed_streams, header.stream_id);
                    put_reject(f_client, header.stream_id, E_PROTOCOL_ERROR, &err_jmp);
                    continue;
                }
            }
            if(header.flags & 0x08) {
                /* padding */
                v1 = read_int(f_client, 1, &err_jmp);
                v2 = 1;
            } else
                v1 = v2 = 0;
            if(header.flags & 0x20) {
                /* stream dependency - ignored */
                n = read_int(f_client, 4, &err_jmp);
                logmsg(4, "%lX %d depends on %d %s:%d", pthread_self(), header.stream_id, n, __FILE__, __LINE__);
                /* weight - ignored */
                (void)read_int(f_client, 1, &err_jmp);
                v2 += 5;
            }
            logmsg(4, "%lX length %d v1 %d v2 %d %s:%d", pthread_self(), header.length, v1, v2, __FILE__, __LINE__);
            if((buf = malloc(header.length - v1 - v2)) == NULL) {
                logmsg(2, "%lX stream %d out of memory %s:%d", pthread_self(), header.stream_id, __FILE__, __LINE__);
                del_active(&active_streams, &closed_streams, header.stream_id);
                put_reject(f_client, header.stream_id, E_PROTOCOL_ERROR, &err_jmp);
                continue;
            }
            h_size = header.length - v1 - v2;
            if(fread(buf, 1, h_size, f_client) != h_size) {
                logmsg(2, "%lX stream %d failed to read content %s:%d", pthread_self(), header.stream_id, __FILE__, __LINE__);
                put_reject(f_client, header.stream_id, E_PROTOCOL_ERROR, &err_jmp);
                free(buf);
                cleanup();
                return;
            }
            if(v1 > 0)
                fread(content, 1, v1, f_client);
            as->at_eos = (header.flags & 0x01);
            if(as->state == TRAILERS && !as->at_eos) {
                logmsg(2, "%lX stream %d trailers but nor end-of-stream %s:%d", pthread_self(), header.stream_id, __FILE__, __LINE__);
                put_reject(f_client, as->stream_id, E_PROTOCOL_ERROR, &err_jmp);
                del_active(&active_streams, &closed_streams, as->stream_id);
                free(buf);
                continue;
            }
            while(!(header.flags & 0x04)) {
                get_frame(&header, f_client, &err_jmp);
                if(header.type != F_CONT || header.stream_id != as->stream_id) {
                    logmsg(2, "%lX stream %d HEADER not followed by CONTINUATION %s:%d", pthread_self(), as->stream_id, __FILE__, __LINE__);
                    put_reject(f_client, as->stream_id, E_PROTOCOL_ERROR, &err_jmp);
                    if(header.stream_id != as->stream_id) {
                        put_reject(f_client, header.stream_id, E_PROTOCOL_ERROR, &err_jmp);
                        del_active(&active_streams, &closed_streams, header.stream_id);
                    }
                    del_active(&active_streams, &closed_streams, as->stream_id);
                    free(buf);
                    buf = NULL;
                    break;
                }
                if((buf = realloc(buf, h_size + header.length)) == NULL) {
                    logmsg(2, "%lX stream %d out of memory %s:%d", pthread_self(), header.stream_id, __FILE__, __LINE__);
                    put_reject(f_client, as->stream_id, E_PROTOCOL_ERROR, &err_jmp);
                    del_active(&active_streams, &closed_streams, as->stream_id);
                    break;
                }
                if(fread(buf + h_size, 1, header.length, f_client) != header.length) {
                    logmsg(2, "%lX stream %d failed to read content %s:%d", pthread_self(), header.stream_id, __FILE__, __LINE__);
                    put_reject(f_client, header.stream_id, E_PROTOCOL_ERROR, &err_jmp);
                    free(buf);
                    cleanup();
                    return;
                }
                h_size += header.length;
            }
            if(as->state == HEADERS) {
                if((as->h_headers = hpack_decode(buf, h_size, as->h_tab)) == NULL) {
                    logmsg(2, "%lX stream %d failed to decode %s:%d", pthread_self(), header.stream_id, __FILE__, __LINE__);
                    put_reject(f_client, header.stream_id, E_PROTOCOL_ERROR, &err_jmp);
                    free(buf);
                    cleanup();
                    return;
                }
            } else {
                if((as->h_trailers = hpack_decode(buf, h_size, as->h_tab)) == NULL) {
                    logmsg(2, "%lX stream %d failed to decode %s:%d", pthread_self(), header.stream_id, __FILE__, __LINE__);
                    put_reject(f_client, header.stream_id, E_PROTOCOL_ERROR, &err_jmp);
                    free(buf);
                    cleanup();
                    return;
                }
            }
            free(buf);
            if(as->state == HEADERS) {
                if((as->s_be = get_be(http, peer_name, NULL, NULL, as->h_headers)) < 0) {
                    logmsg(2, "%lX stream %d no back-end %s:%d", pthread_self(), header.stream_id, __FILE__, __LINE__);
                    put_msg(f_client, header.stream_id + 1, "500", global.err500, TABSIZE, FRAMESIZE, &err_jmp);
                    del_active(&active_streams, &closed_streams, header.stream_id);
                    continue;
                }
                if(crt_buf[0])
                    hpack_header_add(as->h_headers, "X-Pound-Cert", crt_buf, HPACK_INDEX);
                v1 = 2;
                nn_send(as->s_be, &v1, sizeof(int), 0);
                nn_send(as->s_be, peer_name, strlen(peer_name) + 1, 0);
                nn_send(as->s_be, &FRAMESIZE, sizeof(int), 0);
                nn_send(as->s_be, &TABSIZE, sizeof(int), 0);
                nn_send(as->s_be, &as->h_headers, sizeof(as->h_headers), 0);
                if(as->at_eos) {
                    nn_send(as->s_be, "", 0, 0);    /* no content */
                    nn_send(as->s_be, "", 0, 0);    /* no trailers */
                    as->state = REPLY;
                } else
                    as->state = DATA;
            } else {
                nn_send(as->s_be, &as->h_trailers, sizeof(as->h_trailers), 0);
                as->state = REPLY;
            }
            break;
        case F_CONT:
            logmsg(4, "%lX received CONT not following HEAD %d %s:%d", pthread_self(), header.stream_id, __FILE__, __LINE__);
            put_reject(f_client, header.stream_id, E_PROTOCOL_ERROR, &err_jmp);
            del_active(&active_streams, &closed_streams, header.stream_id);
            break;
        case F_DATA:
            logmsg(4, "%lX received DATA %d %s:%d", pthread_self(), header.stream_id, __FILE__, __LINE__);
            HASH_FIND_INT(active_streams, &header.stream_id, as);
            if(as == NULL || as->state != DATA) {
                /* DATA without previous HEADER/CONT frame without END -> RST/PRTOCOL_ERROR */
                logmsg(0, "http2: CONT without previous HEADERS frame or after END");
                del_active(&active_streams, &closed_streams, header.stream_id);
                put_reject(f_client, header.stream_id, E_PROTOCOL_ERROR, &err_jmp);
                continue;
            }
            if(header.flags & 0x08) {
                /* padding */
                v1 = read_int(f_client, 1, &err_jmp);
                v2 = 1;
            } else
                v1 = v2 = 0;
            if(fread(content, 1, header.length - v2, f_client) != header.length) {
                put_reject(f_client, header.stream_id, E_PROTOCOL_ERROR, &err_jmp);
                del_active(&active_streams, &closed_streams, header.stream_id);
                logmsg(0, "%lX http2: premature EOF %s:%d", pthread_self(), __FILE__, __LINE__);
                return;
            }
            nn_send(as->s_be, content, header.length - v2 - v1, 0);
            if(v1 > 0)
                fread(content, 1, v1, f_client);
            if(as->at_eos = (header.flags & 0x01)) {
                nn_send(as->s_be, "", 0, 0);    /* no trailers */
                as->state = REPLY;
            }
            break;
        case F_RST:
            logmsg(4, "%lX received RST %d %s:%d", pthread_self(), header.stream_id, __FILE__, __LINE__);
            break;
        case F_SETTINGS:
            logmsg(4, "%lX received SETTINGS %d %s:%d", pthread_self(), header.stream_id, __FILE__, __LINE__);
            if(header.flags & 0x1) {    /* ACK for what we sent */
                logmsg(4, "%lX received ACK %d type %d length %d %s:%d", pthread_self(), header.stream_id, header.type, header.flags, __FILE__, __LINE__);
                continue;
            }
            if(header.stream_id != 0) {
                logmsg(4, "%lX non-zero stream_id %d %d %s:%d", pthread_self(), __FILE__, __LINE__);
                last_stream = header.stream_id;
                header.stream_id = 0;
                header.type = F_GOAWAY;
                header.flags = 0;
                header.length = 8;
                put_frame(&header, f_client, &err_jmp);
                write_int(f_client, last_stream, 4, &err_jmp);
                err = 0x1; /*protocol error */
                write_int(f_client, err, 4, &err_jmp);
                cleanup();
                return;
            }
            for(n = header.length; n > 0; n -= 6) {
                get_settings(&settings, f_client, &err_jmp);
                if(settings.id == S_TABSIZE)
                    TABSIZE = settings.val;
                else if(settings.id == S_MAXFRAME) {
                    FRAMESIZE = settings.val;
                    if((content = realloc(content, FRAMESIZE)) == NULL) {
                        logmsg(0, "HTTP2: Out of memory");
                        cleanup();
                        return;
                    }
                } else if(settings.id == S_MAXSTREAMS)
                    MAXSTREAMS = settings.val;
                else
                    logmsg(4, "%lX ignored settings %d/%d %s:%d", pthread_self(), settings.id, settings.val, __FILE__, __LINE__);
            }
            logmsg(3, "%lX TABSIZE %d FRAMESIZE %d MAXSTREAMS %d %s:%d", pthread_self(), TABSIZE, FRAMESIZE, MAXSTREAMS, __FILE__, __LINE__);
            /* ACK */
            header.length = 0;
            header.type = 0x04;
            header.flags = 0x01;
            header.stream_id = 0;
            put_frame(&header, f_client, &err_jmp);
            logmsg(3, "%lX ACK SETTINGS %s:%d", pthread_self(), __FILE__, __LINE__);
            break;
        case F_PING:
            logmsg(4, "%lX received PING %d %s:%d", pthread_self(), header.stream_id, __FILE__, __LINE__);
            if(header.stream_id != 0 || header.length != 8) {
                last_stream = header.stream_id;
                header.stream_id = 0;
                header.type = F_GOAWAY;
                header.flags = 0;
                header.length = 8;
                put_frame(&header, f_client, &err_jmp);
                write_int(f_client, last_stream, 4, &err_jmp);
                err = 0x1; /*protocol error */
                write_int(f_client, err, 4, &err_jmp);
                cleanup();
                return;
            }
            v1 = read_int(f_client, 4, &err_jmp);
            v2 = read_int(f_client, 4, &err_jmp);
            header.flags = 0x1; /* ACK */
            put_frame(&header, f_client, &err_jmp);
            write_int(f_client, v1, 4, &err_jmp);
            write_int(f_client, v2, 4, &err_jmp);
            break;
        case F_GOAWAY:
            logmsg(4, "%lX received GOAWAY %d %s:%d", pthread_self(), header.stream_id, __FILE__, __LINE__);
            /* should really send a GOAWAY back */
            cleanup();
            return;
            // break;
        /* rejected - no push promise */
        case F_PUSH:
            logmsg(4, "%lX received PUSH %d %s:%d", pthread_self(), header.stream_id, __FILE__, __LINE__);
            add_closed(&closed_streams, header.stream_id);
            put_reject(f_client, header.stream_id, E_REFUSED_STREAM, &err_jmp);
            break;
        /* acknowledged but otherwise ignored */
        case F_PRIORITY:
            logmsg(4, "%lX received PRIORITY %d length %d %s:%d", pthread_self(), header.stream_id, header.length, __FILE__, __LINE__);
            if(header.length != 5 || header.stream_id == 0) {
                cleanup();
                return;
            }
            v1 = read_int(f_client, 4, &err_jmp);
            v2 = read_int(f_client, 1, &err_jmp);
            logmsg(4, "%lX PRIORITY %d stream %d weight %d %s:%d", pthread_self(), header.stream_id, v1 & 0x7FFFFFFF, v2, __FILE__, __LINE__);
            if(header.stream_id == 0)
                put_reject(f_client, 0, E_PROTOCOL_ERROR, &err_jmp);
            break;
        case F_WINUPD:
            logmsg(4, "%lX received WINUPD %d %s:%d", pthread_self(), header.stream_id, __FILE__, __LINE__);
            if(header.length != 4) {
                cleanup();
                return;
            }
            v1 = read_int(f_client, 4, &err_jmp);
            break;
        default:
            logmsg(4, "%lX received UNKNOWN %d stream_id %d length %d flags %d %s:%d", pthread_self(), header.type, header.stream_id, header.length, header.flags, __FILE__, __LINE__);
            cleanup();
            return;
        }
        HASH_ITER(hh, active_streams, as, tas) {
            if(as->state == REPLY) {
                for(v1 = 1; (header.length = nn_recv(as->s_be, &msg, NN_MSG, 0)) > 0; v1 = 0) {
                    header.stream_id = as->stream_id;
                    header.flags = 0;
                    header.type = (v1? F_HEADERS: F_CONT);
                    put_frame(&header, f_client, &err_jmp);
                    fwrite(msg, 1, header.length, f_client);
                    nn_freemsg(msg);
                    logmsg(4, "%lX write %d:%s -> %d %s:%d", pthread_self(), header.stream_id, v1? "HEADER": "CONT", header.length, __FILE__, __LINE__);
                }
                header.stream_id = as->stream_id;
                header.length = 0;
                header.flags = 0x04;
                header.type = (v1? F_HEADERS: F_CONT);
                put_frame(&header, f_client, &err_jmp);
                logmsg(4, "%lX write %d -> end-headers %s:%d", pthread_self(), header.stream_id, __FILE__, __LINE__);
                while((header.length = nn_recv(as->s_be, &msg, NN_MSG, 0)) > 0) {
                    header.stream_id = as->stream_id;
                    header.flags = 0;
                    header.type = F_DATA;
                    put_frame(&header, f_client, &err_jmp);
                    fwrite(msg, 1, header.length, f_client);
                    nn_freemsg(msg);
                    logmsg(4, "%lX write %d:DATA -> %d %s:%d", pthread_self(), header.stream_id, header.length, __FILE__, __LINE__);
                }
                for(v1 = 1; (header.length = nn_recv(as->s_be, &msg, NN_MSG, 0)) > 0; v1 = 0) {
                    header.stream_id = as->stream_id;
                    header.flags = (v1? 0x01: 0);
                    header.type = (v1? F_HEADERS: F_CONT);
                    put_frame(&header, f_client, &err_jmp);
                    fwrite(msg, 1, header.length, f_client);
                    nn_freemsg(msg);
                    logmsg(4, "%lX write trailer %d:%s -> %d %s:%d", pthread_self(), header.stream_id, v1? "HEADER": "CONT", header.length, __FILE__, __LINE__);
                }
                header.stream_id = as->stream_id;
                header.length = 0;
                header.flags = (v1? 0x01 | 0x04: 0x04);
                header.type = (v1? F_HEADERS: F_CONT);
                put_frame(&header, f_client, &err_jmp);
                logmsg(4, "%lX write trailer %d -> end-headers %s:%d", pthread_self(), header.stream_id, __FILE__, __LINE__);

                /* end of comms; has to be done from this side, as no LINGER available */
                nn_send(as->s_be, "", 0, 0);
                /* sleep to make sure all messages are through before closing the channel */
                t_wait.tv_sec = 0;
                t_wait.tv_nsec = 1000000;
                nanosleep(&t_wait, NULL);

                nn_close(as->s_be);

                del_active(&active_streams, &closed_streams, as->stream_id);
            }
        }
    }
}