/*
    zhttp.c - http client
    Copyright (C) 2012-2023 Ladislav Vaiz <ok1zia@nagano.cz>

    This program is free software; you can redistribute it and/or
    modify it under the terms of the GNU General Public License
    version 2 as published by the Free Software Foundation.

*/

#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif

#include <zhttp.h>

#include "libziaint.h"

#include "eprintf.h"
#include <zdebug.h>
#include <zerror.h>
#include <zglib.h>
#include <zhash.h>
#include <zpath.h>
#include <zselect.h>
#include <zsock.h>
#include <zstr.h>
#include <zthread.h>

#include <string.h>

#include <glib.h>

#ifdef Z_HAVE_GNUTLS
static gnutls_certificate_credentials_t xcred;
#endif

#ifdef Z_MSC
#pragma warning(disable : 4996)
#endif

#define SLOWDOWNx 100

static void zhttp_adns_callback(struct zasyncdns *adns, int n, int *family, int *socktype, int *protocol, int *addrlen, union zsockaddr *addr, char *errorstr);
static void zhttp_connected_handler(void *arg);
static void zhttp_write_handler(void *arg);
static void zhttp_read_handler(void *arg);
static void zhttp_connecting_timer(void *arg);
#if defined(Z_HAVE_GNUTLS) || defined(Z_HAVE_OPENSSL)
static void zhttp_tls_handshake(void *arg);
#endif

struct zhttp *zhttp_init(void){
    struct zhttp *http = g_new0(struct zhttp, 1);
    http->request = zbinbuf_init();
    http->response = zbinbuf_init();    
	http->state = ZHTTPST_NEW;
	http->dataofs = -1;
	http->cookies = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
	http->headers = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free); /* TODO: who should free it? */
	http->sock = -1;
	//dbg("zhttp_init(%p)\n", http);

	return http;
}

void zhttp_free(struct zhttp *http){
	//dbg("zhttp_free(%p)\n", http);
	if (!http) return;
	if (http->sock >= 0){
		zselect_set(http->zsel, http->sock, NULL, NULL, NULL, NULL);
#ifdef Z_HAVE_GNUTLS
		if (http->istls){
			gnutls_deinit(http->session);
		}
#endif
#ifdef Z_HAVE_OPENSSL
		if (http->istls /*&& http->ctx != NULL*/) {
			SSL_shutdown(http->ssl);
			SSL_free(http->ssl);
			SSL_CTX_free(http->ctx);
		}
#endif
		closesocket(http->sock);
		http->sock = -1;
	}
	zbinbuf_free(http->request);
    zbinbuf_free(http->response);
    zasyncdns_free(http->adns);
    g_free(http->errorstr);
    g_free(http->server);
    g_free(http->serveraddr);
    g_free(http->page);
	g_free(http->url);
	g_hash_table_destroy(http->cookies);
	g_hash_table_destroy(http->headers);

	zhttp_post_free(http);
	g_free(http->datastr);
	if (http->connecting_timer_id > 0) zselect_timer_kill(http->zsel, http->connecting_timer_id);
	g_free(http);
}

void zhttp_post_free(struct zhttp *http){
	unsigned int i;

	if (!http->posts) return;

	for (i = 0; i < http->posts->len; i++){
		struct zhttp_post_var *var = (struct zhttp_post_var*)g_ptr_array_index(http->posts, i);
		g_free(var->name);
		g_free(var->filename);
		g_free(var->value);
		g_free(var);
	}
	g_ptr_array_free(http->posts, TRUE);
	http->posts = NULL;
}

void zhttp_disconnect(struct zhttp *http, char *errorstr){
	if (http->sock >= 0){
		zselect_set(http->zsel, http->sock, NULL, NULL, NULL, NULL);
#ifdef Z_HAVE_GNUTLS
		if (http->istls){
			gnutls_deinit(http->session);
		}
#endif
#ifdef Z_HAVE_OPENSSL
		if (http->istls/* && http->ctx != NULL*/) {
			SSL_shutdown(http->ssl);
			SSL_free(http->ssl);
			SSL_CTX_free(http->ctx);
		}
#endif
		closesocket(http->sock);
		http->sock = -1;
	}
	g_free(http->errorstr);
	http->errorstr = errorstr;
	if (http->errorstr) http->state = ZHTTPST_ERROR;
	http_get_data(http);
	if (http->callback) http->callback(http);
}


