
/*---------------------------------------------------------------------------*/
/* appproxy.cpp                                                              */
/* copyright (c) innovaphone 2016                                            */
/*                                                                           */
/*---------------------------------------------------------------------------*/

#include <ctype.h>
#include "platform/platform.h"
#include "common/os/iomux.h"
#include "common/interface/task.h"
#include "common/interface/time.h"
#include "common/interface/socket.h"
#include "common/interface/webserver_plugin.h"
#include "common/interface/database.h"
#include "common/interface/json_api.h"
#include "common/interface/http_client.h"
#include "common/interface/dns.h"
#include "common/interface/file.h"
#include "common/interface/network.h"
#include "common/ilib/str.h"
#include "common/ilib/json.h"
#include "common/ilib/base64.h"
#include "common/lib/appservice.h"
#include "common/interface/stask.h"
#if defined (TEST_TASKS)
#include "test-engine/lap/test_tasks_file.h"
#else
#include "common/lib/tasks_file.h"
#endif

#include "appproxy_if.h"
#include "appproxy.h"

//#define APPPROXY_PATCH
#define DBG_APPPROXY(x) //debug->printf x
#define DBG_PASSTHROUGH(x) //debug->printf x
#define DBG_MANIFEST(x) //debug->printf x
#define DBG_WEBSOCKET(x) //debug->printf x

#define APPPROXY_MANIFEST_ID        "innovaphone.manifest"
#define APPPROXY_CACHEABLE_RELOAD_TIME (24 * 60 * 60)
#define APPPROXY_CACHEABLE_REUSE_TIME 60
#define APPPROXY_REUSE_DETECT_TIME  100
#define APPPROXY_WEBSOCKET_CONNECT_TIMEOUT 3000

static unsigned readLine(const char * buffer, unsigned len, bool & eol);
static unsigned readJson(const char * buffer, unsigned len, bool & eod);
static unsigned readHost(const char * s, bool & dns, bool & ip6);
static const char * urlPathToFilePath(const char * urlPath);
static const char * filePathToUrlPath(const char * filePath);
static const char * fileToMimeType(const char * name);
static const char * mimeToFileExtension(const char * type);

#ifdef APPPROXY_PATCH

typedef struct {
    const char * filename;
    const byte * data;
    unsigned len;
} AppProxyPatchFile;

static byte phone_sdp_js_gz[] = {
#include "phone_sdp_js_gz.h"
};

static byte PhoneVideoSender_js_gz[] = {
#include "PhoneVideoSender_js_gz.h"
};

static byte PhoneVideoReceiver_js_gz[] = {
#include "PhoneVideoReceiver_js_gz.h"
};

static AppProxyPatchFile appProxyPatchTable[] = {
    { "phone_sdp.js", phone_sdp_js_gz, sizeof(phone_sdp_js_gz) },
    { "PhoneVideoSender.js", PhoneVideoSender_js_gz, sizeof(PhoneVideoSender_js_gz) },
    { "PhoneVideoReceiver.js", PhoneVideoReceiver_js_gz, sizeof(PhoneVideoReceiver_js_gz) },
};

#endif

static byte dummy_png[] = {
#include "dummy_png.h"
};

static unsigned UrlDecodeSimple(const char *in, char *out)
{
    unsigned i;

    i = 0;
    while (*in) {
        if ((in[0] == '%') && isxdigit(in[1]) && isxdigit(in[2])) {
            out[i++] = ((isdigit(in[1]) ? in[1] - '0' : tolower(in[1]) - 'a' + 10) << 4) | (isdigit(in[2]) ? in[2] - '0' : tolower(in[2]) - 'a' + 10);
            in += 3;
        }
        else {
            out[i++] = *(in++);
        }
    }
    out[i] = '\0';
    return i;
}

/*---------------------------------------------------------------------------*/
/* class AppProxyService                                                     */
/*---------------------------------------------------------------------------*/

AppProxyService::AppProxyService(class IIoMux * const iomux, class ISocketProvider * localSocketProvider, class IWebserverPluginProvider * const webserverPluginProvider,
    class ISocketProvider * tcpSocketProvider, class ISocketProvider * tlsSocketProvider, AppServiceArgs * args, UAppProxy * user, bool tlsTranslate) :
        AppService(iomux, localSocketProvider, args)
{
    this->iomux = iomux;
    this->localSocketProvider = localSocketProvider;
    this->webserverPluginProvider = webserverPluginProvider;
    this->tcpSocketProvider = tcpSocketProvider;
    this->tlsSocketProvider = tlsSocketProvider;
    this->user = user;
    this->tlsTranslate = tlsTranslate;
    this->appproxy = NULL;
}

AppProxyService::~AppProxyService()
{

}

class AppInstance * AppProxyService::CreateInstance(AppInstanceArgs * args)
{
	return new AppProxy(iomux, this, args);
}

void AppProxyService::AppServiceApps(istd::list<AppServiceApp> * appList)
{

}

void AppProxyService::Cacheable(const char* url, const char* filename)
{
    this->Log(LOG_APPPROXY, "AppProxyService::Cacheable url=%s filename=%s", url ? url : "", filename ? filename : "");
    if (appproxy) appproxy->Cacheable(url, filename);
}

void AppProxyService::ConnectionMonitor(bool on)
{
    if (appproxy) appproxy->ConnectionMonitor(on);
}


/*---------------------------------------------------------------------------*/
/* class AppProxy                                                            */
/*---------------------------------------------------------------------------*/

AppProxy::AppProxy(IIoMux * const iomux, class AppProxyService * service, AppInstanceArgs * args) : AppInstance(service, args), STaskContext(iomux)
{
    this->stopping = false;
    this->service = service;
    this->webserverPlugin = service->webserverPluginProvider->CreateWebserverPlugin(iomux, service->localSocketProvider, this, args->webserver, args->webserverPath, this);
    IInstanceLog::Log(LOG_APPPROXY, "App instance started %s", service->GetAppServiceId());

    workingPath = _strdup(args->workingPath);
    webserverPath = _strdup(args->webserverPath);
    char tmp[200];
    _sprintf(tmp, "%s%smanifests.json", &args->webserverPath[1], (args->webserverPath[1] == '\0') ? "" : "/");
    manifestsFile = _strdup(tmp);

    read = new TaskFileRead(this, workingPath, tmp, MAX_MANIFESTS_LENGTH, true);
    read->Start(this);

    write = 0;
    manifests = 0;
    redirects = 0;
    cacheables = 0;
    manifestsUpdated = false;
    connectionMonitorOn = false;

    readManifests = 0;
    service->appproxy = this;
}

AppProxy::~AppProxy()
{
    ASSERT(write == 0, "AppProxy write task still running...");
    if (workingPath) free(workingPath);
    if (webserverPath) free(webserverPath);
    if (manifestsFile) free(manifestsFile);
    while (!manifestList.empty()) {
        delete manifestList.front();
    }
    while (!closedManifests.empty()) {
        delete closedManifests.front();
    }
    while (cacheables) {
        class AppProxyCacheable * c = (class AppProxyCacheable *)cacheables;
        cacheables = cacheables->btree_get(c);
        delete c;
    }
    if (this->webserverPlugin) delete this->webserverPlugin;
}

void AppProxy::Cacheable(const char * url, const char * filename)
{
    unsigned i, j;

    i = 0;
    while ((url[i] != '\0') && (url[i] != '/')) {
        i++;
    }
    if (url[i] != '\0') {
        do {
            i++;
        }
        while ((url[i] != '\0') && (url[i] != '/'));
        if (url[i] != '\0') {
            do {
                i++;
            }
            while ((url[i] != '\0') && (url[i] != '/'));
            if (url[i] != '\0') {
                i++;
                j = strlen(url);
                while ((j > i) && (url[j-1] != '/')) {
                    j--;
                }
                if (j > i) {
                    char b[2048];
                    memcpy(b, &url[i], j-i);
                    strcpy(&b[j-i], filename);
                    if (addCacheable(b)) {
                        updateManifestFile();
                    }
                }
            }
        }
    }
}

class AppProxyCacheable * AppProxy::addCacheable(const char * path)
{
    char b[4096];
    unsigned i;
    class AppProxyCacheable * c = (class AppProxyCacheable *)(cacheables->btree_find(path));
    DBG_MANIFEST(("AppProxy::addCacheable(%s) c=%p", path, c));
    if (!c) {
        c = new AppProxyCacheable(iomux, path);
        cacheables = cacheables->btree_put(c);

        _sprintf(b, "%s%s", workingPath, urlPathToFilePath(path));
        IDirectory * d = FileSystem::OpenDirectory(b, this);
        if (d) {
            IDirectoryEntry * e;
            while ((e = d->ReadNext()) != NULL) {
                if (e->IsFile()) {
                    const char * n = e->GetName();
                    i = _sprintf(b, "%s%s/%s", workingPath, urlPathToFilePath(path), n);
                    time_t t = FileSystem::GetLastWriteTime(b, this);
                    const char * type = NULL;
                    while ((i != 0) && (b[i-1] != '.') && (b[i-1] != '/')) {
                        i--;
                    }
                    if ((i != 0) && (b[i-1] == '.')) {
                        i--;
                        type = fileToMimeType(&b[i]);
                        b[i] = '\0';
                        while ((i != 0) && (b[i-1] != '/')) {
                            i--;
                        }
                        class AppProxyManifestFile * f = c->addFile(&b[i], NULL, type);
                        if (f) {
                            f->loaded = true;
                            f->lastWriteTime = t;
                        }
                    }
                }
                delete e;
            }
            d->Close();
            delete d;
        }

        return c;
    }
    return 0;
}

class AppProxyCacheable * AppProxy::findCacheable(const char * path)
{
    class AppProxyCacheable * c = (class AppProxyCacheable *)(cacheables->btree_find(path));
    DBG_MANIFEST(("AppProxy::findCacheable(%s) %x", path, c));
    return c;
}

void AppProxy::ConnectionMonitor(bool on)
{
    connectionMonitorOn = on;
    if (on && !passthroughList.empty()) {
        class AppProxyPassthrough * p;
        for (p = passthroughList.front(); p; p = p->goNext()) {
            p->UpdateConnectionMonitor();
        }
    }
}

void AppProxy::WebserverPluginRegisterPathResult(IWebserverPlugin * plugin, const char * pathName, wsp_path_type_t pathType)
{
    if (service->user) service->user->AppProxyReady();
}

void AppProxy::WebserverPluginPassthroughListenResult(IWebserverPlugin * plugin, char * resourceName, const char * registeredPathForRequest, ulong64 dataSize)
{
    if (!stopping) {
        class AppProxyPassthrough * passthrough = new AppProxyPassthrough(this, service->tcpSocketProvider, service->tlsSocketProvider, service->tlsTranslate);
        passthroughList.push_back(passthrough);
        plugin->Accept(passthrough);
    }
    else {
        plugin->Cancel(wsr_cancel_type_t::WSP_CANCEL_UNAVAILABLE);
    }
}


void AppProxy::Stop()
{
    TryStop();
}

void AppProxy::AppInstanceShutdownTimeout()
{
    debug->printf("AppProxy::AppInstanceShutdownTimeout %d %d %d %d %d %d %d",
        (webserverPlugin != NULL), !socketRetainers.empty(), !passthroughList.empty(),
        (read != NULL), (readManifests != NULL), (write != NULL), !manifestList.empty());
    if (!passthroughList.empty()) {
        class AppProxyPassthrough * p;
        for(p = passthroughList.front(); p; p = p->goNext()) {
            p->ShutdownTimeout();
        }
    }
    if (!manifestList.empty()) {
        class AppProxyManifest * m;
        for (m = manifestList.front(); m; m = m->goNext()) {
            m->ShutdownTimeout();
        }
    }
}

void AppProxy::ServerCertificateUpdate(const byte * cert, size_t certLen)
{
    IInstanceLog::Log(LOG_APPPROXY, "AppProxy::ServerCertificateUpdate cert:%x certLen:%u", cert, certLen);
}

void AppProxy::SocketRetainerRemoved(class AppProxySocketRetainer * retainer)
{
    if(stopping) TryStop();
}

void AppProxy::AppProxyPassthroughClosed(class AppProxyPassthrough * passthrough)
{
    delete passthrough;
    if (stopping) {
        TryStop();
    }
}

void AppProxy::TryStop()
{
    if (!stopping) {
        IInstanceLog::Log(LOG_APPPROXY, "App instance stopping");
        stopping = true;
        service->appproxy = NULL;
        if (webserverPlugin) webserverPlugin->Close();
        if (!socketRetainers.empty()) {
            class AppProxySocketRetainer * r;
            for(r = socketRetainers.front(); r; r = r->goNext()) {
                r->Shutdown();
            }
        }
        if (!passthroughList.empty()) {
            class AppProxyPassthrough * p;
            for(p = passthroughList.front(); p; p = p->goNext()) {
                p->Close();
            }
        }
    }
    if(!webserverPlugin && socketRetainers.empty() && passthroughList.empty()) {
        if (read || readManifests || write) {
            return;
        }
        if(!manifestList.empty()) {
            class AppProxyManifest * m, * next;
            m = manifestList.front();
            while (m) {
                next = m->goNext();
                if (m->loadComplete()) {
                    m->Close();
                }
                else {
                    m->RemoveFiles();
                }
                m = next;
            }
            if (!manifestList.empty()) {
                return;
            }
        }
        appService->AppStopped(this);
    }
}

void AppProxy::DeleteManifest(class AppProxyManifest * m)
{
    char b[2048];
    unsigned i;

    if (m->removeFiles) {
        IInstanceLog::Log(LOG_APPPROXY, "AppProxyManifest(%s)::Remove", m->fileNames.front()->name);
        i = _sprintf(b, "%s%s", workingPath, urlPathToFilePath(m->path));
        b[i-1] = '\0';
        FileSystem::RemoveDirectory(b, this);
        IInstanceLog::Log(LOG_APPPROXY, "AppProxyManifest(%s)::Removed", m->fileNames.front()->name);
    }
    delete m;
    if(stopping && manifestList.empty()) {
        TryStop();
    }
}

void AppProxy::IoExec(AppProxyManifestClose * execContext)
{
    class AppProxyManifest * m = static_cast<class AppProxyManifest *>(execContext);
    DeleteManifest(m);
}

void AppProxy::ManifestCloseComplete(class AppProxyManifest * m)
{
    if (m->removeFiles && m->loadComplete()) {
        m->remove();
        closedManifests.push_back(m);
        updateManifestFile();
    }
    else {
        SetExec(iomux, this, m);
    }
}

/* Webserver plugin */

void AppProxy::WebserverPluginClose(IWebserverPlugin * plugin, wsp_close_reason_t reason, bool lastUser)
{
    IInstanceLog::Log(LOG_APPPROXY, "WebserverPlugin closed");
    if (lastUser) {
        delete webserverPlugin;
        webserverPlugin = 0;
        TryStop();
    }
}

void AppProxy::TaskComplete(class ITask * const task)
{
    DBG_APPPROXY(("AppProxy::TaskComplete()"));
    if (task == read) {
        unsigned l = read->getLength();
        byte * b = (byte *)alloca(l + 1);
        read->getData(b, l);
        b[l] = '\0';
        class json_io json((char * )b);
        if (json.decode()) {
            word last = 0;
            word base = json.get_object(JSON_ID_ROOT, last);
            word handle = json.get_array(base, "manifests");
            if (handle != JSON_ID_ROOT) {
                const char * f;
                for (word last = 0; (f = json.get_string(handle, last)); ) {
                    if (!readManifests) readManifests = new TaskReadManifests(this);
                    readManifests->AddManifest(f);
                }
            }
            word redirectsHandle = json.get_array(base, "redirects");
            if (redirectsHandle != JSON_ID_ROOT) {
                const char * f;
                for (word last = 0; (f = json.get_string(redirectsHandle, last)); ) {
                    if (!readManifests) readManifests = new TaskReadManifests(this);
                    readManifests->AddManifest(f);
                }
            }
            word cacheablesHandle = json.get_array(base, "cacheables");
            if (cacheablesHandle != JSON_ID_ROOT) {
                const char * n;
                for (word last = 0; (n = json.get_string(cacheablesHandle, last)); ) {
                    addCacheable(n);
                }
            }
        }
        delete read;
        read = 0;
        if (readManifests) {
            ((class ITask *)readManifests)->Start(this);
        }
        else if (stopping) {
            TryStop();
        }
        else {
            webserverPlugin->PassthroughListen(APPPROXY_REGISTERED_PATH, this);
        }
    }
    else if (task == write) {
        delete write;
        write = 0;
        if (manifestsUpdated) {
            manifestsUpdated = false;
            updateManifestFile();
        }
        else {
            while (!closedManifests.empty()) {
                DeleteManifest(closedManifests.front());
            }
        }
        if (stopping && !write) {
            TryStop();
        }
    }
    else if (task == readManifests) {
        delete readManifests;
        readManifests = 0;
        if (stopping) {
            TryStop();
        }
        else {
            webserverPlugin->PassthroughListen(APPPROXY_REGISTERED_PATH, this);
        }
    }
}