static void zhttp_prepare1(struct zhttp *http, struct zselect *zsel, const char *url, void *arg){
    char *u, *c, *host = NULL;

	http->zsel = zsel;
	http->arg = arg;
	http->dataofs = -1;
	http->sent = 0;
    http->url = g_strdup(url);
	
    u = g_strdup(url);

	c = z_strcasestr(u, "https:"); 
	if (c != NULL){
#if defined(Z_HAVE_GNUTLS) || defined(Z_HAVE_OPENSSL)
		zhttp_init_tls();
		http->istls = 1;

		c = z_strcasestr(u, "https://");
		if (c){
			host = c + strlen("https://");
		}else{
			host = u;
		}
		http->port = 443;
#else
		zhttp_disconnect(http, g_strdup("TLS not supported"));
#endif
	}else{
		c = z_strcasestr(u, "http:");
		if (c != NULL){
			host = c + strlen("http://");
		}else{
			host = u;
			c = z_strcasestr(u, "://");
			if (c != NULL) zhttp_disconnect(http, g_strdup("Unsupported URL schema"));
		}
		http->port = 80;
	}

	if (http->state == ZHTTPST_ERROR) {
		g_free(u);
		return;
	}

    c = strchr(host, '/');
    if (c){
        http->page = g_strdup(c);
        *c = '\0';
    }else{
        http->page = g_strdup("/");
    }

    c = strchr(host, ':');
    if (c){
        http->port = atoi(c + 1);
        *c = '\0';
    }
    http->server = g_strdup(host);

	zbinbuf_truncate(http->request, 0);
	zbinbuf_truncate(http->response, 0);
    g_free(u);

#ifdef Z_HAVE_GNUTLS
	if (http->istls){
		int ret = gnutls_init(&http->session, GNUTLS_CLIENT);
		if (ret != 0) {
			zhttp_disconnect(http, g_strdup("Can't init TLS session"));
			return;
		}

		ret = gnutls_server_name_set(http->session, GNUTLS_NAME_DNS, http->server, strlen(http->server));
		if (ret != 0){
			zhttp_disconnect(http, g_strdup("Can't set TLS server name"));
			return;
		}

		ret = gnutls_set_default_priority(http->session);
		if (ret != 0){
			zhttp_disconnect(http, g_strdup("Can't set TLS default cipher priority"));
			return;
		}

		ret = gnutls_credentials_set(http->session, GNUTLS_CRD_CERTIFICATE, xcred);
		if (ret != 0){
			zhttp_disconnect(http, g_strdup("Can't set TLS credentials"));
			return;
		}

		gnutls_session_set_verify_cert(http->session, http->server, (gnutls_certificate_verify_flags)0);

	}
#endif

#ifdef Z_HAVE_OPENSSL
	if (http->istls) {
		const SSL_METHOD* method = TLS_client_method();
		http->ctx = SSL_CTX_new(method);
		SSL_CTX_set_verify(http->ctx, SSL_VERIFY_NONE, NULL); // Tucnak needs data also when there is a certificate problem
		//SSL_CTX_set_verify_depth(ctx, 4);
		SSL_CTX_set_options(http->ctx, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3); // TLS 1.0 and 1.1 are also obsolete
		//SSL_CTX_set_options(http->ctx, SSL_OP_NO_COMPRESSION);

		/*int ret = SSL_CTX_load_verify_locations(ctx, http->server, NULL);
		if (ret != 1) {
			char errbuf[256];
			ERR_error_string_n(SSL_get_error(http->ssl, ret), errbuf, sizeof(errbuf));
			zhttp_disconnect(http, g_strdup_printf("Can't verify SSL locations: %s", errbuf));
			return;
		}*/
	}
#endif
}

static void http_fill_cookies(gpointer key, gpointer value, gpointer user_data){
	GString *gs = (GString *)user_data;

	if (gs->len > 0) g_string_append(gs, "; ");
	g_string_append_printf(gs, "%s=%s", (char*)key, (char*)value);
}

static void http_fill_user_headers(gpointer key, gpointer value, gpointer user_data){
	zbinbuf_sprintfa((struct zbinbuf *)user_data, "%s: %s\r\n", key, value);
	//GString *gs = g_string_new("");
	//struct zbinbuf *zb = (struct zbinbuf *)user_data;
	//g_string_append_printf(gs, "%s: %s\n", (char*)key, (char*)value);
	//zbinbuf_append(zb, gs->str);
	//g_string_free(gs, TRUE);
}


static void zhttp_headers(struct zhttp *http){

	zbinbuf_sprintfa(http->request, "Host: %s\r\n", http->server);
	zbinbuf_append(http->request, "User-Agent: Libzia " Z_VERSION "\r\n");
#ifdef Z_HAVE_ZLIB
	zbinbuf_append(http->request, "Accept-Encoding: gzip, deflate\r\n");
#else
	zbinbuf_append(http->request, "Accept-Encoding:\r\n");
#endif
	zbinbuf_append(http->request, "Connection: close\r\n");
	if (g_hash_table_size(http->headers) > 0){
		g_hash_table_foreach(http->headers, http_fill_user_headers, http->request);
	}
	if (g_hash_table_size(http->cookies) > 0){
		GString *cookies = g_string_new("");
		g_hash_table_foreach(http->cookies, http_fill_cookies, cookies);
		zbinbuf_sprintfa(http->request, "Cookie: %s\r\n", cookies->str);	
		g_string_free(cookies, TRUE);
	}

}

static void zhttp_do(struct zhttp *http, struct zselect *zsel, void (*callback)(struct zhttp *)){
	http->origreqlen = http->request->len;
	http->adns = zasyncdns_init();
	http->callback = callback;
	zasyncdns_getaddrinfo(
            http->adns,
            zsel,
            zhttp_adns_callback,
            http->server,
            AF_INET,
            http);
	http->state = ZHTTPST_DNS;
}

void zhttp_get(struct zhttp *http, struct zselect *zsel, const char *url, void (*callback)(struct zhttp *), void *arg){
	zhttp_prepare1(http, zsel, url, arg);
	if (http->state == ZHTTPST_ERROR){
		callback(http);
		return;
	}
	zbinbuf_sprintfa(http->request, "GET %s HTTP/1.0\r\n", http->page);
	zhttp_headers(http);
	zbinbuf_append(http->request, "\r\n");
	zhttp_do(http, zsel, callback);
}

void zhttp_add_header(struct zhttp *http, const char *name, const char *value)
{
	g_hash_table_insert(http->headers, g_strdup(name), g_strdup(value));
}

void zhttp_raw(struct zhttp *http, struct zselect *zsel, const char *url, const char *raw_request,  void (*callback)(struct zhttp *), void *arg){
	zhttp_prepare1(http, zsel, url, arg);
	zbinbuf_append(http->request, raw_request);
	zhttp_do(http, zsel, callback);
}

void zhttp_post(struct zhttp *http, struct zselect *zsel, const char *url, void (*callback)(struct zhttp *), void *arg){
	struct zbinbuf *zbb;
	char *boundary = g_strdup_printf("---------------------------%d%d%d%d", rand(), rand(), rand(), rand());

	zhttp_prepare1(http, zsel, url, arg);
	zbinbuf_sprintfa(http->request, "POST %s HTTP/1.1\r\n", http->page);
	zhttp_headers(http);
	zbinbuf_sprintfa(http->request, "Content-Type: multipart/form-data; boundary=%s\r\n", boundary);
	zbb = zbinbuf_init();
	if (http->posts){
		unsigned int i;
    	
		for (i = 0; i < http->posts->len; i++){
		    struct zhttp_post_var *var = (struct zhttp_post_var*)g_ptr_array_index(http->posts, i);
			zbinbuf_sprintfa(zbb, "--%s\r\n", boundary);
			if (var->filename != NULL) {
				char *contenttype = "application/octet-stream";
				const char *ext = z_extension(var->filename);
				if (strcasecmp(ext, ".png") == 0) contenttype = "image/png";
				if (strcasecmp(ext, ".csv") == 0) contenttype = "text/plain";

				zbinbuf_sprintfa(zbb, "Content-Disposition: form-data; name=\"%s\"; filename=\"%s\"\r\n", var->name, var->filename);
				zbinbuf_sprintfa(zbb, "Content-Type: %s\r\n", contenttype);
				zbinbuf_sprintfa(zbb, "\r\n");
				if (var->localfilename != NULL)
					zbinbuf_append_file(zbb, var->localfilename);
				else
					zbinbuf_append(zbb, var->value);
				zbinbuf_sprintfa(zbb, "\r\n");
			}else{
				zbinbuf_sprintfa(zbb, "Content-Disposition: form-data; name=\"%s\"\r\n", var->name);
				zbinbuf_sprintfa(zbb, "\r\n");
				zbinbuf_sprintfa(zbb, "%s\r\n", var->value);
			}
		}
		zbinbuf_sprintfa(zbb, "--%s--\r\n", boundary);
		
	}
	zbinbuf_sprintfa(http->request, "Content-Length: %d\r\n", zbb->len);
	zbinbuf_append(http->request, "\r\n");
	zbinbuf_append_bin(http->request, zbb->buf, zbb->len);
/*    {
static int fileN = 1;
#warning TODO
        char fn[256];
        sprintf(fn, "post%d.bin", fileN++);
        zbinbuf_write_to_file(http->request, fn); 
    }*/

	zhttp_do(http, zsel, callback);
	zbinbuf_free(zbb);
	g_free(boundary); 
}

void zhttp_post_add(struct zhttp *http, const char *name, const char *value){
	
	struct zhttp_post_var *var = g_new0(struct zhttp_post_var, 1);
	var->name = g_strdup(name);
	var->value = g_strdup(value);

	if (!http->posts) http->posts = g_ptr_array_new();
	g_ptr_array_add(http->posts, var);
}
 
void zhttp_post_add_file_mem(struct zhttp *http, const char *name, const char *filename, const char *value){
	
	struct zhttp_post_var *var = g_new0(struct zhttp_post_var, 1);
	var->name = g_strdup(name);
	var->filename = g_strdup(filename);
	var->value = g_strdup(value);

	if (!http->posts) http->posts = g_ptr_array_new();
	g_ptr_array_add(http->posts, var);
}