void AppProxy::TaskFailed(class ITask * const task)
{
    DBG_APPPROXY(("AppProxy::TaskFailed()"));
    if (task == read) {
        delete read;
        read = 0;
        if (stopping) {
            TryStop();
        }
        else {
            webserverPlugin->PassthroughListen(APPPROXY_REGISTERED_PATH, this);
        }
    }
    else if (task == write) {
        delete write;
        write = 0;
        if (manifestsUpdated) {
            manifestsUpdated = false;
            updateManifestFile();
        }
        if (stopping && !write) {
            TryStop();
        }
    }
    else if (task == readManifests) {
        delete readManifests;
        readManifests = 0;
        if (stopping) {
            TryStop();
        }
        else {
            webserverPlugin->PassthroughListen(APPPROXY_REGISTERED_PATH, this);
        }
    }
}

void AppProxy::TaskProgress(class ITask * const task, dword progress)
{
    DBG_APPPROXY(("AppProxy::TaskProgress()"));
    if (task == read) {
    }
    else if (task == write) {
        char buffer[MAX_MANIFESTS_LENGTH];
        class json_io json(buffer);
        word base = json.add_object(0xffff, 0);
        word handle = json.add_array(base, "manifests");
        for (class btree * b = manifests->btree_find_left(); b; b = manifests->btree_find_next_left(((class AppProxyManifest *)b)->path)) {
            class AppProxyManifest *m = (class AppProxyManifest *)b;
            if (m->loadComplete() && m->manifest) {
                AppProxyManifestFile * f;
                DBG_MANIFEST(("AppProxy::TaskProgress complete manifest %s", m->path));
                for (f = m->fileNames.front(); f; f = f->goNext()) {
                    json.add_string(handle, 0, f->name);
                }
            }
        }
        word redirectsHandle = json.add_array(base, "redirects");
        for (class btree * b = manifests->btree_find_left(); b; b = manifests->btree_find_next_left(((class AppProxyManifest *)b)->path)) {
            class AppProxyManifest *m = (class AppProxyManifest *)b;
            if (m->loadComplete() && !m->manifest) {
                DBG_MANIFEST(("AppProxy::TaskProgress complete redirect %s", m->path));
                json.add_string(redirectsHandle, 0, m->fileNames.front()->name);
            }
        }
        word cacheablesHandle = json.add_array(base, "cacheables");
        for (class btree * b = cacheables->btree_find_left(); b; b = cacheables->btree_find_next_left(((class AppProxyCacheable *)b)->path)) {
            class AppProxyCacheable *c = (class AppProxyCacheable *)b;
            DBG_MANIFEST(("AppProxy::TaskProgress complete cacheable %s", c->path));
            json.add_string(cacheablesHandle, 0, c->path);
        }
        json.encode();
        write->write((byte *)buffer, strlen(buffer), true);
        manifestsUpdated = false;
    }
}

class AppProxyManifest * AppProxy::findManifest(const char * fileName)
{
    char * path;
    unsigned pathLen;

    pathLen = strlen(fileName);
    path = (char *)malloc(pathLen + 1);
    memcpy(path, fileName, pathLen + 1);
    while (pathLen) {
        if (path[pathLen - 1] == '/') {
            path[pathLen] = '\0';
            class btree * b = manifests->btree_find(path);
            if (b) {
                free(path);
                return ((AppProxyManifest *)b);
            }
        }
        pathLen--;
    }
    free(path);
    return 0;
}

class AppProxyManifestFile * AppProxy::findManifestFile(class AppProxyManifest * m, const char * fileName)
{
    if (!m) {
        DBG_MANIFEST(("AppProxy::findManifestFile(%s) no manifest", fileName));
        return 0;
    }
    class AppProxyManifestFile * f = m->findFile(&fileName[m->pathLen]);
    DBG_MANIFEST(("AppProxy::findManifestFile(%s) ret=%x", fileName, f));
    return f;
}

class AppProxyManifest * AppProxy::findRedirect(const char * fileName)
{
    char * redirectPath;
    unsigned fileNameLen, redirectLen;

    fileNameLen = strlen(fileName);
    redirectLen = fileNameLen;
    redirectPath = (char *)malloc(redirectLen + 1);
    memcpy(redirectPath, fileName, redirectLen + 1);
    while (redirectLen) {
        if ((redirectPath[redirectLen - 1] == '/') && !isdigit(redirectPath[redirectLen])) {
            redirectPath[redirectLen] = '\0';
            class btree * b = redirects->btree_find(redirectPath);
            if (b) {
                class AppProxyRedirect * r = (class AppProxyRedirect *)b;
                class AppProxyManifest * m = r->manifest;
                free(redirectPath);
                return m;
            }
        }
        redirectLen--;
    }
    free(redirectPath);
    return 0;
}

class AppProxyManifestFile * AppProxy::findAddCacheableFile(AppProxyCacheable * c, const char * fileName, const char * fileParams, const char *addType)
{
    class AppProxyManifestFile * f = NULL;
    char b[2056];
    unsigned i, j;

    i = 0;
    while ((fileParams[i] != '\0') && (fileParams[i] != '=')) {
        i++;
    }
    if (fileParams[i] != '\0') {
        i++;
        j = i;
        while ((fileParams[i] != '\0') && (fileParams[i] != '&')) {
            i++;
        }
        if (fileParams[i] != '\0') {
            memcpy(b, &fileParams[j], i-j);
            b[i-j] = '\0';
            if (addType) {
                f = c->addFile(b, NULL, addType);
                if (!f) {
                    f = c->findFile(b);
                    if (f && (strcasecmp(f->type, addType) != 0) && f->loaded) {
                        i = _sprintf(b, "%s%s", workingPath, urlPathToFilePath(fileName));
                        _sprintf(&b[i], "/%s.%s", urlPathToFilePath(f->name), mimeToFileExtension(f->type));
                        FileSystem::RemoveFile(b, this);
                        f->loaded = false;
                        if (f->contentType) free(f->contentType);
                        f->contentType = _strdup(addType);
                        f->type = f->contentType;
                    }
                    else {
                        f = NULL;
                    }
                }
                if (f) {
                    time_t t = ITime::TimeStampMilliseconds() / 1000;
                    f->lastWriteTime = t;
                }
            }
            else {
                f = c->findFile(b);
            }
            if (f && strstr(&fileParams[j], "&no-cache")) {
                time_t t = ITime::TimeStampMilliseconds() / 1000;
                f->lastWriteTime = t - APPPROXY_CACHEABLE_RELOAD_TIME;
            }
        }
    }
    return f;
}

void AppProxy::updateManifestFile()
{
    DBG_APPPROXY(("AppProxy::updateManifestFile()"));
    if (!write) {
        write = new TaskFileWrite(this, workingPath, manifestsFile, false);
        write->Start(this);
    }
    else {
        manifestsUpdated = true;
    }
}

bool AppProxy::ConnectivityUp()
{
    if (!passthroughList.empty()) {
        class AppProxyPassthrough * p;
        for(p = passthroughList.front(); p; p = p->goNext()) {
            if (p->websocket) {
                return true;
            }
        }
    }
    return false;
}

/*---------------------------------------------------------------------------*/
/* class AppProxyPassthrough                                                 */
/*---------------------------------------------------------------------------*/

static char AppProxy503ServiceUnavailable[] =
    "HTTP/1.1 503 Service Unavailable\r\n"
    "Retry-After: 1\r\n"
    "Cache-Control: no-cache, no-store\r\n"
    "Content-length: 0\r\n\r\n";

AppProxyPassthrough::AppProxyPassthrough(class AppProxy * appProxy, ISocketProvider * tcpSocketProvider, ISocketProvider * tlsSocketProvider, bool tlsTranslate) :
    connectTimer(appProxy->iomux, this),
    retryTimer(appProxy->iomux, this)
{
    DBG_PASSTHROUGH(("AppProxyPassthrough(%x)::AppProxyPassthrough()", this));
    this->tcpSocketProvider = tcpSocketProvider;
    this->tlsSocketProvider = tlsSocketProvider;
    this->tlsTranslate = tlsTranslate;
    this->appProxy = appProxy;

    closing = false;
    loadingManifest = 0;
    loadingCacheable = 0;
    loadingFile = 0;
    write = 0;
    writing = false;
    read = 0;
    retryInterval = 500;
    read_started = false;
    reading = false;
    websocket = false;
    shutdown = false;
    dns = 0;
    dnsPending = false;
    connect_tunnel = false;
    proxyAddr = NULL;
    proxies = NULL;
    proxy = NULL;
    retryAuthenticated = false;

    xmit.content = false;
    xmit.contentLength = 0;
    xmit.chunked = false;
    xmit.request = 0;
    xmit.ptr = 0;
    xmit.startHdr = 0;
    xmit.startHost = 0;
    xmit.endHost = 0;
    xmit.fragmentOffset = 0;
    xmit.fileName = 0;
    xmit.fileNameShort = 0;
    xmit.fileParams = 0;
    xmit.fileParamsShort = 0;
    xmit.cr = false;
    xmit.eod = false;
    xmit.endChunk = false;
    xmit.line = 0;
    xmit.sending = 0;
    xmit.httpGetMethod = false;

    recv.content = false;
    recv.contentLength = 0;
    recv.responseCode = 0;
    recv.responseProcessed = false;
    recv.chunked = false;
    recv.connectionClose = false;
    recv.cr = false;
    recv.eod = false;
    recv.endChunk = false;
    recv.encoding = 0;
    recv.type = 0;
    recv.location = 0;
    recv.cacheControl = 0;
    recv.line = 0;
    recv.payload = 0;
    recv.payloadEol = 0;
    recv.payloadManifest = false;
    recv.hasSent = false;
    recv.sending = 0;

    tunnel.responseCode = 0;
    tunnel.cr = false;
    tunnel.line = 0;

    socket = 0;
    hostIn = 0;
    host = 0;
    hostName = 0;
    hostPort = 0;
    hostAddr = 0;
    hostPath = 0;
}

AppProxyPassthrough::~AppProxyPassthrough()
{
    ASSERT(read == 0, "AppProxyPassthrough::~AppProxyPassthrough read task still set!");
    ASSERT(write == 0, "AppProxyPassthrough::~AppProxyPassthrough write task still set!");
    DBG_PASSTHROUGH(("AppProxyPassthrough(%x)::~AppProxyPassthrough()", this));
    while(!xmit.header.empty()) {
        delete xmit.header.front();
    }
    while(!queue.empty()) {
        delete queue.front();
    }
    if(xmit.fileName) free(xmit.fileName);
    if(xmit.fileParams) free(xmit.fileParams);
    if(xmit.line) free(xmit.line);
    if(xmit.request) free(xmit.request);
    if(recv.encoding) free(recv.encoding);
    if(recv.type) free(recv.type);
    if(recv.location) free(recv.location);
    if(recv.cacheControl) free(recv.cacheControl);
    if(recv.line) free(recv.line);
    if(tunnel.line) free(tunnel.line);
    if(hostIn) free(hostIn);
    if(host) free(host);
    if(hostName) free(hostName);
    if(hostPort) free(hostPort);
    if(hostAddr) free(hostAddr);
    if(hostPath) free(hostPath);
    if(socket) delete socket;
    if(dns) delete dns;
    if(proxyAddr) free(proxyAddr);
    if(proxies) delete proxies;
}

void AppProxyPassthrough::WebserverPassthroughRequestAcceptComplete(IWebserverPassthrough * const webserverPassthrough)
{
    DBG_PASSTHROUGH(("AppProxyPassthrough(%x)::WebserverPassthroughRequestAcceptComplete()", this));
    passthrough = webserverPassthrough;
    tls = passthrough->IsEncryptedConnection();
    passthrough->Recv();
}

void AppProxyPassthrough::WebserverPassthroughSendResult(IWebserverPassthrough * const webserverPassthrough)
{
    DBG_PASSTHROUGH(("AppProxyPassthrough(%x)::WebserverPassthroughSendResult() sending=%u", this, recv.sending));
    recv.sending--;
    if (!recv.sending) {
        if (read) {
            if (!read_started) {
                read->Start(this);
                read_started = true;
                reading = true;
            }
            else {
                if (recv.eod) {
                    if (!reading) {
                        read->read(0, true);
                        reading = true;
                    }
                    if (passthrough && !closing) {
                        passthrough->Close();
                        closing = true;
                    }
                }
                else {
                    if (!reading) {
                        read->read(CHUNKED_TRANSFER_CHUNK_SIZE, false);
                        reading = true;
                    }
                }
            }
        }
        else {
            if (recv.eod) {
                if (!xmit.sending) {
                    if (socket) {
                        if (websocket || recv.connectionClose) {
                            socket->Shutdown();
                        }
                        else {
                            appProxy->socketRetainers.push_front(new AppProxySocketRetainer(appProxy->iomux, appProxy, tls || tlsTranslate, host, hostAddr, socket));
                            SocketShutdown(NULL, SOCKET_SHUTDOWN_NORMAL);
                            if (tryDelete()) {
                                return;
                            }
                        }
                    }
                }
                if (passthrough && !closing) {
                    passthrough->Close();
                    closing = true;
                }
            }
            else {
                if (socket) {
                    unsigned l = recv.contentLength ? MIN(10000, recv.contentLength) : 2000;
                    socket->Recv(malloc(l), l, recv.contentLength ? false : true);
                }
                else if (shutdown) {
                    if (passthrough) {
                        passthrough->Cancel(WSP_CANCEL_STREAM);
                        closing = true;
                    }
                }
            }
        }
    }
}

void AppProxyPassthrough::WebserverPassthroughRecvResult(IWebserverPassthrough * const webserverPassthrough, void * buffer, size_t len)
{
    DBG_PASSTHROUGH(("AppProxyPassthrough(%x)::WebserverPassthroughRecvResult(%u)", this, len));
    if (passthrough && !closing) {
        char * b = (char *)buffer;
        unsigned p = 0;
        if (!xmit.content) {
            xmit.header.push_back(new fragment((byte *)buffer, len));
            while (p < len) {
                bool eol;
                unsigned l;
                char * s = &b[p];
                l = readLine(s, len - p, eol);
                p += l;
                if (xmit.line) {
                    if (xmit.lineLen < MAX_HTTP_LINE) {
                        memcpy(&xmit.line[xmit.lineLen], s, MIN(MAX_HTTP_LINE - xmit.lineLen, l));
                    }
                    xmit.lineLen += l;
                    s = xmit.line;
                    l = xmit.lineLen;
                }
                if (eol) {
                    int p0 = p - l;
                    l = (xmit.line && (l > MAX_HTTP_LINE)) ? MAX_HTTP_LINE : l - (((l > 1) && (s[l-2] == '\r')) ? 2 : 1);
                    if (!xmit.request) {
                        xmitRequest(s, l, xmit.ptr + p0, xmit.ptr + p);
                    }
                    else if (l) {
                        xmitHeader(s, l, xmit.ptr + p0, xmit.ptr + p);
                    }
                    else {
                        if (xmit.line) free(xmit.line);
                        xmit.line = 0;
                        xmit.content = true;
                        xmitEoh();
                        if (!websocket && !xmit.chunked && !xmit.contentLength) {
                            xmit.eod = true;
                        }
                        break;
                    }
                    if (xmit.line) free(xmit.line);
                    xmit.line = 0;
                }
                else if (!xmit.line) {
                    xmit.line = (char *)malloc(MAX_HTTP_LINE);
                    xmit.lineLen = l;
                    memcpy(xmit.line, s, MIN(MAX_HTTP_LINE, l));
                }
            }
            xmit.ptr += len;
            if (!xmit.content) {
                if (passthrough && !closing) {
                    passthrough->Recv();
                }
            }
            else if (p < len) {
                xmitContent(&b[p], len - p);
            }
        }
        else {
            if (socket) {
                socket->Send(buffer, len);
                xmit.sending++;
            }
            xmitContent((char *)buffer, len);
            free(buffer);
        }
    }
    else {
        free(buffer);
    }
}

void * AppProxyPassthrough::WebserverPassthroughRecvBuffer(size_t len)
{
    DBG_PASSTHROUGH(("AppProxyPassthrough(%x)::WebserverPassthroughRecvBuffer(%u)", this, len));
    return malloc(len);
}

void AppProxyPassthrough::WebserverPassthroughRecvCanceled(IWebserverPassthrough * const webserverPassthrough, void * buffer)
{
    free(buffer);
}