void zhttp_post_add_file_disk(struct zhttp *http, const char *name, const char *filename, const char *localfilename){
	
	struct zhttp_post_var *var = g_new0(struct zhttp_post_var, 1);
	var->name = g_strdup(name);
	var->filename = g_strdup(filename);
	var->localfilename = g_strdup(localfilename);

	if (!http->posts) http->posts = g_ptr_array_new();
	g_ptr_array_add(http->posts, var);
}

static void zhttp_adns_callback(struct zasyncdns *adns, int n, int *family, int *socktype, int *protocol, int *addrlen, union zsockaddr *addr, char *errorstr){
    struct zhttp *http = (struct zhttp *)adns->arg;
    int i, ret;
    char errbuf[256];
	GString *gs2;            
        
	int port = http->port;

    if (errorstr != NULL){
		http->state = ZHTTPST_ERROR;
		g_free(http->errorstr);
		http->errorstr = g_strdup_printf("Can't resolve %s: %s", http->server, errorstr);
        http->callback(http);
        return;
    }

    if (n == 0){
		http->state = ZHTTPST_ERROR;
		g_free(http->errorstr);
		http->errorstr = g_strdup_printf("Can't resolve %s: No data returned", http->server);
        http->callback(http);
        return;
    }


    
    for (i = 0; i < n; i++){
//        dbg("socket(%d, %d, %d)\n", family[i], socktype[i], protocol[i]);
        
        //http->sock = socket(family[i], socktype[i], protocol[i]);
        http->sock = socket(family[i], SOCK_STREAM, 0);
        if (http->sock < 0) continue;

        if (z_sock_nonblock(http->sock, 1)){
            closesocket(http->sock);
            http->sock = -1;
            continue;
        }


        
        switch (family[i]){
            case AF_INET:                
                ((struct sockaddr_in *)(addr + i))->sin_port = htons(port);                
                break;
#ifdef AF_INET6            
            case AF_INET6:              
                ((struct sockaddr_in6 *)(addr + i))->sin6_port = htons(port);                
                break;
#endif            
            default:                   
                closesocket(http->sock);                
                http->sock = -1;
                continue;       // unsupported protocol family        
        }       

		gs2 = g_string_new("");
		z_sock_ntoa(gs2, family[i], addr + i);
		g_free(http->serveraddr);
		http->serveraddr = g_strdup(gs2->str);
        g_string_free(gs2, TRUE);       


	//addr[i].in.sin_addr.S_un.S_un_b.s_b1 = 127; //nefunguje
	//addr[i].in.sin_addr.S_un.S_un_b.s_b2 = 0;
	//addr[i].in.sin_addr.S_un.S_un_b.s_b3 = 0;
	//addr[i].in.sin_addr.S_un.S_un_b.s_b4 = 1;
	//((struct sockaddr_in *)(addr + i))->sin_port = htons(8888);


        ret = connect(http->sock, (struct sockaddr *)(addr + i), addrlen[i]);     
        if (ret < 0){           
            int err = z_sock_errno;         
            if (z_sock_wouldblock(err)){                
				//dbg("connecting to %s\n", http->serveraddr);      
                zselect_set(adns->zsel, http->sock, NULL, zhttp_connected_handler, NULL, http);           
				http->state = ZHTTPST_CONNECTING;
				http->connecting_timer_id = zselect_timer_new(http->zsel, 30000, zhttp_connecting_timer, http);
            }else{                
				http->state = ZHTTPST_ERROR;
				g_free(http->errorstr);
				http->errorstr = g_strdup_printf("Can't connect to %s, %s", http->serveraddr, z_sock_strerror());
                closesocket(http->sock);                
                http->sock = -1;         
            }           
        }        
        else{            
            zhttp_connected_handler(http);        
        }        
        break;
    }
};


static void zhttp_connecting_timer(void *arg){
	struct zhttp *http = (struct zhttp*)arg;									   
	http->connecting_timer_id = -1;
	zhttp_disconnect(http, g_strdup_printf("Timeout connecting to %s:%d", http->server, http->port));
}

static void zhttp_connected_handler(void *arg){
    struct zhttp *http = (struct zhttp *)arg;
	char errbuf[256];

	int err = z_sock_error(http->sock);
	if (err){
        zhttp_disconnect(http, g_strdup_printf("Can't connect to %s:%d, %s", http->server, http->port, z_sock_strerror()));
        return;
    }

#ifdef Z_HAVE_GNUTLS
	if (http->istls){
		gnutls_transport_set_int(http->session, http->sock);
		gnutls_handshake_set_timeout(http->session, GNUTLS_DEFAULT_HANDSHAKE_TIMEOUT);

		zselect_set(http->zsel, http->sock, zhttp_tls_handshake, zhttp_tls_handshake, NULL, http);
		http->state = ZHTTPST_TLS_HANDSHAKE;
	}
	else
#endif
#ifdef Z_HAVE_OPENSSL
	if (http->istls) {
		http->ssl = SSL_new(http->ctx);
		if (http->ssl == NULL) {
			zhttp_disconnect(http, g_strdup("SSL_new failed"));
			return;
		}

		int ret = SSL_set_fd(http->ssl, http->sock);
		if (ret != 1) {
			zhttp_disconnect(http, g_strdup("SSL_set_fd failed"));
			return;
		}
		long lret = SSL_set_tlsext_host_name(http->ssl, http->server); // needed for SNI enabled servers
		if (lret != SSL_TLSEXT_ERR_OK && lret != SSL_TLSEXT_ERR_ALERT_WARNING) {
			zhttp_disconnect(http, g_strdup_printf("SSL_set_tlsext_host_name failed, error %ld", lret));
			return;
		}

		SSL_set_connect_state(http->ssl);

		zselect_set(http->zsel, http->sock, zhttp_tls_handshake, zhttp_tls_handshake, NULL, http);
		http->state = ZHTTPST_TLS_HANDSHAKE;
	}
	else
#endif
	{
		zselect_set(http->zsel, http->sock, NULL, zhttp_write_handler, NULL, http);
		http->state = ZHTTPST_REQUEST;
	}
}

#ifdef Z_HAVE_GNUTLS
static void zhttp_tls_handshake(void *arg){
	struct zhttp *http = (struct zhttp*)arg;

	int ret = gnutls_handshake(http->session);
	if (ret < 0 && gnutls_error_is_fatal(ret) == 0){
		// like EWOULDBLOCK
		return;
	}
	else if (ret < 0) {
		if (ret == GNUTLS_E_CERTIFICATE_VERIFICATION_ERROR) {
			/* check certificate verification status */
			int type = gnutls_certificate_type_get(http->session);
			unsigned status = gnutls_session_get_verify_cert_status(http->session);
			gnutls_datum_t out;
			int ret2 = gnutls_certificate_verification_status_print(status, type, &out, 0);
			if (ret2 == 0){
				zhttp_disconnect(http, g_strdup_printf("Cert verify failed: %s", out.data));
			}else{
				zhttp_disconnect(http, g_strdup_printf("Cert verify failed"));
			}
			gnutls_free(out.data);
			return;
		}
		zhttp_disconnect(http, g_strdup_printf("Handshake failed: %s", gnutls_strerror(ret)));
		return;
	}
	
	//char *desc = gnutls_session_get_desc(http->session);
	//dbg("Session info: %s\n", desc);
	//gnutls_free(desc);
	zselect_set(http->zsel, http->sock, NULL, zhttp_write_handler, NULL, http);
	http->state = ZHTTPST_REQUEST;
}
#endif

#ifdef Z_HAVE_OPENSSL
static void zhttp_tls_handshake(void* arg) {
	struct zhttp* http = (struct zhttp*)arg;

	int ret = SSL_do_handshake(http->ssl);

	if (ret <= 0) {
		int error = SSL_get_error(http->ssl, ret);
		//dbg("SSL_do_handshake  ret=%d  error=%d     %s\n", ret, error, http->url);

		switch (error) {
			case SSL_ERROR_WANT_READ:
				return;
			case SSL_ERROR_WANT_WRITE:
				return;
				break;
			default:
				break;
		}
	}else{
		//dbg("SSL_do_handshake  ret=%d               %s\n", ret, http->url);
	}

	if (ret != 1) { 
		char errbuf[256];
		ERR_error_string_n(SSL_get_error(http->ssl, ret), errbuf, sizeof(errbuf));
		zhttp_disconnect(http, g_strdup_printf("SSL_do_handshake failed: ret=%d, %s", ret, errbuf));
		return;
	}

	//STACK_OF(X509) *cert = SSL_get_peer_cert_chain(http->ssl); // refcnt not incremented

	//ret = SSL_get_verify_result(http->ssl);

	zselect_set(http->zsel, http->sock, NULL, zhttp_write_handler, NULL, http);
	http->state = ZHTTPST_REQUEST;
}
#endif