void AppProxyPassthrough::WebserverPassthroughCloseComplete(IWebserverPassthrough * const webserverPassthrough)
{
    DBG_PASSTHROUGH(("AppProxyPassthrough(%x)::WebserverPassthroughCloseComplete()", this));
    delete passthrough;
    passthrough = 0;
    if(loadingCacheable) {
        loadingCacheable->loadComplete(this);
        loadingCacheable = NULL;
    }
    recv.sending = 0;
    if (read) {
        if (read_started) {
            if (!reading) {
                read->read(0, true);
                reading = true;
            }
        }
        else {
            delete read;
            read = 0;
        }
    }
    if (write) {
        if (writing) {
            queue.push_back(new fragment(0, 0));
        }
        else {
            write->write(NULL, 0, true);
            writing = true;
        }
    }
    if (!xmit.sending) {
        if (socket) {
            if (websocket || recv.connectionClose || !recv.eod) {
                socket->Shutdown();
            }
            else {
                appProxy->socketRetainers.push_front(new AppProxySocketRetainer(appProxy->iomux, appProxy, tls || tlsTranslate, host, hostAddr, socket));
                SocketShutdown(NULL, SOCKET_SHUTDOWN_NORMAL);
            }
        }
    }
    tryDelete();
}

void AppProxyPassthrough::LoadCacheable(bool reuse)
{
    char b[2048];
    class AppProxyManifestFile * f = loadingFile;
    loadingFile = NULL;
    if (f && f->loaded && (reuse || !appProxy->ConnectivityUp())) {
        ((IInstanceLog *)appProxy)->Log(LOG_APPPROXY, "Reused(%s%s://%s%s)", (websocket ? "ws" : "http"), (tls ? "s" : ""), xmit.fileName, ((loadingCacheable && xmit.fileParamsShort) ? xmit.fileParamsShort : ""));
        readingManifest = NULL;
        readingFile = f;
        _sprintf(b, "%s/%s.%s", xmit.fileName, f->name, mimeToFileExtension(f->type));
        read = new TaskFileRead(appProxy, appProxy->workingPath, urlPathToFilePath(b), CHUNKED_TRANSFER_CHUNK_SIZE, false);
        if (!recv.sending) {
            read->Start(this);
            read_started = true;
            reading = true;
        }
    }
    else {
        Forward();
    }
}

void AppProxyPassthrough::SocketConnectComplete(ISocket * const socket)
{
    DBG_PASSTHROUGH(("AppProxyPassthrough(%x)::SocketConnectComplete()", this));
    connectTimer.Cancel();
    if (connect_tunnel) {
        int len;
        ((IInstanceLog *)appProxy)->Log(LOG_APPPROXY, "Connected(%s%s://%s%s) proxy", (websocket ? "ws" : "http"), (tls ? "s" : ""), xmit.fileName, ((loadingCacheable && xmit.fileParamsShort) ? xmit.fileParamsShort : ""));
        char buf[1024];
        if (hostName) {
            char * h = (char *)(alloca(strlen(hostName) + 1));
            UrlDecodeSimple(hostName, h);
            len = snprintf(buf, sizeof(buf),"CONNECT %s%s HTTP/1.1\r\nHost: %s%s\r\n\r\n",
                h, hostPort ? hostPort : ((tls || tlsTranslate) ? ":443" : ":80"),
                h, hostPort ? hostPort : ((tls || tlsTranslate) ? ":443" : ":80"));
        }
        else {
            len = snprintf(buf, sizeof(buf), "CONNECT %s HTTP/1.1\r\nHost: %s\r\n\r\n", hostAddr, hostAddr);
        }
        socket->Send(buf, len);
        xmit.sending++;
    }
    else {
        ((IInstanceLog *)appProxy)->Log(LOG_APPPROXY, "Connected(%s%s://%s%s)", (websocket ? "ws" : "http"), (tls ? "s" : ""), xmit.fileName, ((loadingCacheable && xmit.fileParamsShort) ? xmit.fileParamsShort : ""));
        socket->Send(xmit.request, xmit.requestLen);
        xmit.sending++;
        headerSend(xmit.startHdr, false);
        headerSend(xmit.startHost - xmit.startHdr, true);
        headerSend(xmit.endHost - xmit.startHost, false);
        headerSend(0xffffffff, true);
    }
    socket->Recv(malloc(2000), 2000, true);
}

void AppProxyPassthrough::SocketUpgradeToTLSComplete(ISocket* const socket)
{
    SocketConnectComplete(socket);
}

void AppProxyPassthrough::TimerOnTimeout(AppProxyPassthroughConnectTimer * timer)
{
    ((IInstanceLog *)appProxy)->Log(LOG_APPPROXY, "Abort(%s%s://%s%s)", (websocket ? "ws" : "http"), (tls ? "s" : ""), xmit.fileName, ((loadingCacheable && xmit.fileParamsShort) ? xmit.fileParamsShort : ""));
    socket->Shutdown();
}

void AppProxyPassthrough::TimerOnTimeout(AppProxyPassthroughRetryTimer * timer)
{
    if (passthrough && !closing) {
        Forward();
    }
}

void AppProxyPassthrough::SocketShutdown(ISocket * const socket, shutdownreason_t reason)
{
    bool unsuitableAddress = false;
    DBG_PASSTHROUGH(("AppProxyPassthrough(%x)::SocketShutdown()", this));
    connectTimer.Cancel();
    if (socket) delete socket;
    this->socket = 0;
    if (connect_tunnel) {
        if (dns && proxy->host && proxyAddr) {
            char b[256];
            const char *p = proxyAddr;
            if (*p == '[') {
                do {
                    p++;
                }
                while ((*p != '\0') && (*p != ']'));
                memcpy(b, &proxyAddr[1], p - &proxyAddr[1]);
                b[p - &proxyAddr[1]] = '\0';
                if (*p != '\0') p++;
            }
            else {
                while ((*p != '\0') && (*p != '.')) p++;
                while ((*p != '\0') && (*p != ':')) p++;
                memcpy(b, proxyAddr, p - proxyAddr);
                b[p - proxyAddr] = '\0';
            }
            dns->UnsuitableAddress(proxy->host, appProxy, b, (*p == ':') ? atoi(p+1) : 80);
        }
        proxy = proxy->goNext();
        ConnectProxy();
        return;
    }
    xmit.sending = 0;
    shutdown = true;
    if (recv.sending) {
        if (passthrough && !recv.eod && (!websocket || !recv.content)) {
            unsuitableAddress = true;
            ((IInstanceLog *)appProxy)->Log(LOG_APPPROXY, "Failed(%s%s://%s%s) %u", (websocket ? "ws" : "http"), (tls ? "s" : ""), xmit.fileName, ((loadingCacheable && xmit.fileParamsShort) ? xmit.fileParamsShort : ""), reason);
        }
    }
    else {
        if (passthrough) {
            if (!xmit.header.empty() && !websocket && (retryInterval < 16000) && appProxy->findManifestFile(appProxy->findManifest(xmit.fileName), xmit.fileName)) {
                unsuitableAddress = true;
                shutdown = false;
                ((IInstanceLog *)appProxy)->Log(LOG_APPPROXY, "Retry(%s%s://%s%s) %u after %u ms", (websocket ? "ws" : "http"), (tls ? "s" : ""), xmit.fileName, ((loadingCacheable && xmit.fileParamsShort) ? xmit.fileParamsShort : ""), reason, retryInterval);
                retryTimer.Start(retryInterval);
                retryInterval *= 2;
            }
            else if (!recv.hasSent) {
                unsuitableAddress = true;
                ((IInstanceLog *)appProxy)->Log(LOG_APPPROXY, "Failed(%s%s://%s%s) %u", (websocket ? "ws" : "http"), (tls ? "s" : ""), xmit.fileName, ((loadingCacheable && xmit.fileParamsShort) ? xmit.fileParamsShort : ""), reason);
                passthrough->Send(AppProxy503ServiceUnavailable, sizeof(AppProxy503ServiceUnavailable) - 1);
                recv.sending++;
                recv.hasSent = true;
                recv.eod = true;
            }
            else {
                if (recv.eod) {
                    if (!closing) {
                        passthrough->Close();
                        closing = true;
                    }
                }
                else {
                    if (!websocket || !recv.content) {
                        unsuitableAddress = true;
                        ((IInstanceLog *)appProxy)->Log(LOG_APPPROXY, "Failed(%s%s://%s%s) %u", (websocket ? "ws" : "http"), (tls ? "s" : ""), xmit.fileName, ((loadingCacheable && xmit.fileParamsShort) ? xmit.fileParamsShort : ""), reason);
                    }
                    passthrough->Cancel(WSP_CANCEL_STREAM);
                    closing = true;
                }
            }
        }
    }
    if (unsuitableAddress && dns && hostName && hostAddr) {
        char b[256];
        const char *p = hostAddr;
        if (*p == '[') {
            do {
                p++;
            }
            while ((*p != '\0') && (*p != ']'));
            memcpy(b, &hostAddr[1], p - &hostAddr[1]);
            b[p - &hostAddr[1]] = '\0';
            if (*p != '\0') p++;
        }
        else {
            while ((*p != '\0') && (*p != '.')) p++;
            while ((*p != '\0') && (*p != ':')) p++;
            memcpy(b, hostAddr, p - hostAddr);
            b[p - hostAddr] = '\0';
        }
        char * h = (char *)(alloca(strlen(hostName) + 1));
        UrlDecodeSimple(hostName, h);
        dns->UnsuitableAddress(h, appProxy, b, (*p == ':') ? atoi(p+1) : ((tls || tlsTranslate) ? 443 : 80));
    }
    if (socket) tryDelete();
}

void AppProxyPassthrough::SocketRecvCanceled(ISocket * const socket, void * buf)
{
    DBG_PASSTHROUGH(("AppProxyPassthrough(%x)::SocketRecvCanceled() buf=%p", this, buf));
    free(buf);
}

void AppProxyPassthrough::SocketSendResult(ISocket * const socket)
{
    DBG_PASSTHROUGH(("AppProxyPassthrough(%x)::SocketSendResult() xmit.sending=%u, websocket=%x, xmit.contentLength=%u, xmit.chunked=%x", this, xmit.sending, websocket, xmit.contentLength, xmit.chunked));
    xmit.sending--;
    if (!xmit.sending && !connect_tunnel) {
        if (passthrough && !closing) {
            if (websocket) {
                DBG_WEBSOCKET(("Websocket(%x) SocketSendResult() recv.content=%x", this, recv.content));
                if (recv.content) {
                    passthrough->Recv();
                }
            }
            else if (xmit.contentLength || xmit.chunked) {
                passthrough->Recv();
            }
        }
        else {
            if (socket) {
                if (websocket || recv.connectionClose || !recv.eod) {
                    socket->Shutdown();
                }
                else {
                    appProxy->socketRetainers.push_front(new AppProxySocketRetainer(appProxy->iomux, appProxy, tls || tlsTranslate, host, hostAddr, socket));
                    SocketShutdown(NULL, SOCKET_SHUTDOWN_NORMAL);
                    if (tryDelete()) {
                        return;
                    }
                }
            }
        }
    }
}

void AppProxyPassthrough::SocketRecvResult(ISocket * const socket, void * buf, size_t len)
{
    DBG_PASSTHROUGH(("AppProxyPassthrough(%x)::SocketRecvResult(%u)", this, len));
    char * b = (char *)buf;

    if (passthrough && !closing) {
        unsigned p = 0;
        if (connect_tunnel) {
            while (p < len) {
                bool eol;
                unsigned l;
                char * s = &b[p];
                l = readLine(s, len - p, eol);
                p += l;
                if (tunnel.line) {
                    if (tunnel.lineLen < MAX_HTTP_LINE) {
                        memcpy(&tunnel.line[tunnel.lineLen], s, MIN(MAX_HTTP_LINE - tunnel.lineLen, l));
                    }
                    tunnel.lineLen += l;
                    s = tunnel.line;
                    l = tunnel.lineLen;
                }
                if (eol) {
                    l = (tunnel.line && (l > MAX_HTTP_LINE)) ? MAX_HTTP_LINE : l - (((l > 1) && (s[l-2] == '\r')) ? 2 : 1);
                    if (l) {
                        tunnelHeader(s, l);
                    }
                    else {
                        if (tunnel.line) free(tunnel.line);
                        tunnel.line = 0;
                        tunnelEoh();
                        free(buf);
                        return;
                    }
                    if (tunnel.line) free(tunnel.line);
                    tunnel.line = 0;
                }
                else if (!tunnel.line) {
                    tunnel.line = (char *)malloc(MAX_HTTP_LINE);
                    tunnel.lineLen = l;
                    memcpy(tunnel.line, s, MIN(MAX_HTTP_LINE, l));
                }
            }
            socket->Recv(malloc(2000), 2000, true);
            free(buf);
            return;
        }
        if (!recv.content) {
            while (p < len) {
                bool eol;
                unsigned l;
                char * s = &b[p];
                l = readLine(s, len - p, eol);
                p += l;
                if (recv.line) {
                    if (recv.lineLen < MAX_HTTP_LINE) {
                        memcpy(&recv.line[recv.lineLen], s, MIN(MAX_HTTP_LINE - recv.lineLen, l));
                    }
                    recv.lineLen += l;
                    s = recv.line;
                    l = recv.lineLen;
                }
                if (eol) {
                    l = (recv.line && (l > MAX_HTTP_LINE)) ? MAX_HTTP_LINE : l - (((l > 1) && (s[l-2] == '\r')) ? 2 : 1);
                    if (l) {
                        recvHeader(s, l);
                    }
                    else {
                        if (recv.line) free(recv.line);
                        recv.line = 0;
                        recv.content = true;
                        recvEoh();
                        if (!websocket && !recv.chunked && !recv.contentLength) {
                            recv.eod = true;
                            recvPayload(0, 0);
                        }
                        break;
                    }
                    if (recv.line) free(recv.line);
                    recv.line = 0;
                }
                else if (!recv.line) {
                    recv.line = (char *)malloc(MAX_HTTP_LINE);
                    recv.lineLen = l;
                    memcpy(recv.line, s, MIN(MAX_HTTP_LINE, l));
                }
            }
        }
        if (recv.content && (p < len)) {
            passthrough->Send(&b[p], len - p);
            recv.sending++;
            recvContent(&b[p], len - p);
        }
        if (!recv.sending) {
            if (recv.eod) {
                if (!xmit.sending) {
                    if (websocket || recv.connectionClose) {
                        socket->Shutdown();
                    }
                    else {
                        appProxy->socketRetainers.push_front(new AppProxySocketRetainer(appProxy->iomux, appProxy, tls || tlsTranslate, host, hostAddr, socket));
                        SocketShutdown(NULL, SOCKET_SHUTDOWN_NORMAL);
                        if (tryDelete()) {
                            free(buf);
                            return;
                        }
                    }
                }
                if (passthrough && !closing) {
                    passthrough->Close();
                    closing = true;
                }
            }
            else {
                unsigned l = recv.contentLength ? MIN(10000, recv.contentLength) : 2000;
                socket->Recv(malloc(l), l, recv.contentLength ? false : true);
            }
        }
    }
    free(buf);
}

void AppProxyPassthrough::TaskComplete(class ITask * const task)
{
    DBG_PASSTHROUGH(("AppProxyPassthrough(%x)::TaskComplete()", this));
    if (task == write) {
        delete write;
        write = 0;
        if (recv.eod) {
            if (loadingManifest) {
                loadingManifest->fileLoadComplete(loadingFile);
            }
            else {
                loadingFile->loading = false;
                loadingFile->loaded = true;
            }
        }
        else {
            if (loadingManifest) {
                loadingManifest->fileLoadFailed(loadingFile);
            }
            else {
                loadingFile->loading = false;
            }
        }
        loadingManifest = 0;
        loadingFile = 0;
        tryDelete();
    }
    else if (task == read) {
        delete read;
        read = 0;
        if (readingManifest) {
            readingManifest->fileReadComplete(readingFile);
        }
        readingFile = 0;
        tryDelete();
    }
}

void AppProxyPassthrough::TaskFailed(class ITask * const task)
{
    DBG_PASSTHROUGH(("AppProxyPassthrough(%x)::TaskFailed()", this));
    if (task == write) {
        delete write;
        write = 0;
        if (loadingManifest) {
            loadingManifest->fileLoadFailed(loadingFile);
        }
        else {
            loadingFile->loading = false;
        }
        loadingManifest = 0;
        loadingFile = 0;
        tryDelete();
    }
    else if (task == read) {
        delete read;
        read = 0;
        if (readingManifest) {
            readingManifest->fileReadComplete(readingFile);
        }
        readingFile = 0;
        if (!recv.hasSent) {
            ((IInstanceLog *)appProxy)->Log(LOG_APPPROXY, "Cached(%s%s://%s%s) unavailable", (websocket ? "ws" : "http"), (tls ? "s" : ""), xmit.fileName, ((loadingCacheable && xmit.fileParamsShort) ? xmit.fileParamsShort : ""));
            Forward();
        }
        else if (!recv.eod) {
            if (passthrough) {
                passthrough->Cancel(WSP_CANCEL_STREAM);
                closing = true;
            }
        }
        tryDelete();
    }
}