static void zhttp_write_handler(void *arg){
    struct zhttp *http = (struct zhttp *)arg;
	int ret, tosend;
	char errbuf[256];

	tosend = http->request->len;
#ifdef SLOWDOWN
	tosend = Z_MIN(tosend, SLOWDOWN);
	usleep(100000);
#endif

#ifdef Z_HAVE_GNUTLS
	if (http->istls){
		ret = gnutls_record_send(http->session, http->request->buf, tosend);
		if (ret < 0){
			if (gnutls_error_is_fatal(ret) == 0) return;

			zhttp_disconnect(http, g_strdup_printf("Error writing gnutls: %s", gnutls_strerror(ret)));
			return;
		}
	}else
#endif
#ifdef Z_HAVE_OPENSSL
	if (http->istls) {
		ret = SSL_write(http->ssl, http->request->buf, tosend);
		//dbg("SSL_write(%d) = %d\n", tosend, ret);
		if (ret <= 0) { // yes, <=
			char errbuf[256];
			ERR_error_string_n(SSL_get_error(http->ssl, ret), errbuf, sizeof(errbuf));
			zhttp_disconnect(http, g_strdup_printf("SSL_write failed: %s", errbuf));
			return;
		}
	}
	else
#endif
	{
		ret = send(http->sock, http->request->buf, tosend, 0);
		int err = z_sock_errno;
		if (ret <= 0){
			zhttp_disconnect(http, g_strdup_printf("Error writing http socket: %s", z_sock_strerror()));
			return;
		}
	}

    //dbg("send(%d) = %d\n", tosend, ret);
	

	http->sent += ret;

    //dbg("buf=%d -> ", http->request->len);
	zbinbuf_erase(http->request, 0, ret);
    //dbg("%d\n", http->request->len);
	if (http->request->len == 0){
		zselect_set(http->zsel, http->sock, zhttp_read_handler, NULL, NULL, http);
		http->state = ZHTTPST_HEADER;
	}

#ifdef Z_HAVE_GNUTLS
	if (http->istls){
		size_t pending = gnutls_record_check_pending(http->session);
		if (pending > 0){
			zhttp_read_handler(arg);
		}
	}
#endif
#ifdef Z_HAVE_OPENSSL
	if (http->istls) {
		int pending = SSL_pending(http->ssl);
		if (pending > 0) {
			zhttp_read_handler(arg);
		}
	}
#endif
}

#define ZHTTP_BUFSIZE 4096

static void zhttp_read_handler(void *arg){
    struct zhttp *http = (struct zhttp *)arg;
	int ret;
	char buf[ZHTTP_BUFSIZE];
	char errbuf[256];
	char *c;
	int i, ncnt, torecv;
	
	torecv = ZHTTP_BUFSIZE;
#ifdef SLOWDOWN
	torecv = Z_MIN(ZHTTP_BUFSIZE, SLOWDOWN);
	usleep(100000);
#endif

#ifdef Z_HAVE_GNUTLS
	if (http->istls){
		ret = gnutls_record_recv(http->session, buf, torecv);
		if (ret < 0){
			if (gnutls_error_is_fatal(ret) == 0) return;
			if (ret == GNUTLS_E_PREMATURE_TERMINATION){ // fix for https://tilecache.rainviewer.asia
				ret = 0;
			}else{
				zhttp_disconnect(http, g_strdup_printf("Error reading gnutls: %s", gnutls_strerror(ret)));
				// return ???
			}
		}
	}else
#endif
#ifdef Z_HAVE_OPENSSL
	if (http->istls) {
		ret = SSL_read(http->ssl, buf, torecv);
		if (ret <= 0) {
			int error = SSL_get_error(http->ssl, ret);
			//dbg("SSL_read  ret=%d  error=%d     %s\n", ret, error, http->url);

			switch (error) {
				case SSL_ERROR_WANT_READ:
					return;
				case SSL_ERROR_WANT_WRITE:
					return;
				default:
					break;
			}
			if (error != SSL_ERROR_ZERO_RETURN) {
				char errbuf[256];
				ERR_error_string_n(SSL_get_error(http->ssl, ret), errbuf, sizeof(errbuf));
				zhttp_disconnect(http, g_strdup_printf("SSL_read failed: %s", errbuf));
				return;
			}
		}else{
			//dbg("SSL_read(%d) = %d\n", torecv, ret);
		}
	}
	else
#endif
	{
		ret = recv(http->sock, buf, torecv, 0);
		int err = z_sock_errno;
		if (ret < 0){
			zhttp_disconnect(http, g_strdup_printf("Error reading http socket: %s", z_sock_strerror()));
			return;
		}
	}

	
	if (ret == 0){
		int ofs = 0;
		
		// parse status code
		http->status = 0;
		for (i = 0; i < http->response->len; i++){
			if (http->response->buf[i] == '\n'){
				zhttp_disconnect(http, g_strdup("Can't parse status line"));				
				return;
			}
			if (http->response->buf[i] == ' '){
				char s[100];
				ofs = i + 1;
				zbinbuf_getline(http->response, &ofs, s, sizeof(s));
				http->status = atoi(s);

				
				if (http->status != 200){
					zhttp_disconnect(http, g_strdup_printf("HTTP error %s", s));				
					return;
				}
				break;
			}
			
		}

		if (!http->status){
			zhttp_disconnect(http, g_strdup("Can't parse status code"));				
			return;
		}

		http->state = ZHTTPST_DONE;
#ifdef Z_HAVE_ZLIB
		char *content_encoding = http_get_header(http, "Content-Encoding");
		if (content_encoding != NULL && strcmp(content_encoding, "gzip") == 0) {
			g_free(content_encoding);
			if (zhttp_decode_gzip(http) < 0) {
				return; // disconnect allready called
			}
		}
#endif
		zhttp_disconnect(http, NULL);
		return;
	}

	zbinbuf_append_bin(http->response, buf, ret);

	if (http->dataofs < 0){
		c = http->response->buf;
		ncnt = 0;
		for (i = 0; i < http->response->len; i++, c++){
			if (*c == '\r') continue;
			if (*c != '\n') {
				ncnt = 0;
				continue;
			}
			ncnt++;
			if (ncnt == 2){
				http->dataofs = i + 1;
				http->state = ZHTTPST_DATA;
				// save cookies
				zhttp_store_cookies(http, http->response->buf, i);
				break;
			}
		}
	}
}

#ifdef Z_HAVE_ZLIB
int zhttp_decode_gzip(struct zhttp* http) {
	z_stream stream;
	struct zbinbuf* decompressed = zbinbuf_init();
	char buf[4096];

	stream.zalloc = Z_NULL;
	stream.zfree = Z_NULL;
	stream.opaque = Z_NULL;
	// setup "b" as the input and "c" as the compressed output
	stream.avail_in = (uInt)http->response->len - http->dataofs; // size of input
	stream.next_in = (Bytef*)http->response->buf + http->dataofs; // input char array

	inflateInit2(&stream, 16 + MAX_WBITS);

	do {
		stream.avail_out = (uInt)sizeof(buf); // size of output
		stream.next_out = (Bytef*)buf; // output char array

		int ret = inflate(&stream, Z_NO_FLUSH);
		switch (ret) {
			case Z_NEED_DICT:
				ret = Z_DATA_ERROR;     /* and fall through */
			case Z_DATA_ERROR:
			case Z_STREAM_ERROR:
			case Z_MEM_ERROR:
				(void)inflateEnd(&stream);
				zbinbuf_free(decompressed);
				zhttp_disconnect(http, "gzip decompress error");
				return -1;
		}

		zbinbuf_append_bin(decompressed, buf, sizeof(buf) - stream.avail_out);

	} while (stream.avail_out == 0);

	inflateEnd(&stream);

	zbinbuf_prepend(decompressed, http->response->buf, http->dataofs);
	zbinbuf_free(http->response);
	http->response = decompressed;

	return 0;
}
#endif

void zhttp_status(struct zhttp *http, GString *gs){
	switch (http->state){
		case ZHTTPST_NEW:
			g_string_append(gs, "Waiting for command");
			break;
		case ZHTTPST_DNS:
			g_string_append_printf(gs, "Resolving %s", http->server);
			break;
		case ZHTTPST_CONNECTING:
			g_string_append_printf(gs, "Connecting to %s", http->serveraddr);
			break;
		case ZHTTPST_REQUEST:
			g_string_append_printf(gs, "Sending request %d KB / %d KB", http->sent / 1024, http->origreqlen / 1024);
			break;
		case ZHTTPST_HEADER:
			g_string_append(gs, "Fetching headers");
			break;
		case ZHTTPST_DATA:
			g_string_printf(gs, "Downloading %d KB", (http->response->len - http->dataofs) / 1024);
			break;
		case ZHTTPST_DONE:
			g_string_append(gs, "Done");
			break;
		default:
			g_string_append_printf(gs, "Unknown state %d", (int)http->state);
			break;
	}
	if (http->url) {
		char *c = g_strdup(http->url);
		z_strip_from(c, '?');
		g_string_append_printf(gs, "\n%s", c);
		g_free(c);
	}
}

int zhttp_write_data(struct zhttp *http, const char *filename){
	int towrite, ret;
	char *wfn;
	FILE *f;

	towrite = http->response->len - http->dataofs;
	
	if (towrite <= 0 || http->dataofs <= 0){
		http->state = ZHTTPST_ERROR;
		g_free(http->errorstr);
		http->errorstr = g_strdup_printf("Bad http response (len=%d ofs=%d)", http->response->len, http->dataofs);
		return -1;
	}
	wfn = g_strdup(filename);
	z_wokna(wfn);
	f = fopen(wfn, "wb");
	if (!f){
		http->state = ZHTTPST_ERROR;
		g_free(http->errorstr);
		http->errorstr = g_strdup_printf("Can't write %s", wfn);
		g_free(wfn);
		return -2;
	}
	ret = fwrite(http->response->buf + http->dataofs, 1, towrite, f);
	if (ret != towrite){
        fclose(f);
		http->state = ZHTTPST_ERROR;
		g_free(http->errorstr);
		http->errorstr = g_strdup_printf("Can't write to %s", wfn);
		g_free(wfn);
		return -3;
	}
	fclose(f);
	g_free(wfn);
	return 0;
}

#define SCLEN 11
void zhttp_store_cookies(struct zhttp *http, const char *data, int len){
	int i;
	const char *s, *pos;

	for (i = 0; i < len; i++){
		s = data + i;
		pos = strchr(s, '\n');
		if (pos == NULL) break;

		if (strncasecmp(s, "Set-Cookie:", SCLEN) == 0){
			char *key, *val, *oldval;
			char *d = g_strndup(s + SCLEN, (pos - s) - SCLEN);
			key = z_trim(d);
			z_strip_from(d, ';');

			val = strchr(key, '=');
			if (val != NULL){
				*val = '\0';
				val++;
				oldval = (char *)g_hash_table_lookup(http->cookies, key);
				if (oldval != NULL) g_hash_table_remove(http->cookies, key);
				g_hash_table_insert(http->cookies, g_strdup(key), g_strdup(val));
			}
			g_free(d);
		}
		i += (pos - s);
	}
}