void AppProxyPassthrough::TaskProgress(class ITask * const task, dword progress)
{
    if (task == write) {
        DBG_PASSTHROUGH(("AppProxyPassthrough(%x)::TaskProgress(write)", this));
        if (queue.front()) {
            class fragment * f = queue.pop_front();
            write->writeBuffer(f->buffer, f->len, f->buffer ? false : true);
            f->buffer = 0;
            delete f;
        }
        else {
            writing = false;
        }
    }
    else if (task == read) {
        DBG_PASSTHROUGH(("AppProxyPassthrough(%x)::TaskProgress(read)", this));
        unsigned len = read->getLength();
        char * b = (char *)alloca(len + 256);
        byte * p;
        unsigned l;
        if (len) {
            l = _sprintf(b, "\r\n%x\r\n", len);
            p = (byte *)&b[l];
            read->getData(p, len);
            l += len;
        }
        else {
            l = 0;
            p = (byte *)b;
        }
        if (len < CHUNKED_TRANSFER_CHUNK_SIZE) {
            l += _sprintf(&b[l], "\r\n0\r\n\r\n");
            recv.eod = true;
        }
        reading = false;
        if (passthrough && !closing) {
            if (!recv.hasSent) {
                unsigned i;
                if (readingFile->encoding && (((p[0] == 0x1f) && (p[1] == 0x8b)) || (strcmp(readingFile->encoding, "gzip") != 0))) {
                    i = _sprintf(&b[l], "HTTP/1.1 200 OK\r\nCache-Control: max-age=60\r\nContent-type: %s\r\nContent-Encoding: %s\r\nTransfer-Encoding: chunked\r\n", readingFile->type, readingFile->encoding);
                }
                else {
                    i = _sprintf(&b[l], "HTTP/1.1 200 OK\r\nCache-Control: %s\r\nContent-type: %s\r\nTransfer-Encoding: chunked\r\n",
                        (str::n_casecmp(readingFile->type, "text/html", 9) == 0) ? "no-cache, no-store" : "max-age=60", readingFile->type);
                }
                passthrough->Send(&b[l], i);
                recv.sending++;
                recv.hasSent = true;
            }
            passthrough->Send(b, l);
            recv.sending++;
        }
        else {
            read->read(0, true);
            reading = true;
        }
    }
}

void AppProxyPassthrough::DnsGetHostByNameResult(const char * addr, bool isIPv6)
{
    unsigned l;
    DBG_PASSTHROUGH(("AppProxyPassthrough(%x)::DnsGetHostByNameResult(%s %u)", this, addr, isIPv6));
    dnsPending = false;
    if (passthrough && !closing && !shutdown) {
        char b[256];
        if (!addr) {
            if (connect_tunnel) {
                proxy = proxy->goNext();
                ConnectProxy();
                return;
            }
            ((IInstanceLog *)appProxy)->Log(LOG_APPPROXY, "Appproxy host name couldn't be resolved (%s)", hostName);
            if (!recv.hasSent) {
                l = strlen(xmit.fileName);
                if ((l >= 4) && (strcasecmp(xmit.fileName + (l-4), ".png") == 0)) {
                    l = sprintf(b, "HTTP/1.1 200 OK\r\n"
                        "Cache-Control: no-cache, no-store\r\n"
                        "expires: Friday, 25-Jul-97 00:00:00 GMT\r\n"
                        "Pragma: no-cache\r\n"
                        "Content-type: %s\r\n"
                        "Content-length: %u\r\n\r\n",
                        "image/png", (unsigned)(sizeof(dummy_png)));
                    passthrough->Send(b, l);
                    passthrough->Send(dummy_png, sizeof(dummy_png));
                    recv.sending++;
                }
                else {
                    passthrough->Send(AppProxy503ServiceUnavailable, sizeof(AppProxy503ServiceUnavailable) - 1);
                }
                recv.sending++;
                recv.hasSent = true;
                recv.eod = true;
            }
            else if (!recv.eod) {
                passthrough->Cancel(WSP_CANCEL_STREAM);
                closing = true;
                tryDelete();
            }
        }
        else {
            if (connect_tunnel) {
                _sprintf(b, isIPv6 ? "[%s]:%u" : "%s:%u", addr, (proxy->port != 0) ? proxy->port : 80);
                if (proxyAddr) free(proxyAddr);
                proxyAddr = _strdup(b);
                ((IInstanceLog *)appProxy)->Log(LOG_APPPROXY, "Forward(%s%s://%s%s) proxy... %s%s://%s %s", (websocket ? "ws" : "http"), (tls ? "s" : ""), xmit.fileName, ((loadingCacheable && xmit.fileParamsShort) ? xmit.fileParamsShort : ""), (websocket ? "ws" : "http"), ((tls || tlsTranslate) ? "s" : ""), proxy->host, proxyAddr);
                socket = tcpSocketProvider->CreateSocket(appProxy->service->iomux, this, appProxy, isIPv6);
                char * h = NULL;
                if (hostName) {
                    h = (char *)(alloca(strlen(hostName) + 1));
                    UrlDecodeSimple(hostName, h);
                }
                socket->Connect(proxyAddr, h);
                if (websocket) {
                    connectTimer.Start(APPPROXY_WEBSOCKET_CONNECT_TIMEOUT);
                }
            }
            else {
                _sprintf(b, isIPv6 ? "[%s]%s" : "%s%s", addr, hostPort ? hostPort : ((tls || tlsTranslate) ? ":443" : ":80"));
                if (hostAddr) free(hostAddr);
                hostAddr = _strdup(b);
                class AppProxySocketRetainer * r = NULL;
                if (!websocket && hostAddr) {
                    if (!appProxy->socketRetainers.empty()) {
                        r = appProxy->socketRetainers.front();
                        while (r && ((r->tls != (tls || tlsTranslate)) || (strcmp(r->host, host) != 0))) {
                            r = r->goNext();
                        }
                    }
                }
                if (r) {
                    ((IInstanceLog *)appProxy)->Log(LOG_APPPROXY, "Forward(%s%s://%s%s) to %s%s://%s %s", (websocket ? "ws" : "http"), (tls ? "s" : ""), xmit.fileName, ((loadingCacheable && xmit.fileParamsShort) ? xmit.fileParamsShort : ""), (websocket ? "ws" : "http"), ((tls || tlsTranslate) ? "s" : ""), host, hostAddr);
                    socket = r->socket;
                    delete r;
                    socket->SetUser(this);
                    SocketConnectComplete(socket);
                }
                else {
                    ((IInstanceLog *)appProxy)->Log(LOG_APPPROXY, "Forward(%s%s://%s%s) to... %s%s://%s %s", (websocket ? "ws" : "http"), (tls ? "s" : ""), xmit.fileName, ((loadingCacheable && xmit.fileParamsShort) ? xmit.fileParamsShort : ""), (websocket ? "ws" : "http"), ((tls || tlsTranslate) ? "s" : ""), host, hostAddr);
                    socket = ((tls || tlsTranslate) ? tlsSocketProvider : tcpSocketProvider)->CreateSocket(appProxy->service->iomux, this, appProxy, isIPv6);
                    char * h = NULL;
                    if (hostName) {
                        h = (char *)(alloca(strlen(hostName) + 1));
                        UrlDecodeSimple(hostName, h);
                    }
                    socket->Connect(hostAddr, h);
                    if (websocket) {
                        connectTimer.Start(APPPROXY_WEBSOCKET_CONNECT_TIMEOUT);
                    }
                }
            }
        }
    }
    else {
        tryDelete();
    }
}

void AppProxyPassthrough::xmitRequest(char * s, unsigned l, unsigned start, unsigned end)
{
    DBG_PASSTHROUGH(("AppProxyPassthrough(%x)::xmitRequest(start=%u, end=%u [%.*s])", this, start, end, l, s));
    unsigned p;
    unsigned methodEnd = 0;
    unsigned hostStart = 0;
    unsigned hostEnd = 0;
    for (p = 0; p < l && s[p] != ' '; p++);
    while (p < l && s[p++] != '/');
    methodEnd = p;
    if (methodEnd > 3 && str::n_casecmp(s, "GET ", 4) == 0) {    // let's see if this is a GET request
        xmit.httpGetMethod = true;
    }
    if (appProxy->webserverPath[1] != '\0') {
        unsigned w = 0;
        while (appProxy->webserverPath[w] == '/') {
            while (p < l && s[p++] != '/');
            do {
                w++;
            }
            while (appProxy->webserverPath[w] != '\0' && appProxy->webserverPath[w] != '/');
        }
    }
    while (p < l && s[p++] != '/');
    hostStart = p;
    while (p < l && s[p] != '/' && s[p] != ' ') p++;
    unsigned path = (p < l && s[p] == '/') ? 1 : 0;
    hostEnd = p;
    while (p < l && s[p] != ' ' && s[p] != '?') p++;
    unsigned fileEnd = p;
    while (p < l && s[p] != ' ') p++;
    unsigned paramsEnd = p;
    xmit.requestLen = methodEnd + (l - hostStart) + 10 - path;
    if(xmit.request) free(xmit.request);
    xmit.request = (char *)malloc(xmit.requestLen);
    unsigned o = 0;
    memcpy(xmit.request, s, methodEnd);
    o += methodEnd;
    memcpy(&xmit.request[o], &s[hostEnd + path], l - hostEnd - path);
    o += l - hostEnd - path;
    memcpy(&xmit.request[o], "\r\nHost: ", 8);
    o += 8;
    memcpy(&xmit.request[o], &s[hostStart], hostEnd - hostStart);
    xmit.request[o + hostEnd - hostStart] = '\0';
    bool dns;
    unsigned h = readHost(&xmit.request[o], dns, ip6);
    if(host) free(host);
    host = (char *)malloc(hostEnd - hostStart + 9);
    if (!xmit.request[o + h] || (tlsTranslate && (strcmp(&host[h], ":80") == 0))) {
        if (ip6 && (xmit.request[o] != '[')) {
            host[0] = '[';
            memcpy(&host[1], &xmit.request[o], h);
            host[1 + h] = ']';
            h += 2;
        }
        else {
            memcpy(host, &xmit.request[o], h);
        }
        strcpy(&host[h], (tls || tlsTranslate) ? ":443" : ":80");
    }
    else {
        host = _strdup(&xmit.request[o]);
    }
    if (hostName) {
        free(hostName);
        hostName = 0;
    }
    if (hostPort) {
        free(hostPort);
        hostPort = 0;
    }
    if (hostAddr) {
        free(hostAddr);
        hostAddr = 0;
    }
    if (dns) {
        hostName = (char *)malloc(h + 1);
        hostPort = _strdup(&host[h]);
        memcpy(hostName, host, h);
        hostName[h] = '\0';
    }
    o += hostEnd - hostStart;
    memcpy(&xmit.request[o], "\r\n", 2);

    if(xmit.fileName) free(xmit.fileName);
    xmit.fileName = (char *)memcpy(malloc(fileEnd - methodEnd + 1), &s[methodEnd], fileEnd - methodEnd);
    xmit.fileName[fileEnd - methodEnd] = '\0';
    for (p = fileEnd - methodEnd; p && xmit.fileName[p-1] != '.'; p--);
    const char * ext = &xmit.fileName[p];
    xmit.fileTypeText = (p &&
        ((strcasecmp(ext, "htm") == 0) || (strcasecmp(ext, "html") == 0) || (strcasecmp(ext, "js") == 0) || (strcasecmp(ext, "css") == 0)));
    xmit.fileNameShort = &xmit.fileName[hostStart - methodEnd];

    if(xmit.fileParams) free(xmit.fileParams);
    unsigned paramsShortEnd = fileEnd;
    while ((paramsShortEnd < paramsEnd) && (s[paramsShortEnd] != '&')) paramsShortEnd++;
    xmit.fileParams = (char *)memcpy(malloc(paramsEnd - fileEnd + 1 + paramsShortEnd - fileEnd + ((paramsShortEnd < paramsEnd) ? 5 : 1)), &s[fileEnd], paramsEnd - fileEnd);
    xmit.fileParams[paramsEnd - fileEnd] = '\0';
    xmit.fileParamsShort = &xmit.fileParams[paramsEnd - fileEnd + 1];
    memcpy(xmit.fileParamsShort, &s[fileEnd], paramsShortEnd - fileEnd);
    if (paramsShortEnd < paramsEnd) {
        xmit.fileParamsShort[paramsShortEnd - fileEnd] = '&';
        xmit.fileParamsShort[paramsShortEnd - fileEnd + 1] = '.';
        xmit.fileParamsShort[paramsShortEnd - fileEnd + 2] = '.';
        xmit.fileParamsShort[paramsShortEnd - fileEnd + 3] = '.';
        xmit.fileParamsShort[paramsShortEnd - fileEnd + 4] = '\0';
    }
    else {
        xmit.fileParamsShort[paramsShortEnd - fileEnd] = '\0';
    }

    for (p = fileEnd; p > hostEnd && s[p]!='/'; p--);
    if(hostPath) free(hostPath);
    hostPath = (char *)malloc(2 + p - hostEnd);
    memcpy(hostPath, &s[hostEnd], 1 + p - hostEnd);
    hostPath[1 + p - hostEnd] = '\0';
    xmit.startHdr = end;
}

void AppProxyPassthrough::xmitHeader(char * s, unsigned l, unsigned start, unsigned end)
{
    unsigned i;
    DBG_PASSTHROUGH(("AppProxyPassthrough(%x)::xmitHeader(start=%u, end=%u [%.*s])", this, start, end, l, s));
    if ((l >= 5) && !str::n_casecmp(s, "host:", 5)) {
        xmit.startHost = start;
        xmit.endHost = end;
        if(hostIn) free(hostIn);
        hostIn = (char *)malloc(l - 5);
        strncpy(hostIn, &s[6], l - 6);
        hostIn[l - 6] = '\0';
    }
    else if ((l >= 15) && !str::n_casecmp(s, "content-length:", 15)) {
        xmit.contentLength = strtoul(&s[15], 0, 0);
    }
    else if ((l >= 18 + 7) && !str::n_casecmp(s, "transfer-encoding:", 18)) {
        i = 18;
        while ((i <= l - 7) && str::n_casecmp(&s[i], "chunked", 7)) {
            i++;
        }
        if (i <= l - 7) {
            xmit.chunked = true;
            xmit.contentLength = 0;
        }
    }
}

void AppProxyPassthrough::Forward()
{
    if (!proxies) {
        char buf[1024];
        _sprintf(buf, "%s%s://%s", (websocket ? "ws" : "http"), ((tls || tlsTranslate) ? "s" : ""), host);
        proxies = INetwork::GetHTTPProxiesForURL(buf);
        proxy = proxies->httpProxies.empty() ? NULL : proxies->httpProxies.front();
    }
    class AppProxySocketRetainer * r = NULL;
    if (!websocket) {
        if (!appProxy->socketRetainers.empty()) {
            r = appProxy->socketRetainers.front();
            while (r && ((r->tls != (tls || tlsTranslate)) || (strcmp(r->host, host) != 0))) {
                r = r->goNext();
            }
        }
    }
    if (r) {
        ((IInstanceLog *)appProxy)->Log(LOG_APPPROXY, "Forward(%s%s://%s%s) to %s%s://%s", (websocket ? "ws" : "http"), (tls ? "s" : ""), xmit.fileName, ((loadingCacheable && xmit.fileParamsShort) ? xmit.fileParamsShort : ""), (websocket ? "ws" : "http"), ((tls || tlsTranslate) ? "s" : ""), host);
        socket = r->socket;
        delete r;
        socket->SetUser(this);
        SocketConnectComplete(socket);
    }
    else {
        ConnectProxy();
    }
}

void AppProxyPassthrough::ConnectProxy()
{
    while (proxy && (proxy->type != HTTP_PROXY_TYPE_HTTP) && (proxy->type != HTTP_PROXY_TYPE_HTTPS)) {
        proxy = proxy->goNext();
    }
    if (proxy) {
        connect_tunnel = true;
        char * p = 0;
        str::to_ip(proxy->host, &p, /*prefix*/0);
        if (p != proxy->host) {
            bool isIPv6 = (strchr(proxy->host, ':') !=  NULL);
            if (proxyAddr) free(proxyAddr);
            proxyAddr = (char *)(malloc(100));
            _sprintf(proxyAddr, isIPv6 ? "[%s]:%u" : "%s:%u", proxy->host, (proxy->port != 0) ? proxy->port : 80);
            ((IInstanceLog *)appProxy)->Log(LOG_APPPROXY, "Forward(%s%s://%s%s) proxy... %s%s://%s %s", (websocket ? "ws" : "http"), (tls ? "s" : ""), xmit.fileName, ((loadingCacheable && xmit.fileParamsShort) ? xmit.fileParamsShort : ""), (websocket ? "ws" : "http"), ((tls || tlsTranslate) ? "s" : ""), proxy->host, proxyAddr);
            socket = tcpSocketProvider->CreateSocket(appProxy->service->iomux, this, appProxy, isIPv6);
            char * h = NULL;
            if (hostName) {
                h = (char *)(alloca(strlen(hostName) + 1));
                UrlDecodeSimple(hostName, h);
            }
            socket->Connect(proxyAddr, h);
            if (websocket) {
                connectTimer.Start(APPPROXY_WEBSOCKET_CONNECT_TIMEOUT);
            }
        }
        else {
            if (!dns) {
                dns = IDns::Create(appProxy->iomux);
            }
            dns->GetHostByName(proxy->host, this, appProxy, DNS_DEFAULT_TIMEOUT, false, proxy->port);
            dnsPending = true;
        }
    }
    else {
        connect_tunnel = false;
        if (hostName) {
            if (!dns) {
                dns = IDns::Create(appProxy->iomux);
            }
            char * b = (char *)(alloca(strlen(hostName) + 1));
            UrlDecodeSimple(hostName, b);
            dns->GetHostByName(b, this, appProxy, DNS_DEFAULT_TIMEOUT, false, hostPort ? atoi(&hostPort[1]) : ((tls || tlsTranslate) ? 443 : 80));
            dnsPending = true;
        }
        else {
            ((IInstanceLog *)appProxy)->Log(LOG_APPPROXY, "Forward(%s%s://%s%s) to... %s%s://%s", (websocket ? "ws" : "http"), (tls ? "s" : ""), xmit.fileName, ((loadingCacheable && xmit.fileParamsShort) ? xmit.fileParamsShort : ""), (websocket ? "ws" : "http"), ((tls || tlsTranslate) ? "s" : ""), host);
            socket = ((tls || tlsTranslate) ? tlsSocketProvider : tcpSocketProvider)->CreateSocket(appProxy->service->iomux, this, appProxy, ip6);
            char * h = NULL;
            if (hostName) {
                h = (char *)(alloca(strlen(hostName) + 1));
                UrlDecodeSimple(hostName, h);
            }
            socket->Connect(host, h);
            if (websocket) {
                connectTimer.Start(APPPROXY_WEBSOCKET_CONNECT_TIMEOUT);
            }
        }
    }
}

void AppProxyPassthrough::xmitEoh()
{
    char b[2048];
    DBG_PASSTHROUGH(("AppProxyPassthrough(%x)::xmitEoh(%s%s://%s%s)", this, (websocket ? "ws" : "http"), (tls ? "s" : ""), xmit.fileName, ((loadingCacheable && xmit.fileParamsShort) ? xmit.fileParamsShort : "")));
    time_t t = ITime::TimeStampMilliseconds() / 1000;
    class AppProxyManifest * m = NULL;
    class AppProxyCacheable * c = NULL;
    class AppProxyManifestFile * f = NULL;
    UpdateConnectionMonitor();
    if (xmit.httpGetMethod && !websocket) {
        m = appProxy->findManifest(xmit.fileName);
        f = appProxy->findManifestFile(m, xmit.fileName);
        if (!f && xmit.fileParams) {
            c = appProxy->findCacheable(xmit.fileName);
            if (c) {
                f = appProxy->findAddCacheableFile(c, xmit.fileName, xmit.fileParams, NULL);
                if (f) {
                    m = NULL;
                }
            }
        }
    }
    if (f && !m) {
        loadingCacheable = c;
        loadingFile = f;
        c->load(this, !f->loaded || (((time_t)(t - f->lastWriteTime)) >= APPPROXY_CACHEABLE_RELOAD_TIME) || (((time_t)(t - f->lastWriteTime)) < APPPROXY_CACHEABLE_REUSE_TIME) || !appProxy->ConnectivityUp(),
            (((time_t)(t - f->lastWriteTime)) < APPPROXY_CACHEABLE_RELOAD_TIME));
    }
    else if (f && f->loaded) {
        ((IInstanceLog *)appProxy)->Log(LOG_APPPROXY, "Cached(%s%s://%s%s)", (websocket ? "ws" : "http"), (tls ? "s" : ""), xmit.fileName, ((loadingCacheable && xmit.fileParamsShort) ? xmit.fileParamsShort : ""));
        readingManifest = m;
        readingFile = f;
        if (m) {
            m->fileReadStarted(f);
            read = new TaskFileRead(appProxy, appProxy->workingPath, urlPathToFilePath(xmit.fileName), CHUNKED_TRANSFER_CHUNK_SIZE, false);
        }
        else {
            _sprintf(b, "%s/%s.%s", xmit.fileName, f->name, mimeToFileExtension(f->type));
            read = new TaskFileRead(appProxy, appProxy->workingPath, urlPathToFilePath(b), CHUNKED_TRANSFER_CHUNK_SIZE, false);
        }
        if (!recv.sending) {
            read->Start(this);
            read_started = true;
            reading = true;
        }
    }
    else if (xmit.httpGetMethod && !websocket && ((m = appProxy->findRedirect(xmit.fileName)) != NULL) && m->findFile(&xmit.fileName[m->redirect.redirectLen]) && m->loadComplete()) {
        const char *p;
        unsigned l;
        l = 0;
        for (p = &xmit.fileName[m->redirect.redirectLen]; *p != '\0'; p++) {
            if (*p == '/') {
                b[l++] = '.';
                b[l++] = '.';
                b[l++] = '/';
            }
        }
        if (l == 0) {
            b[l++] = '.';
            b[l++] = '/';
        }
        b[l] = '\0';
        ((IInstanceLog *)appProxy)->Log(LOG_APPPROXY, "Redirect(%s%s://%s%s) to %s%s%s", (websocket ? "ws" : "http"), (tls ? "s" : ""), xmit.fileName, ((loadingCacheable && xmit.fileParamsShort) ? xmit.fileParamsShort : ""), b, &m->path[m->redirect.redirectLen], &xmit.fileName[m->redirect.redirectLen]);
        char prefix[] =
            "HTTP/1.1 307 Temporary Redirect\r\n"
            "location: ";
        char suffix[] =
            "\r\n"
            "Cache-Control: no-cache, no-store\r\n"
            "Content-length: 0\r\n\r\n";
        passthrough->Send(prefix, sizeof(prefix) - 1);
        passthrough->Send(b, l);
        passthrough->Send(&m->path[m->redirect.redirectLen], strlen(&m->path[m->redirect.redirectLen]));
        passthrough->Send(&xmit.fileName[m->redirect.redirectLen], strlen(&xmit.fileName[m->redirect.redirectLen]));
        if (xmit.fileParams[0] != '\0') {
            passthrough->Send(xmit.fileParams, strlen(xmit.fileParams));
            recv.sending++;
        }
        passthrough->Send(suffix, sizeof(suffix) - 1);
        recv.sending += 5;
        recv.hasSent = true;
        recv.eod = true;
    }
    else {
        Forward();
    }
}

void AppProxyPassthrough::xmitContent(char * s, unsigned len)
{
    DBG_PASSTHROUGH(("AppProxyPassthrough(%x)::xmitContent(%u) %u %u", this, len, xmit.chunked, xmit.contentLength));
    if (xmit.chunked) {
        if (xmit.contentLength) {
            if (len >= xmit.contentLength) {
                s += xmit.contentLength;
                len -= xmit.contentLength;
                xmit.contentLength = 0;
                if (len) xmitContent(s, len);
            }
            else {
                xmit.contentLength -= len;
            }
        }
        else {
            bool eol;
            unsigned p = readLine(s, len, eol);
            if (xmit.line) {
                if (xmit.lineLen < MAX_HTTP_LINE) {
                    memcpy(&xmit.line[xmit.lineLen], s, MIN(MAX_HTTP_LINE - xmit.lineLen, p));
                }
                xmit.lineLen += len;
            }
            if (eol) {
                if (xmit.endChunk) {
                    xmit.eod = true;
                }
                else {
                    if (xmit.cr) {
                        xmit.cr = false;
                    }
                    else {
                        xmit.cr = true;
                        xmit.contentLength = strtoul(xmit.line ? xmit.line : s, 0, 16);
                        if (xmit.contentLength == 0) {
                            xmit.endChunk = true;
                        }
                    }
                    if (xmit.line) free(xmit.line);
                    xmit.line = 0;
                    s += p;
                    len -= p;
                    if (len) xmitContent(s, len);
                }
            }
            else if (!xmit.line) {
                xmit.line = (char *)malloc(MAX_HTTP_LINE);
                xmit.lineLen = p;
                memcpy(xmit.line, s, MIN(MAX_HTTP_LINE, p));
            }
        }
    }
    else if (!websocket) {
        if (xmit.contentLength) {
            unsigned l = MIN(len, xmit.contentLength);
            xmit.contentLength -= l;
            if (!xmit.contentLength) xmit.eod = true;
        }
    }
    else {
        DBG_WEBSOCKET(("Websocket(%x) XmitContent(%u)", this, len));
    }
}

void AppProxyPassthrough::headerSend(unsigned len, bool send)
{
    DBG_PASSTHROUGH(("AppProxyPassthrough(%x)::headerSend(%u,%x)", this, len, send));
    do {
        unsigned l = MIN(len, xmit.header.front()->len - xmit.fragmentOffset);
        if (send && l) {
            socket->Send(&xmit.header.front()->buffer[xmit.fragmentOffset], l);
            xmit.sending++;
        }
        xmit.fragmentOffset += l;
        if (xmit.fragmentOffset == xmit.header.front()->len) {
            delete xmit.header.pop_front();
            xmit.fragmentOffset = 0;
        }
        len -= l;
    } while (len && xmit.header.front());
}

void AppProxyPassthrough::tunnelHeader(char * s, unsigned l)
{
    DBG_PASSTHROUGH(("AppProxyPassthrough(%x)::tunnelHeader(%.*s)", this, l, s));
    if (!str::n_casecmp(s, "HTTP/", 5)) {
        const char *r = s + 5;
        while ((*r != ' ') && (*r != '\0')) {
            r++;
        }
        while (*r == ' ') {
            r++;
        }
        tunnel.responseCode = atoi(r);
    }
}

void AppProxyPassthrough::tunnelEoh()
{
    DBG_PASSTHROUGH(("AppProxyPassthrough(%x)::tunnelEoh()", this));
    if ((tunnel.responseCode >= 200) && (tunnel.responseCode < 300)) {
        connect_tunnel = false;
        retryAuthenticated = false;
        if (tls || tlsTranslate) {
            socket = tlsSocketProvider->UpgradeToTLSSocket(socket, appProxy->service->iomux, this, appProxy, NULL);
        }
        else {
            SocketConnectComplete(socket);
        }
    }
    else if ((tunnel.responseCode == 407) && !retryAuthenticated && proxy->usr && proxy->pwd) {
        int len;
        char buf[1024];
        char buf2[1024];
        retryAuthenticated = true;
        len = _sprintf(buf, "%s:%s", proxy->usr, proxy->pwd);
        encode_base64((byte *)buf2, buf, len);
        if (hostName) {
            char * h = (char *)(alloca(strlen(hostName) + 1));
            UrlDecodeSimple(hostName, h);
            len = snprintf(buf, sizeof(buf),"CONNECT %s%s HTTP/1.1\r\nHost: %s%s\r\nProxy-Authorization: Basic %s\r\n\r\n",
                h, hostPort ? hostPort : ((tls || tlsTranslate) ? ":443" : ":80"),
                h, hostPort ? hostPort : ((tls || tlsTranslate) ? ":443" : ":80"), buf2);
        }
        else {
            len = snprintf(buf, sizeof(buf), "CONNECT %s HTTP/1.1\r\nHost: %s\r\nProxy-Authorization: Basic %s\r\n\r\n", hostAddr, hostAddr, buf2);
        }
        socket->Send(buf, len);
        xmit.sending++;
        socket->Recv(malloc(2000), 2000, true);
    }
    else {
        socket->Shutdown();
    }
}

void AppProxyPassthrough::recvHeader(char * s, unsigned l)
{
    unsigned o, p, i;
    DBG_PASSTHROUGH(("AppProxyPassthrough(%x)::recvHeader(%.*s)", this, l, s));
    if (!str::n_casecmp(s, "content-length:", 15)) {
        recv.contentLength = strtoul(&s[15], 0, 0);
    }
    else if ((l >= 18 + 7) && !str::n_casecmp(s, "transfer-encoding:", 18)) {
        i = 18;
        while ((i <= l - 7) && str::n_casecmp(&s[i], "chunked", 7)) {
            i++;
        }
        if (i <= l - 7) {
            recv.chunked = true;
            recv.contentLength = 0;
        }
    }
    else if (!str::n_casecmp(s, "content-encoding:", 17)) {
        if (recv.encoding) free(recv.encoding);
        p = (s[17] == ' ') ? 18 : 17;
        recv.encoding = (char *)(malloc(l - p + 1));
        memcpy(recv.encoding, &s[p], l - p);
        recv.encoding[l - p] = '\0';
    }
    else if (!str::n_casecmp(s, "content-type:", 13)) {
        if (recv.type) free(recv.type);
        p = (s[13] == ' ') ? 14 : 13;
        recv.type = (char *)(malloc(l - p + 1));
        memcpy(recv.type, &s[p], l - p);
        recv.type[l - p] = '\0';
    }
    else if ((l >= 11 + 5) && !str::n_casecmp(s, "connection:", 11)) {
        i = 11;
        while ((i <= l - 5) && str::n_casecmp(&s[i], "close", 5)) {
            i++;
        }
        if (i <= l - 5) {
            recv.connectionClose = true;
        }
    }
    else if (!str::n_casecmp(s, "location:", 9)) {
        o = (s[9] == ' ') ? 10 : 9;
        p = 0;
        bool tlsLocation = tls;
        unsigned hostInLen = strlen(hostIn);
        unsigned webserverPathLen = strlen(appProxy->webserverPath);
        unsigned registeredPathLen = strlen((appProxy->webserverPath[1] == '\0') ? APPPROXY_REGISTERED_PATH "/" : "/" APPPROXY_REGISTERED_PATH "/");
        unsigned n = hostInLen + webserverPathLen + registeredPathLen;
        char * r = (char *)(malloc(n + 1));
        memcpy(r, hostIn, hostInLen);
        memcpy(&(r[hostInLen]), appProxy->webserverPath, webserverPathLen);
        memcpy(&(r[hostInLen + webserverPathLen]), (appProxy->webserverPath[1] == '\0') ? APPPROXY_REGISTERED_PATH "/" : "/" APPPROXY_REGISTERED_PATH "/", registeredPathLen);
        r[n] = '\0';
        if (!str::n_casecmp(&s[o], "http://", 7)) {
            p = o + 7;
            tlsLocation = false;
        }
        else if (!str::n_casecmp(&s[o], "https://", 8)) {
            p = o + 8;
            tlsLocation = true;
        }
        else if (!str::n_casecmp(&s[o], "ws://", 5)) {
            p = o + 5;
            tlsLocation = false;
        }
        else if (!str::n_casecmp(&s[o], "wss://", 6)) {
            p = o + 6;
            tlsLocation = true;
        }
        if (recv.location) free(recv.location);
        recv.location = (char *)(malloc(l + n + 2));
        i = 0;
        if (p) {
            if (tlsTranslate && tlsLocation && !tls) {
                memcpy(&recv.location[i], &s[o], p - 4 - o);
                i += p - 4 - o;
                passthrough->Send(s, p - 4);
                memcpy(&recv.location[i], &s[p-3], 3);
                i += 3;
                passthrough->Send(&s[p-3], 3);
                recv.sending++;
            }
            else {
                memcpy(&recv.location[i], &s[o], p - o);
                i += p - o;
                passthrough->Send(s, p);
            }
            if ((l < p + n) || str::n_casecmp(&(s[p]), r, n)) {
                memcpy(&recv.location[i], r, n);
                i += n;
                passthrough->Send(r, n);
                recv.sending++;
            }
            free(r);
            memcpy(&recv.location[i], &s[p], l - p);
            i += l - p;
            passthrough->Send(&s[p], l - p);
            passthrough->Send("\r\n", 2);
            recv.sending += 3;
            recv.location[i] = '\0';
            return;
        }
        memcpy(&recv.location[i], &s[o], l - o);
        i += l - o;
        recv.location[i] = '\0';
        free(r);
    }
    else if (!str::n_casecmp(s, "cache-control:", 14)) {
        if (recv.cacheControl) free(recv.cacheControl);
        p = (s[14] == ' ') ? 15 : 14;
        recv.cacheControl = (char *)(malloc(l - p + 1));
        memcpy(recv.cacheControl, &s[p], l - p);
        recv.cacheControl[l - p] = '\0';
        return;
    }
    else if ((l >= 8 + 9) && !str::n_casecmp(s, "upgrade:", 8)) {
        i = 8;
        while ((i <= l - 9) && str::n_casecmp(&s[i], "websocket", 9)) {
            i++;
        }
        if ((i <= l - 9) && (recv.responseCode == 101)) {
            websocket = true;
            DBG_WEBSOCKET(("Websocket(%x) Upgrade", this));
        }
    }
    else if (!str::n_casecmp(s, "HTTP/", 5)) {
        const char *r = s + 5;
        while ((*r != ' ') && (*r != '\0')) {
            r++;
        }
        while (*r == ' ') {
            r++;
        }
        recv.responseCode = atoi(r);
    }
    passthrough->Send(s, l);
    passthrough->Send("\r\n", 2);
    recv.sending += 2;
    recv.hasSent = true;
}