char *http_get_data(struct zhttp *http){
	int size;
	zg_free0(http->datastr);

	if (http->dataofs <= 0)
		size = 1;
	else
		size = http->response->len - http->dataofs + 1 + 1;

	http->datastr = (char *)g_malloc(size);
	zbinbuf_getstr(http->response, http->dataofs, http->datastr, size);

	return http->datastr;
}

void zhttp_post_json(struct zhttp *http, struct zselect *zsel, const char *url, const char *json, void (*callback)(struct zhttp *), void *arg){
	
    zhttp_prepare1(http, zsel, url, arg);
	zbinbuf_sprintfa(http->request, "POST %s HTTP/1.1\r\n", http->page);
	zhttp_headers(http);
	zbinbuf_sprintfa(http->request, "Content-Type: application/json\r\n");
	zbinbuf_sprintfa(http->request, "Content-Length: %d\r\n", strlen(json));
	zbinbuf_append(http->request, "\r\n");
	zbinbuf_append_bin(http->request, json, strlen(json));
    /*{
#warning TODO
        char fn[256];
        sprintf(fn, "post%d.bin", fileN++);
        zbinbuf_write_to_file(http->request, fn, 0, http->request->len); 
        exit(-1);
    }*/ 

	zhttp_do(http, zsel, callback);
}

#ifdef Z_HAVE_GNUTLS

static int zgnutls_initialized = 0;

void zhttp_init_tls(void){

	if (zgnutls_initialized) return;
	zgnutls_initialized = 1;

	if (sizeof(ssize_t) != sizeof(size_t)) zinternal("sizeof(ssize_t) != sizeof(size_t)  %d != %d", sizeof(ssize_t), sizeof(size_t));

	if (gnutls_check_version("3.5.8") == NULL) zinternal("GnuTLS 3.5.8 or later is required");
	int ret = gnutls_global_init();
	if (ret) zinternal("Can't init gnutls");

	ret = gnutls_certificate_allocate_credentials(&xcred);
	if (ret) zinternal("Can't allocate certificate credentials");

	ret = gnutls_certificate_set_x509_system_trust(xcred);
	if (ret < 0) zinternal("Can't set the system trusted CAs");
}
#endif

#ifdef Z_HAVE_OPENSSL
static int zopenssl_initialized = 0;

#ifndef OPENSSL_NO_TRACE
size_t* openssl_trace_cb(const char* buffer, size_t count, int category, int cmd, void* data) {
	char* c = g_strndup(buffer, count);
	dbg("openssl: %s\n", c);
	g_free(c);
}
#endif

void zhttp_init_tls(void) {

	if (zopenssl_initialized) return;
	zopenssl_initialized = 1;

	//OPENSSL_init_crypto(OPENSSL_INIT_LOAD_CRYPTO_STRINGS, NULL);
	OPENSSL_init_ssl(OPENSSL_INIT_LOAD_SSL_STRINGS | OPENSSL_INIT_LOAD_CRYPTO_STRINGS, NULL);
	ERR_clear_error();

#ifndef OPENSSL_NO_TRACE
	OSSL_trace_set_callback(OSSL_TRACE_CATEGORY_ALL, openssl_trace_cb, NULL);
#endif
}

#endif

char *http_get_header(struct zhttp *http, const char *header_name){

	if (http->dataofs < 0) return NULL;
	if (http->response == NULL) return NULL;
	if (http->response->buf == NULL) return NULL;

	char *c;
	for (c = http->response->buf; *c != '\0'; c++){
		char *colon = strchr(c, ':');
		char *lf = strchr(c, '\n');
		if (colon != NULL && lf != NULL){
			int n = Z_MIN(colon - c, lf - c);
			if (strncasecmp(c, header_name, n) == 0){
				char *ct = g_strndup(colon + 1, lf - (colon + 1));
				char *ret = z_strdup_trim(ct);
				g_free(ct);
				return ret;
			}
		}
		if (lf == NULL) break;
		c = lf;
	}
	return NULL;
}

int http_is_content_type(struct zhttp *http, const char *content_type){
	int ret = 0;

	char *ct = http_get_header(http, "content-type");
	if (!ct) return ret;

	z_strip_from(ct, ';');

	ret = (strcasecmp(ct, content_type) == 0);
	g_free(ct);
	return ret;
}

void zhttp_auth_basic(struct zhttp* http, const char* username, const char* password) {
	if (!username || !*username || !password) return;

	GString* gs = g_string_sized_new(100);
	g_string_append(gs, "Basic ");
	char* a = g_strdup_printf("%s:%s", username, password);
	zg_string_eprintfa("b", gs, "%b", a, strlen(a));
	zhttp_add_header(http, "Authorization", gs->str);

	g_free(a);
	g_string_free(gs, TRUE);
}