void AppProxyPassthrough::recvEoh()
{
    DBG_PASSTHROUGH(("AppProxyPassthrough(%x)::recvEoh()", this));
    UpdateConnectionMonitor();
    if ((recv.responseCode == 307)
     || (!recv.encoding && recv.type && (str::n_casecmp(recv.type, "text/html", 9) == 0))) {
        passthrough->Send("Cache-Control: no-cache, no-store\r\n", sizeof("Cache-Control: no-cache, no-store\r\n") - 1);
        recv.sending++;
    }
    else if (recv.cacheControl) {
        passthrough->Send("Cache-Control: ", 15);
        passthrough->Send(recv.cacheControl, strlen(recv.cacheControl));
        passthrough->Send("\r\n", 2);
        recv.sending += 3;
    }
    if (websocket) {
        ((IInstanceLog *)appProxy)->Log(LOG_APPPROXY, "%udone(%s%s://%s%s)", recv.responseCode, (websocket ? "ws" : "http"), (tls ? "s" : ""), xmit.fileName, ((loadingCacheable && xmit.fileParamsShort) ? xmit.fileParamsShort : ""));
    }
    if (websocket && !xmit.sending) {
        DBG_WEBSOCKET(("Websocket(%x) recv Eoh", this));
        passthrough->Recv();
    }
    passthrough->Send("\r\n", 2);
    recv.sending++;
}

void AppProxyPassthrough::recvContent(char * s, unsigned len)
{
    DBG_PASSTHROUGH(("AppProxyPassthrough(%x)::recvContent(%u)", this, len));
    if (recv.chunked) {
        if (recv.contentLength) {
            recvPayload(s, MIN(len, recv.contentLength));
            if (len >= recv.contentLength) {
                s += recv.contentLength;
                len -= recv.contentLength;
                recv.contentLength = 0;
                if (len) recvContent(s, len);
            }
            else {
                recv.contentLength -= len;
            }
        }
        else {
            bool eol;
            unsigned p = readLine(s, len, eol);
            if (recv.line) {
                if (recv.lineLen < MAX_HTTP_LINE) {
                    memcpy(&recv.line[recv.lineLen], s, MIN(MAX_HTTP_LINE - recv.lineLen, p));
                }
                recv.lineLen += len;
            }
            if (eol) {
                if (recv.endChunk) {
                    if ((recv.line && ((recv.lineLen > 2) || ((recv.lineLen == 2) && (recv.line[0] != '\r'))))
                     || (!recv.line && ((p > 2) || ((p == 2) && (s[0] != '\r'))))) {
                        ((IInstanceLog *)appProxy)->Log(LOG_APPPROXY, "Appproxy malformed last chunk");
                        passthrough->Cancel(WSP_CANCEL_STREAM);
                        closing = true;
                        tryDelete();
                    }
                    else {
                        recv.eod = true;
                        recvPayload(0, 0);
                    }
                }
                else {
                    if (recv.cr) {
                        recv.cr = false;
                    }
                    else {
                        recv.cr = true;
                        char * end = recv.line ? recv.line : s;
                        recv.contentLength = strtoul(recv.line ? recv.line : s, &end, 16);
                        if (end == (recv.line ? recv.line : s)) {
                            ((IInstanceLog *)appProxy)->Log(LOG_APPPROXY, "Appproxy malformed chunk size");
                            passthrough->Cancel(WSP_CANCEL_STREAM);
                            closing = true;
                            tryDelete();
                        }
                        else if (recv.contentLength == 0) {
                            recv.endChunk = true;
                        }
                    }
                    if (recv.line) free(recv.line);
                    recv.line = 0;
                    s += p;
                    len -= p;
                    if (len) recvContent(s, len);
                }
            }
            else if (!recv.line) {
                recv.line = (char *)malloc(MAX_HTTP_LINE);
                recv.lineLen = p;
                memcpy(recv.line, s, MIN(MAX_HTTP_LINE, p));
            }
        }
    }
    else if (!websocket) {
        if (recv.contentLength) {
            unsigned l = MIN(len, recv.contentLength);
            recv.contentLength -= l;
            if (!recv.contentLength) recv.eod = true;
            recvPayload(s, l);
        }
    }
    else {
        DBG_WEBSOCKET(("Websocket(%x) RecvContent(%u)", this, len));
    }
}

void AppProxyPassthrough::recvPayload(char * s, unsigned len)
{
    DBG_PASSTHROUGH(("AppProxyPassthrough(%x)::recvPayload(%u)", this, len));
    if (!xmit.httpGetMethod) {
        return;
    }
    if (!recv.responseProcessed) {
        class AppProxyManifest * m;
        if (recv.responseCode == 200) {
            if (xmit.fileTypeText) {
                if (recv.payload) {
                    if (len) {
                        char * l = (char *)malloc(recv.payloadLen + len);
                        memcpy(l, recv.payload, recv.payloadLen);
                        memcpy(&l[recv.payloadLen], s, len);
                        free(recv.payload);
                        recv.payload = l;
                        recv.payloadLen += len;
                    }
                    s = recv.payload;
                    len = recv.payloadLen;
                }
                unsigned p = recv.payloadEol;
                bool eol;
                if (len) {
                    eol = true;
                    while (eol && !recv.payloadManifest) {
                        unsigned l = readLine(&s[p], len - p, eol);
                        p += l;
                        if (eol) {
                            recv.payloadEol = p;
                        }
                        char buf[MAX_HTTP_LINE];
                        memcpy(buf, &s[p-l], (l < MAX_HTTP_LINE - 1) ? l : MAX_HTTP_LINE - 1);
                        buf[(l < MAX_HTTP_LINE - 1) ? l : MAX_HTTP_LINE - 1] = '\0';
                        recv.payloadManifest = (strstr(buf, APPPROXY_MANIFEST_ID) != 0);
                    }
                }
                else {
                    eol = (recv.payloadEol != 0);
                }
                if (p < MAX_MANIFEST_LENGTH) {
                    if (eol) {
                        m = appProxy->findManifest(xmit.fileName);
                        if (!m || !m->manifest) {
                            if (len < MAX_MANIFEST_LENGTH && !recv.eod) {
                                if (!recv.payload) {
                                    recv.payload = (char *)malloc(len);
                                    memcpy(recv.payload, s, len);
                                    recv.payloadLen = len;
                                }
                                return;
                            }
                            if (m) {
                                m->Close();
                                m = NULL;
                            }
                            m = new AppProxyManifest(appProxy, xmit.fileName);
                            appProxy->manifests = appProxy->manifests->btree_put(m);
                            appProxy->manifestList.push_back(m);
                            m->addFiles(xmit.fileName, recv.encoding, recv.type, s, len, false, tls || tlsTranslate, host, hostPath);
                        }
                        else {
                            if (m->loadComplete()) {
                                m->addFiles(xmit.fileName, recv.encoding, recv.type, s, len, false, tls || tlsTranslate, host, hostPath);
                                appProxy->updateManifestFile();
                            }
                            else {
                                m->addFiles(xmit.fileName, recv.encoding, recv.type, s, len, false, tls || tlsTranslate, host, hostPath);
                            }
                        }
                    }
                    else if (!recv.eod) {
                        if (!recv.payload) {
                            recv.payload = (char *)malloc(len);
                            memcpy(recv.payload, s, len);
                            recv.payloadLen = len;
                        }
                        return;
                    }
                    else {
                        m = appProxy->findManifest(xmit.fileName);
                    }
                }
                else {
                    m = appProxy->findManifest(xmit.fileName);
                }
            }
            else {
                m = appProxy->findManifest(xmit.fileName);
            }
            class AppProxyCacheable * c;
            class AppProxyManifestFile * f = appProxy->findManifestFile(m, xmit.fileName);
            if (!f && xmit.fileParams && !recv.encoding && recv.type) {
                c = appProxy->findCacheable(xmit.fileName);
                if (c) {
                    f = appProxy->findAddCacheableFile(c, xmit.fileName, xmit.fileParams, recv.type);
                    if (f) {
                        m = NULL;
                    }
                }
            }
            if (f && !f->loading && !f->loaded) {
                loadingManifest = m;
                loadingFile = f;
                if (m) {
                    m->fileLoadStarted(f);
                    write = new TaskFileWrite(appProxy, appProxy->workingPath, urlPathToFilePath(xmit.fileName), false);
                }
                else {
                    char b[2048];
                    f->loading = true;
                    _sprintf(b, "%s/%s.%s", xmit.fileName, f->name, mimeToFileExtension(f->type));
                    write = new TaskFileWrite(appProxy, appProxy->workingPath, urlPathToFilePath(b), false);
                }
                write->Start(this);
                writing = true;
            }
        }
        else if (recv.responseCode == 307) {
            if (recv.location) {
                unsigned p, q, e, i, pathLen, redirectLen;
                unsigned l = strlen(recv.location);
                unsigned hostInLen = strlen(hostIn);
                unsigned filePathLen = strlen(xmit.fileName);

                while((filePathLen != 0) && (xmit.fileName[filePathLen-1] != '/')) {
                    filePathLen--;
                }
                pathLen = 0;
                redirectLen = 0;
                q = 0;
                if (str::n_casecmp(recv.location, "http://", 7) == 0) {
                    q = 7;
                }
                else if (str::n_casecmp(recv.location, "https://", 8) == 0) {
                    q = 8;
                }
                if (q != 0) {
                    if ((str::n_casecmp(&recv.location[q], hostIn, hostInLen) == 0) && (recv.location[q+hostInLen] == '/')) {
                        q += hostInLen;
                        for (i = q; recv.location[i]; i++) {
                            if (recv.location[i] == '/') {
                                redirectLen = pathLen;
                                pathLen = i + 1;
                            }
                        }
                        q++;
                        if ((pathLen != 0)
                         && ((str::n_casecmp(&recv.location[q], xmit.fileName, redirectLen - q) != 0)
                          || (str::n_casecmp(&recv.location[pathLen], &xmit.fileName[filePathLen], l - pathLen) != 0))) {
                            pathLen = 0;
                        }
                    }
                }
                else {
                    e = filePathLen;
                    if ((recv.location[q] == '.') && (recv.location[q + 1] == '.') && (recv.location[q + 2] == '/')) {
                        do {
                            q += 3;
                            while((filePathLen != 0) && (xmit.fileName[filePathLen-1] != '/')) {
                                filePathLen--;
                            }
                        } while ((recv.location[q] == '.') && (recv.location[q + 1] == '.') && (recv.location[q + 2] == '/'));
                    }
                    else if ((recv.location[q] == '.') && (recv.location[q + 1] == '/')) {
                        q += 2;
                    }
                    pathLen = q;
                    redirectLen = pathLen;
                    for (i = q; recv.location[i]; i++) {
                        if (recv.location[i] == '/') {
                            redirectLen = pathLen;
                            pathLen = i + 1;
                        }
                    }
                    if ((pathLen != 0)
                     && ((str::n_casecmp(&recv.location[q], &xmit.fileName[filePathLen], redirectLen - q) != 0)
                      || (str::n_casecmp(&recv.location[pathLen], &xmit.fileName[e], l - pathLen) != 0))) {
                        pathLen = 0;
                    }
                }
                for (p = strlen(xmit.fileName); p && xmit.fileName[p-1] != '.'; p--);
                if (p && (strcasecmp(&xmit.fileName[p], "png") == 0) && !recv.encoding && (!recv.type || (strcasecmp(recv.type, "image/png") == 0))
                 && (pathLen != 0) && isdigit(recv.location[redirectLen])) {
                    char * b = (char *)(alloca(filePathLen + l - redirectLen + 1));
                    memcpy(b, xmit.fileName, filePathLen);
                    memcpy(&b[filePathLen], &recv.location[redirectLen], l - redirectLen + 1);
                    m = appProxy->findManifest(b);
                    class AppProxyManifestFile * f = appProxy->findManifestFile(m, b);
                    if (!f) {
                        if (m) {
                            if (!m->manifest) {
                                m->addFile(&xmit.fileName[filePathLen], NULL, NULL);
                            }
                        }
                        else {
                            class AppProxyManifest * m = new AppProxyManifest(appProxy, b);
                            appProxy->manifests = appProxy->manifests->btree_put(m);
                            appProxy->manifestList.push_back(m);
                            m->addFiles(b, NULL, NULL, NULL, 0, false, tls || tlsTranslate, host, hostPath);
                        }
                    }
                }
            }
        }
        recv.responseProcessed = true;
    }
    if (recv.eod) {
        ((IInstanceLog *)appProxy)->Log(LOG_APPPROXY, "%udone(%s%s://%s%s)", recv.responseCode, (websocket ? "ws" : "http"), (tls ? "s" : ""), xmit.fileName, ((loadingCacheable && xmit.fileParamsShort) ? xmit.fileParamsShort : ""));
    }
    if (write) {
        if (writing) {
            if (len) {
                queue.push_back(new fragment((byte *)memcpy(malloc(len), s, len), len));
            }
            if (recv.eod) {
                queue.push_back(new fragment(0, 0));
            }
        }
        else {
            write->write((byte *)s, len, recv.eod);
            writing = true;
            if (len && recv.eod) {
                queue.push_back(new fragment(0, 0));
            }
        }
    }
    if (recv.payload) free(recv.payload);
    recv.payload = 0;
    recv.payloadEol = 0;
    recv.payloadManifest = false;
}

bool AppProxyPassthrough::tryDelete()
{
    DBG_PASSTHROUGH(("AppProxyPassthrough(%x)::tryDelete(passthru=%x, socket=%x, write=%x, read=%x, dnsPending=%u)", this, passthrough, socket, write, read, dnsPending));
    if (!passthrough && !socket && !write && !read && !dnsPending) {
        UpdateConnectionMonitor();
        appProxy->AppProxyPassthroughClosed(this);
        return true;
    }
    return false;
}

void AppProxyPassthrough::Close()
{
    if (socket) socket->Shutdown();
    if (passthrough) {
        if (!recv.hasSent && !websocket) {
            passthrough->Send(AppProxy503ServiceUnavailable, sizeof(AppProxy503ServiceUnavailable) - 1);
            recv.sending++;
            recv.hasSent = true;
            recv.eod = true;
        }
        else {
            if (recv.eod) {
                if (!closing) passthrough->Close();
            }
            else {
                passthrough->Cancel(WSP_CANCEL_STREAM);
            }
            closing = true;
        }
    }
    tryDelete();
}

void AppProxyPassthrough::ShutdownTimeout()
{
    debug->printf("AppProxyPassthrough(%x)::ShutdownTimeout %d %d %d %d %d", this,
        (passthrough != NULL), (socket != NULL), (write != NULL), (read != NULL), dnsPending);
}

void AppProxyPassthrough::UpdateConnectionMonitor()
{
    if (appProxy->connectionMonitorOn && xmit.content) {
        char f[4096];
        _sprintf(f, "%s%s://%s", (websocket ? "ws" : "http"), (tls ? "s" : ""), xmit.fileNameShort);
        appProxy->service->user->AppProxyConnectionMonitor(f, recv.content && passthrough);
    }
}

/*---------------------------------------------------------------------------*/
/* AppProxySocketRetainer                                                            */
/*---------------------------------------------------------------------------*/

AppProxySocketRetainer::AppProxySocketRetainer(class IIoMux *iomux, class AppProxy * appProxy, bool tls, const char * host, const char * addr, ISocket * socket) :
    expiryTimer(iomux, this)
{
    DBG_PASSTHROUGH(("AppProxySocketRetainer(%p)::AppProxySocketRetainer socket=%p", this, socket));

    this->appProxy = appProxy;
    this->tls = tls;
    this->host = _strdup(host);
    this->addr = _strdup(addr);
    this->socket = socket;
    socket->SetUser(this);
    expiryTimer.Start(10000);
}

AppProxySocketRetainer::~AppProxySocketRetainer()
{
    DBG_PASSTHROUGH(("AppProxySocketRetainer(%p)::~AppProxySocketRetainer socket=%p", this, socket));
    if(host) free(host);
    if(addr) free(addr);
}

void AppProxySocketRetainer::Shutdown()
{
    DBG_PASSTHROUGH(("AppProxySocketRetainer(%p)::Shutdown socket=%p", this, socket));
    expiryTimer.Cancel();
    socket->Shutdown();
}

void AppProxySocketRetainer::SocketShutdown(ISocket * const socket, shutdownreason_t reason)
{
    DBG_PASSTHROUGH(("AppProxySocketRetainer(%p)::SocketShutdown socket=%p", this, socket));
    delete socket;
    remove();
    appProxy->SocketRetainerRemoved(this);
    delete this;
}

void AppProxySocketRetainer::SocketRecvCanceled(ISocket * const socket, void * buf)
{
    DBG_PASSTHROUGH(("AppProxySocketRetainer(%p)::SocketRecvCanceled() buf=%p", this, buf));
    free(buf);
}

void AppProxySocketRetainer::TimerOnTimeout(AppProxySocketRetainerExpiryTimer * timer)
{
    Shutdown();
}

/*---------------------------------------------------------------------------*/
/* AppProxyManifest                                                          */
/*---------------------------------------------------------------------------*/

AppProxyManifest::AppProxyManifest(class AppProxy * appProxy, const char * fileName) :
    start(appProxy->iomux, this),
    redirect(this)
{
    unsigned i;

    this->appProxy = appProxy;
    host = NULL;
    hostPath = NULL;
    tls = false;
    manifest = false;
    http = 0;
    write = 0;
    loadingFile = 0;
    writing = false;
    complete = false;
    removeFiles = false;
    closing = false;
    closed = false;
    filesTree = 0;

    pathLen = 0;
    for (i = 0; fileName[i]; i++) {
        if (fileName[i] == '/') {
            redirect.redirectLen = pathLen;
            pathLen = i + 1;
        }
    }
    path = (char *)malloc(pathLen + 1);
    memcpy(path, fileName, pathLen);
    path[pathLen] = '\0';
    if (redirect.redirectLen && isdigit(path[redirect.redirectLen])) {
        redirect.redirectPath = (char *)malloc(redirect.redirectLen + 1);
        memcpy(redirect.redirectPath, fileName, redirect.redirectLen);
        redirect.redirectPath[redirect.redirectLen] = '\0';
        class btree * b = appProxy->redirects->btree_find(redirect.redirectPath);
        if (b) {
            DBG_MANIFEST(("AppProxyManifest(%s)::AppProxyManifest new redirect", fileName));
            ((IInstanceLog *)appProxy)->Log(LOG_APPPROXY, "Manifest(%s) new redirect", fileName);
            class AppProxyRedirect * r = (class AppProxyRedirect *)b;
            r->manifest->RemoveFiles();
        }
        appProxy->redirects = appProxy->redirects->btree_put(&redirect);
    }
    else {
        redirect.redirectLen = 0;
    }

    DBG_MANIFEST(("AppProxyManifest(%s)::AppProxyManifest(tls=%x, host=%s, hostPath=%s)", fileName, tls, host, hostPath));
    ((IInstanceLog *)appProxy)->Log(LOG_APPPROXY, "Manifest(%s) host=%s", fileName, host);
}

void AppProxyManifest::addFiles(const char * fileName, const char * encoding, const char * type, const char * data, unsigned len, bool loaded, bool tls, const char * host, const char * hostPath)
{
    class AppProxyManifestFile * f;
    char b[2048];
    const char * n;
    unsigned i;

    if (this->host) free(this->host);
    this->host = _strdup(host);
    if (this->hostPath) free(this->hostPath);
    this->hostPath = _strdup(hostPath);
    this->tls = tls;

    this->fileNames.push_back(new AppProxyManifestFile(fileName, NULL, NULL));
    manifest = (data != NULL);

    f = addFile(&fileName[pathLen], encoding, type);
    if (f) f->loaded = loaded;

    if (data) {
        if (!loaded && !loadingFile) {
            start.Start(1000);
        }
        const char * p;
        unsigned l;
        char buffer[MAX_MANIFEST_LENGTH];
        memcpy(buffer, data, (len < MAX_MANIFEST_LENGTH - 1) ? len : MAX_MANIFEST_LENGTH - 1);
        buffer[(len < MAX_MANIFEST_LENGTH - 1) ? len : MAX_MANIFEST_LENGTH - 1] = '\0';
        p = strstr(buffer, APPPROXY_MANIFEST_ID);
        if (p) {
            p += strlen(APPPROXY_MANIFEST_ID);
            bool eod;
            l = readJson(&data[p - buffer], len - (p - buffer), eod);
            if (eod) {
                memcpy(buffer, &data[p - buffer], l);
                buffer[l] = '\0';
                class json_io json(buffer);
                if (json.decode()) {
                    word last = 0;
                    word base = json.get_object(JSON_ID_ROOT, last);
                    word handle = json.get_array(base, "files");
                    if (handle != JSON_ID_ROOT) {
                        for (word last = 0; (n = json.get_string(handle, last)); ) {
                            const char * e = NULL;
                            const char * t = NULL;
                            const char * c = strchr(n, '|');
                            if (c) {
                                char * q = &(b[c-n]);
                                strcpy(b, n);
                                n = b;
                                *(q++) = '\0';
                                if ((*q != '\0') && (*q != '|')) {
                                    e = q;
                                    q++;
                                }
                                q = strchr(q, '|');
                                if (q) {
                                    *(q++) = '\0';
                                    if ((*q != '\0') && (*q != '|')) {
                                        t = q;
                                        q++;
                                    }
                                    q = strchr(q, '|');
                                    if (q) {
                                        *(q++) = '\0';
                                    }
                                }
                            }
                            f = addFile(n, e, t);
                            if (f) {
#ifdef APPPROXY_PATCH
                                i = 0;
                                while ((i < sizeof(appProxyPatchTable) / sizeof(appProxyPatchTable[0])) && (strcmp(appProxyPatchTable[i].filename, n) != 0)) {
                                    i++;
                                }
                                if (i < sizeof(appProxyPatchTable) / sizeof(appProxyPatchTable[0])) {
                                    char buf[2048];
                                    _sprintf(buf, "%s%s", path, f->name);
                                    _sprintf(b, "%s%s", appProxy->workingPath, urlPathToFilePath(buf));
                                    IFileWrite * file = FileSystem::OpenWriteFile(b, false, NULL);
                                    if (!file) {
                                        unsigned lw = (unsigned)strlen(appProxy->workingPath);
                                        for (unsigned p = lw; b[p]; p++) {
                                            if (b[p] == '/') {
                                                b[p] = 0;
                                                FileSystem::CreateDirectory(b, 0700, 0);
                                                b[p] = '/';
                                            }
                                        }
                                        file = FileSystem::OpenWriteFile(b, false, NULL);
                                    }
                                    if (file && file->Write(appProxyPatchTable[i].data, appProxyPatchTable[i].len)) {
                                        debug->printf("AppProxyManifest(%s)::Patch %s", fileName, b);
                                    }
                                    else {
                                        debug->printf("AppProxyManifest(%s)::Patch failed %s", fileName, b);
                                    }
                                    file->Close();
                                    delete file;
                                    FileSystem::ChangeMode(b, 0777, NULL);
                                    f->loaded = true;
                                }
                                else
#endif
                                {
                                    f->loaded = loaded;
                                }
                            }
                        }
                    }
                }
            }
        }
    }
    else {
        i = _sprintf(b, "%s%s", appProxy->workingPath, urlPathToFilePath(path));
        b[i-1] = '\0';
        IDirectory * d = FileSystem::OpenDirectory(b, appProxy);
        if (d) {
            IDirectoryEntry * e;
            while ((e = d->ReadNext()) != NULL) {
                if (e->IsFile()) {
                    n = e->GetName();
                    for (i = strlen(n); i && n[i-1] != '.'; i--);
                    if (i && (strcasecmp(&n[i], "png") == 0)) {
                        f = addFile(filePathToUrlPath(n), NULL, NULL);
                        if (f) f->loaded = true;
                    }
                }
                delete e;
            }
            d->Close();
            delete d;
        }
    }
}

AppProxyRedirect::AppProxyRedirect(class AppProxyManifest * manifest)
{
    this->manifest = manifest;
    redirectPath = 0;
    redirectLen = 0;
}

AppProxyRedirect::~AppProxyRedirect()
{
    if (redirectPath) free(redirectPath);
}

AppProxyManifest::~AppProxyManifest()
{
    ASSERT(write == 0, "AppProxyManifest::~AppProxyManifest write task still set!");
    while (!fileNames.empty()) {
        delete fileNames.front();
    }
    if (host) free(host);
    if (hostPath) free(hostPath);
    while (!filesList.empty()) {
        delete filesList.front();
    }
    if (path) free(path);
}

void AppProxyManifest::TimerOnTimeout(AppProxyManifestStartTimer * timer)
{
    DBG_MANIFEST(("AppProxyManifest(%s)::Timeout() complete=%x", fileNames.front()->name, loadComplete()));
    fileLoad();
}

void AppProxyManifest::HTTPClientConnectComplete(IHTTPClient * const httpClient)
{
    DBG_MANIFEST(("AppProxyManifest(%s)::HTTPClientConnectComplete()", hostPath));

    char f[2048];
    _sprintf(f, "%s%s", hostPath, loadingFile->name);
    http->SetRequestType(HTTP_GET, f);
    http->Send();
}

void AppProxyManifest::HTTPClientShutdown(IHTTPClient * const httpClient, http_shutdown_reason_t reason)
{
    DBG_MANIFEST(("AppProxyManifest(%s)::HTTPClientShutdown()", fileNames.front()->name));
    delete http;
    http = 0;
    if (write) {
        if (!writing) {
            write->write(NULL, 0, true);
            writing = true;
        }
    }
    else {
        fileLoad();
    }
}

void AppProxyManifest::HTTPClientSendResult(IHTTPClient * const httpClient)
{
    DBG_MANIFEST(("AppProxyManifest(%s)::HTTPClientSendResult()", fileNames.front()->name));
}

void AppProxyManifest::HTTPClientRecvResult(IHTTPClient * const httpClient, byte * buffer, size_t len, bool transferComplete)
{
    DBG_MANIFEST(("AppProxyManifest(%s)::HTTPClientRecvResult()", fileNames.front()->name));
    if (write) {
        write->writeBuffer(buffer, len, transferComplete);
        writing = true;
        complete = transferComplete;
    }
    else {
        if (transferComplete) {
            free(buffer);
            fileLoad();
        }
        else {
            http->Recv(buffer, 2000);
        }
    }
}

void AppProxyManifest::HTTPClientRecvCanceled(IHTTPClient * const httpClient, byte * buffer)
{
    free(buffer);
}

void AppProxyManifest::HTTPClientRequestComplete(IHTTPClient * const httpClient)
{
    DBG_MANIFEST(("AppProxyManifest(%s)::HTTPClientRequestComplete()", fileNames.front()->name));
    char f[2048];
    _sprintf(f, "%s%s", path, loadingFile->name);
    if (http->GetHTTPResult() == HTTP_RESULT_OK) {
        write = new TaskFileWrite(appProxy, appProxy->workingPath, urlPathToFilePath(f), false);
        write->Start(this);
        writing = true;
    }
    else {
        http->Recv((byte *)malloc(2000), 2000);
    }
}

void AppProxyManifest::TaskComplete(class ITask * const task)
{
    DBG_MANIFEST(("AppProxyManifest(%s)::TaskComplete()", fileNames.front()->name));
    delete write;
    write = 0;
    writing = false;
    if (!complete && http) {
        http->Shutdown();
    }
    else {
        fileLoad();
    }
}

void AppProxyManifest::TaskFailed(class ITask * const task)
{
    DBG_MANIFEST(("AppProxyManifest(%s)::TaskFailed()", fileNames.front()->name));
    delete write;
    write = 0;
    writing = false;
    complete = false;
    if (http) {
        http->Shutdown();
    }
    else {
        fileLoad();
    }
}

void AppProxyManifest::TaskProgress(class ITask * const task, dword progress)
{
    DBG_MANIFEST(("AppProxyManifest(%s)::TaskProgress()", fileNames.front()->name));
    writing = false;
    if (http && !complete) {
        if (!closing) {
            http->Recv((byte *)malloc(2000), 2000);
        }
    }
    else {
        write->write(NULL, 0, true);
        writing = true;
    }
}

void AppProxyManifest::Close()
{
    class AppProxyManifestFile * f;

    if (!closing) {
        DBG_MANIFEST(("AppProxyManifest(%s)::Close()", fileNames.front()->name));
        closing = true;
        appProxy->manifests = appProxy->manifests->btree_get(this);
        if (redirect.redirectLen) appProxy->redirects = appProxy->redirects->btree_get(&redirect);
    }
    if (http) {
        http->Shutdown();
        return;
    }
    if (write) {
        return;
    }
    for (f = filesList.front(); f; f = f->goNext()) {
        if (f->loading || (f->readingCount != 0)) {
            return;
        }
    }
    if (!closed) {
        closed = true;
        appProxy->ManifestCloseComplete(this);
    }
}

void AppProxyManifest::RemoveFiles()
{
    removeFiles = true;
    Close();
}

void AppProxyManifest::ShutdownTimeout()
{
    debug->printf("AppProxyManifest(%s)::ShutdownTimeout %d %d", fileNames.front()->name, (http != NULL), (write != NULL));
}

class AppProxyManifestFile * AppProxyManifest::addFile(const char * name, const char * encoding, const char * type)
{
    class AppProxyManifestFile * f = (class AppProxyManifestFile *)(filesTree->btree_find(name));
    DBG_MANIFEST(("AppProxyManifest(%s)::addFile(%s%s%s%s%s)", fileNames.front()->name, name, (encoding || type) ? "|" : "", encoding ? encoding : "", type ? "|" : "", type ? type : ""));
    if (!f) {
        f = new AppProxyManifestFile(name, encoding, type);
        filesTree = filesTree->btree_put(f);
        filesList.push_front(f);
        return f;
    }
    return 0;
}

class AppProxyManifestFile * AppProxyManifest::findFile(const char * name)
{
    class AppProxyManifestFile * f = (class AppProxyManifestFile *)(filesTree->btree_find(name));
    DBG_MANIFEST(("AppProxyManifest(%s)::findFile(%s) %x", fileNames.front()->name, name, f));
    return f;
}

void AppProxyManifest::fileLoad()
{
    class AppProxyManifestFile * file;
    unsigned i;

    if (loadingFile) {
        if (complete) {
            fileLoadComplete(loadingFile);
            loadingFile = 0;
        }
        else {
            fileLoadFailed(loadingFile);
            loadingFile = 0;
            if (!closing) {
                start.Start(60000);
            }
            else {
                Close();
            }
            return;
        }
    }
    if (closing) {
        Close();
    }
    else {
        file = filesList.front();
        if (file->loaded) {
            if (http) {
                http->Shutdown();
            }
        }
        else if (!file->loading) {
            loadingFile = file;
            fileLoadStarted(file);
            complete = false;
            if (http) {
                char f[2048];
                _sprintf(f, "%s%s", hostPath, loadingFile->name);
                ((IInstanceLog *)appProxy)->Log(LOG_APPPROXY, "Manifest(%s) loadFile %s", fileNames.front()->name, f);
                http->SetRequestType(HTTP_GET, f);
                http->Send();
            }
            else {
                char uri[2048];
                i = _sprintf(uri, "http%s://", tls ? "s" : "");
                i += UrlDecodeSimple(host, &uri[i]);
                i += _sprintf(&uri[i], "%s%s", hostPath, file->name);
                ((IInstanceLog *)appProxy)->Log(LOG_APPPROXY, "Manifest load(%s)", uri);
                http = IHTTPClient::Create(appProxy->service->iomux, appProxy->service->tcpSocketProvider, appProxy->service->tlsSocketProvider, this, appProxy);
                http->Connect(uri);
            }
        }
    }
}

void AppProxyManifest::fileLoadStarted(class AppProxyManifestFile * file)
{
    DBG_MANIFEST(("AppProxyManifest(%s)::fileLoadStarted(%s)", fileNames.front()->name, file->name));
    file->loading = true;
    file->remove();
    filesList.push_back(file);
}

void AppProxyManifest::fileLoadComplete(class AppProxyManifestFile * file)
{
    file->loading = false;
    file->loaded = true;
    file->remove();
    filesList.push_back(file);
    DBG_MANIFEST(("AppProxyManifest(%s)::fileLoaded(%s) %d", fileNames.front()->name, file->name, loadComplete()));
    if (loadComplete()) {
        ((IInstanceLog *)appProxy)->Log(LOG_APPPROXY, "Manifest(%s) complete", fileNames.front()->name);
        appProxy->updateManifestFile();
    }
    if (closing) {
        Close();
    }
}

void AppProxyManifest::fileLoadFailed(class AppProxyManifestFile * file)
{
    DBG_MANIFEST(("AppProxyManifest(%s)::fileLoadFailed(%s)", fileNames.front()->name, file->name));
    file->loading = false;
    file->remove();
    filesList.push_front(file);
    if (closing) {
        Close();
    }
}

bool AppProxyManifest::loadComplete()
{
    for (class AppProxyManifestFile * file = filesList.front(); file; file = file->goNext()) {
        if (!file->loaded) return false;
    }
    return true;
}

void AppProxyManifest::fileReadStarted(class AppProxyManifestFile * file)
{
    DBG_MANIFEST(("AppProxyManifest(%s)::fileReadStarted(%s)", fileNames.front()->name, file->name));
    (file->readingCount)++;
}

void AppProxyManifest::fileReadComplete(class AppProxyManifestFile * file)
{
    DBG_MANIFEST(("AppProxyManifest(%s)::fileRead(%s) %d", fileNames.front()->name, file->name, file->readingCount));
    (file->readingCount)--;
    if (closing) {
        Close();
    }
}

/*---------------------------------------------------------------------------*/
/* AppProxyManifestFile                                                      */
/*---------------------------------------------------------------------------*/

AppProxyManifestFile::AppProxyManifestFile(const char * name, const char * encoding, const char * contentType)
{
    this->name = _strdup(name);
    this->encoding = _strdup(encoding);
    this->contentType = _strdup(contentType);
    type = this->contentType;
    if (!type) {
        type = fileToMimeType(name);
    }
    DBG_MANIFEST(("AppProxyManifestFile(%s)::AppProxyManifestFile(), %s", name, type));
    lastWriteTime = 0;
    loading = false;
    loaded = false;
    readingCount = 0;
}

AppProxyManifestFile::~AppProxyManifestFile()
{
    if (name) free(name);
    if (encoding) free(encoding);
    if (contentType) free(contentType);
}

/*---------------------------------------------------------------------------*/
/* AppProxyCacheable                                                      */
/*---------------------------------------------------------------------------*/

AppProxyCacheable::AppProxyCacheable(class IIoMux *iomux, const char * path) :
    reuseTimer(iomux, this)
{
    this->path = _strdup(path);
    DBG_MANIFEST(("AppProxyCacheable(%s)::AppProxyCacheable()", path));
    filesTree = NULL;
    loadingPassthrough = NULL;
    loadingCount = 0;
}

AppProxyCacheable::~AppProxyCacheable()
{
    if (path) free(path);
    while (filesTree) {
        class AppProxyManifestFile * f = (class AppProxyManifestFile *)filesTree;
        filesTree = filesTree->btree_get(f);
        delete f;
    }
}

void AppProxyCacheable::TimerOnTimeout(AppProxyCacheableReuseTimer * timer)
{
    loadingPassthrough->LoadCacheable(false);
    loadingPassthrough = NULL;
}

class AppProxyManifestFile * AppProxyCacheable::addFile(const char * name, const char * encoding, const char * type)
{
    class AppProxyManifestFile * f = (class AppProxyManifestFile *)(filesTree->btree_find(name));
    DBG_MANIFEST(("AppProxyCacheable(%s)::addFile(%s%s%s%s%s)", path, name, (encoding || type) ? "|" : "", encoding ? encoding : "", type ? "|" : "", type ? type : ""));
    if (!f) {
        f = new AppProxyManifestFile(name, encoding, type);
        filesTree = filesTree->btree_put(f);
        return f;
    }
    return 0;
}

class AppProxyManifestFile * AppProxyCacheable::findFile(const char * name)
{
    class AppProxyManifestFile * f = (class AppProxyManifestFile *)(filesTree->btree_find(name));
    DBG_MANIFEST(("AppProxyCacheable(%s)::findFile(%s) %x", path, name, f));
    return f;
}

void AppProxyCacheable::load(AppProxyPassthrough * passthrough, bool immediate, bool reuse)
{
    loadingCount++;
    if ((loadingCount != 1) || immediate) {
        if (loadingPassthrough) {
            loadingPassthrough->LoadCacheable(true);
            loadingPassthrough = NULL;
            reuseTimer.Cancel();
        }
        passthrough->LoadCacheable(reuse);
    }
    else {
        loadingPassthrough = passthrough;
        reuseTimer.Start(APPPROXY_REUSE_DETECT_TIME);
    }
}

void AppProxyCacheable::loadComplete(AppProxyPassthrough * passthrough)
{
    loadingCount--;
    if (loadingPassthrough == passthrough) {
        loadingPassthrough = NULL;
        reuseTimer.Cancel();
    }
}

/*---------------------------------------------------------------------------*/
/* TaskReadManifests                                                         */
/*---------------------------------------------------------------------------*/

TaskReadManifests::TaskReadManifests(class AppProxy * appProxy)
{
    this->appProxy = appProxy;
    this->read = NULL;
}

TaskReadManifests::~TaskReadManifests()
{
    ASSERT(read == 0, "AppProxyPassthrough::~AppProxyPassthrough read task still set!");
    while (!manifests.empty()) {
        char * m = manifests.front();
        free(m);
        manifests.pop();
    }
}

void TaskReadManifests::Start(class UTask * user)
{
    this->user = user;
    Next();
}

void TaskReadManifests::Next()
{
    const char * f;
    unsigned p;

    while (!manifests.empty()) {
        f = manifests.front();
        for (p = strlen(f); p && f[p-1] != '.'; p--);
        const char * ext = &f[p];
        if (p && ((strcasecmp(ext, "htm") == 0) || (strcasecmp(ext, "html") == 0) || (strcasecmp(ext, "js") == 0) || (strcasecmp(ext, "css") == 0))) {
            read = new TaskFileRead(appProxy, appProxy->workingPath, urlPathToFilePath(manifests.front()), MAX_MANIFEST_LENGTH, true);
            read->Start(this);
            return;
        }
        class AppProxyManifest * manifest = new AppProxyManifest(appProxy, manifests.front());
        appProxy->manifests = appProxy->manifests->btree_put(manifest);
        appProxy->manifestList.push_back(manifest);
        manifest->addFiles(manifests.front(), NULL, NULL, NULL, 0, true);
        char * m = manifests.front();
        free(m);
        manifests.pop();
    }
    user->TaskComplete(this);
}

void TaskReadManifests::TaskComplete(class ITask * const task)
{
    if (task == read) {
        ((IInstanceLog *)appProxy)->Log(LOG_APPPROXY, "Manifest(%s) present", manifests.front());
        class AppProxyManifest * manifest = appProxy->findManifest(manifests.front());
        if (!manifest) {
            manifest = new AppProxyManifest(appProxy, manifests.front());
            appProxy->manifests = appProxy->manifests->btree_put(manifest);
            appProxy->manifestList.push_back(manifest);
        }
        unsigned l = read->getLength();
        char * b = (char *)alloca(l);
        read->getData((byte *)b, l);
        manifest->addFiles(manifests.front(), NULL, NULL, b, l, true);
        delete read;
        read = 0;
        char * m = manifests.front();
        free(m);
        manifests.pop();
        Next();
    }
}

void TaskReadManifests::TaskFailed(class ITask * const task)
{
    if (task == read) {
        delete read;
        read = 0;
        ((IInstanceLog *)appProxy)->Log(LOG_APPPROXY, "Manifest(%s) missing", manifests.front());
        user->TaskFailed(this);
    }
}

void TaskReadManifests::TaskProgress(class ITask * const task, dword progress)
{

}

void TaskReadManifests::AddManifest(const char * fileName)
{
    manifests.push(_strdup(fileName));
}


/*---------------------------------------------------------------------------*/

static unsigned readLine(const char * buffer, unsigned len, bool & eol)
{
    unsigned p;
    eol = false;
    for (p = 0; p < len && buffer[p] != '\n'; p++) {
    }
    if (p < len) {
        eol = true;
        p++;
    }
    return p;
}

static unsigned readJson(const char * buffer, unsigned len, bool & eod)
{
    unsigned p, level;
    bool quotes, esc;
    level = 0;
    quotes = false;
    esc = false;
    eod = false;
    for (p = 0; (p < len) && ((buffer[p] != '}') || (level > 1) || quotes); p++) {
        if (quotes) {
            if (esc) {
                esc = false;
            }
            else if (buffer[p] == '\\') {
                esc = true;
            }
            else if (buffer[p] == '"') {
                quotes = false;
            }
        }
        else if (buffer[p] == '"') {
            quotes = true;
        }
        else if (buffer[p] == '{') {
            level++;
        }
        else if (buffer[p] == '}') {
            level--;
        }
    }
    if ((p < len) && (buffer[p] == '}') && (level == 1) && !quotes) {
        eod = true;
        p++;
    }
    return p;
}

static unsigned readHost(const char * s, bool & dns, bool & ip6)
{
    dns = true;
    ip6 = false;
    unsigned i = 0;
    unsigned len = strlen(s);
    unsigned end = len;
    while (end != 0 && (s[end-1] >= '0' && s[end-1] <= '9')) end--;
    end = (end != 0 && end != len && s[end-1] == ':') ? end - 1 : len;
    if (s[0] == '[') {
        i++;
        while ((s[i] >= '0' && s[i] <= '9') || (s[i] >= 'a' && s[i] <= 'f') || (s[i] >= 'A' && s[i] <= 'F') || s[i] == '.' || s[i] == ':') {
            if (s[i++] == ':') ip6 = true;
        }
        if (s[i] == ']' && ip6 && i+1 == end) {
            dns = false;
        }
    }
    else {
        unsigned dots = 0;
        while ((s[i] >= '0' && s[i] <= '9') || (i && s[i] == '.' && s[i+1] >= '0' && s[i+1] <= '9')) {
            if (s[i++] == '.') dots++;
        }
        if (dots == 3 && i == end) {
            dns = false;
        }
        else {
            while ((s[i] >= '0' && s[i] <= '9') || (s[i] >= 'a' && s[i] <= 'f') || (s[i] >= 'A' && s[i] <= 'F') || s[i] == '.' || s[i] == ':') {
                if (s[i++] == ':') ip6 = true;
            }
            if (i == len && ip6) {
                dns = false;
                end = len;
            }
        }
    }
    return end;
}


static const char * urlPathToFilePath(const char * urlPath)
{
static char filePath[2048];
    unsigned i;

    i = 0;
    while (urlPath[i] != '\0') {
        filePath[i] = (urlPath[i] == ':') ? '!' : urlPath[i];
        i++;
    }
    filePath[i] = '\0';
    return filePath;
}

static const char * filePathToUrlPath(const char * filePath)
{
static char urlPath[2048];
    unsigned i;

    i = 0;
    while (filePath[i] != '\0') {
        urlPath[i] = (filePath[i] == '!') ? ':' : filePath[i];
        i++;
    }
    urlPath[i] = '\0';
    return urlPath;
}

static const char * fileToMimeType(const char * name)
{
    const char * type;
    unsigned p;
    for (p = strlen(name); p && name[p-1] != '.'; p--);
    if (p) {
        const char * ext = &(name[p]);
        if ((strcasecmp(ext, "htm") == 0) || (strcasecmp(ext, "html") == 0)) type = "text/html";
        else if (strcasecmp(ext, "xsl") == 0) type = "text/xslt+xml";
        else if ((strcasecmp(ext, "js") == 0) || (strcasecmp(ext, "mjs") == 0)) type = "text/javascript";
        else if (strcasecmp(ext, "css") == 0) type = "text/css";
        else if (strcasecmp(ext, "xml") == 0) type = "text/xml";
        else if (strcasecmp(ext, "png") == 0) type = "image/png";
        else if (strcasecmp(ext, "svg") == 0) type = "image/svg+xml";
        else if ((strcasecmp(ext, "ttf") == 0) || (strcasecmp(ext, "tflite") == 0)) type = "application/octet-stream";
        else if ((strcasecmp(ext, "jpg") == 0) || (strcasecmp(ext, "jpeg") == 0) ||
                 (strcasecmp(ext, "jiff") == 0) || (strcasecmp(ext, "jfif") == 0)) type = "image/jpeg";
        else if (strcasecmp(ext, "gif") == 0) type = "image/gif";
        else if (strcasecmp(ext, "bmp") == 0) type = "image/bmp";
        else if (strcasecmp(ext, "ico") == 0) type = "image/x-icon";
        else if ((strcasecmp(ext, "mp1") == 0) ||
                 (strcasecmp(ext, "mp2") == 0) || (strcasecmp(ext, "mp3") == 0)) type = "audio/mpeg";
        else if (strcasecmp(ext, "wav") == 0) type = "audio/wav";
        else if (strcasecmp(ext, "ogg") == 0) type = "audio/ogg";
        else if ((strcasecmp(ext, "mpg") == 0) || (strcasecmp(ext, "mpe") == 0) ||
                 (strcasecmp(ext, "mp4") == 0) || (strcasecmp(ext, "mpv") == 0)) type = "video/mpeg";
        else if (strcasecmp(ext, "webm") == 0) type = "video/webm";
        else if (strcasecmp(ext, "json") == 0) type = "application/json";
        else if (strcasecmp(ext, "wasm") == 0) type = "application/wasm";
        else if ((strcasecmp(ext, "gz") == 0) || (strcasecmp(ext, "gz") == 0)) type = "application/gzip";
        else if (strcasecmp(ext, "zip") == 0) type = "application/zip";
        else if (strcasecmp(ext, "pdf") == 0) type = "application/pdf";
        else if ((strcasecmp(ext, "doc") == 0) || (strcasecmp(ext, "dot") == 0)) type = "application/msword";
        else if ((strcasecmp(ext, "xls") == 0) || (strcasecmp(ext, "xlsx") == 0) ||
                 (strcasecmp(ext, "xlsm") == 0) || (strcasecmp(ext, "xlsb") == 0) ||
                 (strcasecmp(ext, "xlt") == 0) || (strcasecmp(ext, "xltx") == 0) ||
                 (strcasecmp(ext, "xlm") == 0) || (strcasecmp(ext, "xla") == 0)) type = "application/vnd.ms-excel";
        else if ((strcasecmp(ext, "ppt") == 0) || (strcasecmp(ext, "potx") == 0) ||
                 (strcasecmp(ext, "ppsx") == 0) || (strcasecmp(ext, "ppam") == 0)) type = "application/vnd.ms-powerpoint";
        else type = "text/plain";
    }
    else {
        type = "text/plain";
    }
    return type;
}

static const char * mimeToFileExtension(const char * type)
{
    const char *ext;

    if (strcasecmp(type, "text/html") == 0) ext = "htm";
    else if (strcasecmp(type, "text/xslt+xml") == 0) ext = "xsl";
    else if (strcasecmp(type, "text/javascript") == 0) ext = "js";
    else if (strcasecmp(type, "text/css") == 0) ext = "css";
    else if (strcasecmp(type, "text/xml") == 0) ext = "xml";
    else if (strcasecmp(type, "image/png") == 0) ext = "png";
    else if (strcasecmp(type, "image/svg+xml") == 0) ext = "svg";
    else if (strcasecmp(type, "image/jpeg") == 0) ext = "jpg";
    else if (strcasecmp(type, "image/gif") == 0) ext = "gif";
    else if (strcasecmp(type, "image/bmp") == 0) ext = "bmp";
    else if (strcasecmp(type, "image/x-icon") == 0) ext = "ico";
    else if (strcasecmp(type, "audio/mpeg") == 0) ext = "mp3";
    else if (strcasecmp(type, "audio/wav") == 0) ext = "wav";
    else if (strcasecmp(type, "audio/ogg") == 0) ext = "ogg";
    else if (strcasecmp(type, "video/mpeg") == 0) ext = "mp4";
    else if (strcasecmp(type, "video/webm") == 0) ext = "webm";
    else if (strcasecmp(type, "application/json") == 0) ext = "json";
    else if (strcasecmp(type, "application/wasm") == 0) ext = "wasm";
    else if (strcasecmp(type, "application/gzip") == 0) ext = "gz";
    else if (strcasecmp(type, "application/zip") == 0) ext = "zip";
    else if (strcasecmp(type, "application/pdf") == 0) ext = "pdf";
    else if (strcasecmp(type, "application/msword") == 0) ext = "doc";
    else if (strcasecmp(type, "application/vnd.ms-excel") == 0) ext = "xls";
    else if (strcasecmp(type, "application/vnd.ms-powerpoint") == 0) ext = "ppt";
    else ext = "txt";
    return ext;
}
