/*
  Copyright 2024 Northern.tech AS

  This file is part of CFEngine 3 - written and maintained by Northern.tech AS.

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

  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with this program; if not, write to the Free Software
  Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA

  To the extent this program is licensed as part of the Enterprise
  versions of CFEngine, the applicable Commercial Open Source License
  (COSL) may apply to this file if you as a licensee so wish it. See
  included file COSL.txt.
*/

#include <cfnet.h>                                 /* struct ConnectionInfo */
#include <client_code.h>
#include <communication.h>
#include <connection_info.h>
#include <classic.h>                  /* RecvSocketStream */
#include <net.h>                      /* SendTransaction,ReceiveTransaction */
#include <openssl/err.h>                                   /* ERR_get_error */
#include <protocol.h>                              /* ProtocolIsUndefined() */
#include <tls_client.h>               /* TLSTry */
#include <tls_generic.h>              /* TLSVerifyPeer */
#include <dir.h>
#include <unix.h>
#include <dir_priv.h>                          /* AllocateDirentForFilename */
#include <client_protocol.h>
#include <crypto.h>         /* CryptoInitialize,SavePublicKey,EncryptString */
#include <logging.h>
#include <hash.h>                                               /* HashFile */
#include <mutex.h>                                            /* ThreadLock */
#include <files_lib.h>                               /* FullWrite,safe_open */
#include <string_lib.h>                           /* MemSpan,MemSpanInverse */
#include <misc_lib.h>                                   /* ProgrammingError */
#include <printsize.h>                                         /* PRINTSIZE */
#include <lastseen.h>                                            /* LastSaw */


#define CFENGINE_SERVICE "cfengine"


/**
 * Initialize client's network library.
 */
bool cfnet_init(const char *tls_min_version, const char *ciphers)
{
    CryptoInitialize();
    return TLSClientInitialize(tls_min_version, ciphers);
}

void cfnet_shut()
{
    TLSDeInitialize();
    CryptoDeInitialize();
}

bool cfnet_IsInitialized()
{
    return TLSClientIsInitialized();
}

#define MAX_PORT_NUMBER 65535   /* 2^16 - 1 */

/* These should only be modified by the two functions below! */
int CFENGINE_PORT = 5308;
char CFENGINE_PORT_STR[16] = "5308";

void DetermineCfenginePort()
{
    nt_static_assert(sizeof(CFENGINE_PORT_STR) >= PRINTSIZE(CFENGINE_PORT));
    /* ... but we leave more space, as service names can be longer */

    errno = 0;
    struct servent *server = getservbyname(CFENGINE_SERVICE, "tcp");
    if (server != NULL)
    {
        CFENGINE_PORT = ntohs(server->s_port);
        snprintf(CFENGINE_PORT_STR, sizeof(CFENGINE_PORT_STR),
                 "%d", CFENGINE_PORT);
    }
    else if (errno == 0)
    {
        Log(LOG_LEVEL_VERBOSE,
            "No registered cfengine service, using default");
    }
    else
    {
        Log(LOG_LEVEL_VERBOSE,
            "Unable to query services database, using default. (getservbyname: %s)",
            GetErrorStr());
    }
    Log(LOG_LEVEL_VERBOSE, "Default port for cfengine is %d", CFENGINE_PORT);
}

bool SetCfenginePort(const char *port_str)
{
    /* parse and store the new value (use the string representation of the
     * parsed value because it may potentially be better/nicer/more
     * canonical) */
    long int port;
    int ret = StringToLong(port_str, &port);
    if (ret != 0)
    {
        LogStringToLongError(port_str, "CFENGINE_PORT", ret);
        return false;
    }
    if (port > MAX_PORT_NUMBER)
    {
        Log(LOG_LEVEL_ERR, "Invalid port number given, must be <= %d", MAX_PORT_NUMBER);
        return false;
    }

    CFENGINE_PORT = port;
    Log(LOG_LEVEL_VERBOSE, "Setting default port number to %d",
        CFENGINE_PORT);
    xsnprintf(CFENGINE_PORT_STR, sizeof(CFENGINE_PORT_STR),
              "%d", CFENGINE_PORT);
    return true;
}

/**
 * @return 1 success, 0 auth/ID error, -1 other error
 */
int TLSConnect(ConnectionInfo *conn_info, bool trust_server, const Rlist *restrict_keys,
               const char *ipaddr, const char *username)
{
    int ret;

    ret = TLSTry(conn_info);
    if (ret == -1)
    {
        return -1;
    }

    /* TODO username is local, fix. */
    ret = TLSVerifyPeer(conn_info, ipaddr, username);

    if (ret == -1)                                      /* error */
    {
        return -1;
    }

    const char *key_hash = KeyPrintableHash(conn_info->remote_key);

    // If restrict_key is defined, check if the key is there
    if (restrict_keys != NULL)
    {
        if (RlistContainsString(restrict_keys, key_hash))
        {
            Log(LOG_LEVEL_VERBOSE, "Server key in allowed list: %s", key_hash);
        }
        else
        {
            Log(LOG_LEVEL_ERR,
                "Server key not in allowed keys, server presented: %s", key_hash);
            return -1;
        }
    }

    if (ret == 1)
    {
        Log(LOG_LEVEL_VERBOSE,
            "Server is TRUSTED, received key '%s' MATCHES stored one.",
            key_hash);
    }
    else   /* ret == 0 */
    {
        if (trust_server)             /* We're most probably bootstrapping. */
        {
            Log(LOG_LEVEL_NOTICE, "Trusting new key: %s", key_hash);
            SavePublicKey(username, KeyPrintableHash(conn_info->remote_key),
                          KeyRSA(conn_info->remote_key));
        }
        else
        {
            Log(LOG_LEVEL_ERR,
                "TRUST FAILED, server presented untrusted key: %s", key_hash);
            return -1;
        }
    }

    /* TLS CONNECTION IS ESTABLISHED, negotiate protocol version and send
     * identification data. */
    ret = TLSClientIdentificationDialog(conn_info, username);

    return ret;
}

/**
 * @NOTE if #flags.protocol_version is CF_PROTOCOL_UNDEFINED, then latest
 *       protocol is used by default.
 */
AgentConnection *ServerConnection(const char *server, const char *port, const Rlist *restrict_keys,
                                  unsigned int connect_timeout,
                                  ConnectionFlags flags, int *err)
{
    AgentConnection *conn = NULL;
    int ret;
    *err = 0;

    conn = NewAgentConn(server, port, flags);

#if !defined(__MINGW32__) && !defined(__ANDROID__)
    signal(SIGPIPE, SIG_IGN);

    sigset_t signal_mask;
    sigemptyset(&signal_mask);
    sigaddset(&signal_mask, SIGPIPE);
    pthread_sigmask(SIG_BLOCK, &signal_mask, NULL);

    /* FIXME: username is local */
    GetCurrentUserName(conn->username, sizeof(conn->username));
#else
    /* Always say "root" if windows or termux. */
    strlcpy(conn->username, "root", sizeof(conn->username));
#endif

    if (port == NULL || *port == '\0')
    {
        port = CFENGINE_PORT_STR;
    }

    char txtaddr[CF_MAX_IP_LEN] = "";
    conn->conn_info->sd = SocketConnect(server, port, connect_timeout,
                                        flags.force_ipv4,
                                        txtaddr, sizeof(txtaddr));
    if (conn->conn_info->sd == -1)
    {
        Log(LOG_LEVEL_INFO, "No server is responding on port: %s",
            port);
        DisconnectServer(conn);
        *err = -1;
        return NULL;
    }

    nt_static_assert(sizeof(conn->remoteip) >= sizeof(txtaddr));
    strcpy(conn->remoteip, txtaddr);

    ProtocolVersion protocol_version = flags.protocol_version;
    if (ProtocolIsUndefined(protocol_version))
    {
        protocol_version = CF_PROTOCOL_LATEST;
    }

    if (ProtocolIsTLS(protocol_version))
    {
        /* Set the version to request during protocol negotiation. After
         * TLSConnect() it will have the version we finally ended up with. */
        conn->conn_info->protocol = protocol_version;

        ret = TLSConnect(conn->conn_info, flags.trust_server, restrict_keys,
                         conn->remoteip, conn->username);

        if (ret == -1)                                      /* Error */
        {
            DisconnectServer(conn);
            *err = -1;
            return NULL;
        }
        else if (ret == 0)                             /* Auth/ID error */
        {
            DisconnectServer(conn);
            errno = EPERM;
            *err = -2;
            return NULL;
        }
        assert(ret == 1);

        conn->conn_info->status = CONNECTIONINFO_STATUS_ESTABLISHED;
        if (!flags.off_the_record)
        {
            LastSaw1(conn->remoteip, KeyPrintableHash(conn->conn_info->remote_key),
                     LAST_SEEN_ROLE_CONNECT);
        }
    }
    else if (ProtocolIsClassic(protocol_version))
    {
        conn->conn_info->protocol = CF_PROTOCOL_CLASSIC;
        conn->encryption_type = CfEnterpriseOptions();

        if (!IdentifyAgent(conn->conn_info))
        {
            Log(LOG_LEVEL_ERR, "Id-authentication for '%s' failed", VFQNAME);
            errno = EPERM;
            DisconnectServer(conn);
            *err = -2; // auth err
            return NULL;
        }

        if (!AuthenticateAgent(conn, flags.trust_server))
        {
            Log(LOG_LEVEL_ERR, "Authentication dialogue with '%s' failed", server);
            errno = EPERM;
            DisconnectServer(conn);
            *err = -2; // auth err
            return NULL;
        }
        conn->conn_info->status = CONNECTIONINFO_STATUS_ESTABLISHED;
    }
    else
    {
        ProgrammingError("ServerConnection: ProtocolVersion %d!",
                         protocol_version);
    }

    conn->authenticated = true;
    return conn;
}

/*********************************************************************/

void DisconnectServer(AgentConnection *conn)
{
    /* Socket needs to be closed even after SSL_shutdown. */
    if (conn->conn_info->sd >= 0)                 /* Not INVALID or OFFLINE */
    {
        if (conn->conn_info->protocol >= CF_PROTOCOL_TLS &&
            conn->conn_info->ssl != NULL)
        {
            SSL_shutdown(conn->conn_info->ssl);
        }

        cf_closesocket(conn->conn_info->sd);
        conn->conn_info->sd = SOCKET_INVALID;
        Log(LOG_LEVEL_VERBOSE, "Connection to %s is closed", conn->remoteip);
    }
    DeleteAgentConn(conn);
}

/*********************************************************************/

/* Returning NULL (an empty list) does not mean empty directory but ERROR,
 * since every directory has to contain at least . and .. */
Item *RemoteDirList(const char *dirname, bool encrypt, AgentConnection *conn)
{
    char sendbuffer[CF_BUFSIZE];
    char recvbuffer[CF_BUFSIZE];
    char in[CF_BUFSIZE];
    char out[CF_BUFSIZE];
    int cipherlen = 0, tosend;

    if (strlen(dirname) > CF_BUFSIZE - 20)
    {
        Log(LOG_LEVEL_ERR, "Directory name too long");
        return NULL;
    }

    /* We encrypt only for CLASSIC protocol. The TLS protocol is always over
     * encrypted layer, so it does not support encrypted (S*) commands. */
    encrypt = encrypt && conn->conn_info->protocol == CF_PROTOCOL_CLASSIC;

    if (encrypt)
    {
        if (conn->session_key == NULL)
        {
            Log(LOG_LEVEL_ERR, "Cannot do encrypted copy without keys (use cf-key)");
            return NULL;
        }

        snprintf(in, CF_BUFSIZE, "OPENDIR %s", dirname);
        cipherlen = EncryptString(out, sizeof(out), in, strlen(in) + 1, conn->encryption_type, conn->session_key);

        tosend = cipherlen + CF_PROTO_OFFSET;

        if (tosend < 0)
        {
            ProgrammingError("RemoteDirList: tosend (%d) < 0", tosend);
        }
        else if ((unsigned long) tosend > sizeof(sendbuffer))
        {
            ProgrammingError("RemoteDirList: tosend (%d) > sendbuffer (%zd)",
                             tosend, sizeof(sendbuffer));
        }

        snprintf(sendbuffer, CF_BUFSIZE - 1, "SOPENDIR %d", cipherlen);
        memcpy(sendbuffer + CF_PROTO_OFFSET, out, cipherlen);
    }
    else
    {
        snprintf(sendbuffer, CF_BUFSIZE, "OPENDIR %s", dirname);
        tosend = strlen(sendbuffer);
    }

    if (SendTransaction(conn->conn_info, sendbuffer, tosend, CF_DONE) == -1)
    {
        return NULL;
    }

    Item *start = NULL, *end = NULL;                  /* NULL is empty list */
    while (true)
    {
        /* TODO check the CF_MORE flag, no need for CFD_TERMINATOR. */
        int nbytes = ReceiveTransaction(conn->conn_info, recvbuffer, NULL);

        /* If recv error or socket closed before receiving CFD_TERMINATOR. */
        if (nbytes == -1)
        {
            /* TODO mark connection in the cache as closed. */
            goto err;
        }

        if (encrypt)
        {
            memcpy(in, recvbuffer, nbytes);
            DecryptString(recvbuffer, sizeof(recvbuffer), in, nbytes,
                          conn->encryption_type, conn->session_key);
        }

        if (recvbuffer[0] == '\0')
        {
            Log(LOG_LEVEL_ERR,
                "Empty%s server packet when listing directory '%s'!",
                (start == NULL) ? " first" : "",
                dirname);
            goto err;
        }

        if (FailedProtoReply(recvbuffer))
        {
            Log(LOG_LEVEL_INFO, "Network access to '%s:%s' denied", conn->this_server, dirname);
            goto err;
        }

        if (BadProtoReply(recvbuffer))
        {
            Log(LOG_LEVEL_INFO, "%s", recvbuffer + strlen("BAD: "));
            goto err;
        }

        /* Double '\0' means end of packet. */
        for (char *sp = recvbuffer; *sp != '\0'; sp += strlen(sp) + 1)
        {
            if (strcmp(sp, CFD_TERMINATOR) == 0)      /* end of all packets */
            {
                return start;
            }

            Item *ip = xcalloc(1, sizeof(Item));
            ip->name = (char *) AllocateDirentForFilename(sp);

            if (start == NULL)  /* First element */
            {
                start = ip;
                end = ip;
            }
            else
            {
                end->next = ip;
                end = ip;
            }
        }
    }

    return start;

  err:                                                         /* free list */
    for (Item *ip = start; ip != NULL; ip = start)
    {
        start = ip->next;
        free(ip->name);
        free(ip);
    }

    return NULL;
}

/*********************************************************************/

bool CompareHashNet(const char *file1, const char *file2, bool encrypt, AgentConnection *conn)
{
    unsigned char d[EVP_MAX_MD_SIZE + 1];
    char *sp;
    char sendbuffer[CF_BUFSIZE] = {0};
    char recvbuffer[CF_BUFSIZE] = {0};
    int i, tosend, cipherlen;

    HashFile(file2, d, CF_DEFAULT_DIGEST, false);

    memset(recvbuffer, 0, CF_BUFSIZE);

    /* We encrypt only for CLASSIC protocol. The TLS protocol is always over
     * encrypted layer, so it does not support encrypted (S*) commands. */
    encrypt = encrypt && conn->conn_info->protocol == CF_PROTOCOL_CLASSIC;

    if (encrypt)
    {
        char in[CF_BUFSIZE] = {0};
        char out[CF_BUFSIZE] = {0};
        snprintf(in, CF_BUFSIZE, "MD5 %s", file1);

        sp = in + strlen(in) + CF_SMALL_OFFSET;

        for (i = 0; i < CF_DEFAULT_DIGEST_LEN; i++)
        {
            *sp++ = d[i];
        }

        cipherlen =
            EncryptString(out, sizeof(out), in,
                          strlen(in) + CF_SMALL_OFFSET + CF_DEFAULT_DIGEST_LEN,
                          conn->encryption_type, conn->session_key);

        tosend = cipherlen + CF_PROTO_OFFSET;

        if (tosend < 0)
        {
            ProgrammingError("CompareHashNet: tosend (%d) < 0", tosend);
        }
        else if ((unsigned long) tosend > sizeof(sendbuffer))
        {
            ProgrammingError("CompareHashNet: tosend (%d) > sendbuffer (%zd)",
                             tosend, sizeof(sendbuffer));
        }

        snprintf(sendbuffer, CF_BUFSIZE, "SMD5 %d", cipherlen);
        memcpy(sendbuffer + CF_PROTO_OFFSET, out, cipherlen);
    }
    else
    {
        snprintf(sendbuffer, CF_BUFSIZE, "MD5 %s", file1);
        sp = sendbuffer + strlen(sendbuffer) + CF_SMALL_OFFSET;

        for (i = 0; i < CF_DEFAULT_DIGEST_LEN; i++)
        {
            *sp++ = d[i];
        }

        tosend = strlen(sendbuffer) + CF_SMALL_OFFSET + CF_DEFAULT_DIGEST_LEN;
    }

    if (SendTransaction(conn->conn_info, sendbuffer, tosend, CF_DONE) == -1)
    {
        Log(LOG_LEVEL_ERR, "Failed send. (SendTransaction: %s)", GetErrorStr());
        Log(LOG_LEVEL_VERBOSE, "Networking error, assuming different checksum");
        return true;
    }

    if (ReceiveTransaction(conn->conn_info, recvbuffer, NULL) == -1)
    {
        /* TODO mark connection in the cache as closed. */
        Log(LOG_LEVEL_ERR, "Failed receive. (ReceiveTransaction: %s)", GetErrorStr());
        Log(LOG_LEVEL_VERBOSE, "No answer from host, assuming different checksum");
        return true;
    }

    if (strcmp(CFD_TRUE, recvbuffer) == 0)
    {
        return true;            /* mismatch */
    }
    else
    {
        return false;
    }

/* Not reached */
}

/*********************************************************************/


static bool EncryptCopyRegularFileNet(const char *source, const char *dest, off_t size, AgentConnection *conn)
{
    int blocksize = 2048, n_read = 0, plainlen, more = true, finlen;
    int tosend, cipherlen = 0;
    char *buf, in[CF_BUFSIZE], out[CF_BUFSIZE], workbuf[CF_BUFSIZE], cfchangedstr[265];
    unsigned char iv[32] =
        { 1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, 7, 8 };

    snprintf(cfchangedstr, 255, "%s%s", CF_CHANGEDSTR1, CF_CHANGEDSTR2);

    if ((strlen(dest) > CF_BUFSIZE - 20))
    {
        Log(LOG_LEVEL_ERR, "Filename too long");
        return false;
    }

    unlink(dest);                /* To avoid link attacks */

    int dd = safe_open_create_perms(dest, O_WRONLY | O_CREAT | O_TRUNC | O_EXCL | O_BINARY, CF_PERMS_DEFAULT);
    if (dd == -1)
    {
        Log(LOG_LEVEL_ERR,
            "Copy from server '%s' to destination '%s' failed (open: %s)",
            conn->this_server, dest, GetErrorStr());
        unlink(dest);
        return false;
    }

    if (size == 0)
    {
        // No sense in copying an empty file
        close(dd);
        return true;
    }

    workbuf[0] = '\0';

    snprintf(in, CF_BUFSIZE - CF_PROTO_OFFSET, "GET dummykey %s", source);
    cipherlen = EncryptString(out, sizeof(out), in, strlen(in) + 1, conn->encryption_type, conn->session_key);

    tosend = cipherlen + CF_PROTO_OFFSET;

    if (tosend < 0)
    {
        ProgrammingError("EncryptCopyRegularFileNet: tosend (%d) < 0", tosend);
    }
    else if ((unsigned long) tosend > sizeof(workbuf))
    {
        ProgrammingError("EncryptCopyRegularFileNet: tosend (%d) > workbuf (%zd)",
                         tosend, sizeof(workbuf));
    }

    snprintf(workbuf, CF_BUFSIZE, "SGET %4d %4d", cipherlen, blocksize);
    memcpy(workbuf + CF_PROTO_OFFSET, out, cipherlen);

/* Send proposition C0 - query */

    if (SendTransaction(conn->conn_info, workbuf, tosend, CF_DONE) == -1)
    {
        Log(LOG_LEVEL_ERR, "Couldn't send data. (SendTransaction: %s)", GetErrorStr());
        close(dd);
        return false;
    }

    EVP_CIPHER_CTX *crypto_ctx = EVP_CIPHER_CTX_new();
    if (crypto_ctx == NULL)
    {
        Log(LOG_LEVEL_ERR, "Failed to allocate cipher: %s",
            TLSErrorString(ERR_get_error()));
        close(dd);
        return false;
    }

    buf = xmalloc(CF_BUFSIZE + sizeof(int));

    bool   last_write_made_hole = false;
    size_t n_wrote_total        = 0;

    while (more)
    {
        if ((cipherlen = ReceiveTransaction(conn->conn_info, buf, &more)) == -1)
        {
            close(dd);
            free(buf);
            EVP_CIPHER_CTX_free(crypto_ctx);
            return false;
        }


        /* If the first thing we get is an error message, break. */

        if (n_wrote_total == 0 &&
            strncmp(buf + CF_INBAND_OFFSET, CF_FAILEDSTR, strlen(CF_FAILEDSTR)) == 0)
        {
            Log(LOG_LEVEL_INFO, "Network access to '%s:%s' denied", conn->this_server, source);
            close(dd);
            free(buf);
            EVP_CIPHER_CTX_free(crypto_ctx);
            return false;
        }

        if (strncmp(buf + CF_INBAND_OFFSET, cfchangedstr, strlen(cfchangedstr)) == 0)
        {
            Log(LOG_LEVEL_INFO, "Source '%s:%s' changed while copying", conn->this_server, source);
            close(dd);
            free(buf);
            EVP_CIPHER_CTX_free(crypto_ctx);
            return false;
        }

        EVP_DecryptInit_ex(crypto_ctx, CfengineCipher(CfEnterpriseOptions()), NULL, conn->session_key, iv);

        if (!EVP_DecryptUpdate(crypto_ctx, (unsigned char *) workbuf, &plainlen, (unsigned char *) buf, cipherlen))
        {
            close(dd);
            free(buf);
            EVP_CIPHER_CTX_free(crypto_ctx);
            return false;
        }

        if (!EVP_DecryptFinal_ex(crypto_ctx, (unsigned char *) workbuf + plainlen, &finlen))
        {
            close(dd);
            free(buf);
            EVP_CIPHER_CTX_free(crypto_ctx);
            return false;
        }

        n_read = plainlen + finlen;

        bool w_ok = FileSparseWrite(dd, workbuf, n_read,
                                    &last_write_made_hole);
        if (!w_ok)
        {
            Log(LOG_LEVEL_ERR,
                "Local disk write failed copying '%s:%s' to '%s'",
                conn->this_server, source, dest);
            free(buf);
            unlink(dest);
            close(dd);
            conn->error = true;
            EVP_CIPHER_CTX_free(crypto_ctx);
            return false;
        }

        n_wrote_total += n_read;
    }

    const bool do_sync = false;

    bool ret = FileSparseClose(dd, dest, do_sync,
                               n_wrote_total, last_write_made_hole);
    if (!ret)
    {
        unlink(dest);
        free(buf);
        EVP_CIPHER_CTX_free(crypto_ctx);
        return false;
    }

    free(buf);
    EVP_CIPHER_CTX_free(crypto_ctx);
    return true;
}

static void FlushFileStream(int sd, int toget)
{
    int i;
    char buffer[2];

    Log(LOG_LEVEL_VERBOSE, "Flushing rest of file...%d bytes", toget);

    for (i = 0; i < toget; i++)
    {
        recv(sd, buffer, 1, 0); /* flush to end of current file */
    }
}

/* TODO finalise socket or TLS session in all cases that this function fails
 * and the transaction protocol is out of sync. */
bool CopyRegularFileNet(const char *source, const char *dest, off_t size,
                        bool encrypt, AgentConnection *conn, mode_t mode)
{
    char *buf, workbuf[CF_BUFSIZE], cfchangedstr[265];
    const int buf_size = 2048;

    /* We encrypt only for CLASSIC protocol. The TLS protocol is always over
     * encrypted layer, so it does not support encrypted (S*) commands. */
    encrypt = encrypt && conn->conn_info->protocol == CF_PROTOCOL_CLASSIC;

    if (encrypt)
    {
        return EncryptCopyRegularFileNet(source, dest, size, conn);
    }

    snprintf(cfchangedstr, 255, "%s%s", CF_CHANGEDSTR1, CF_CHANGEDSTR2);

    if ((strlen(dest) > CF_BUFSIZE - 20))
    {
        Log(LOG_LEVEL_ERR, "Filename too long");
        return false;
    }

    unlink(dest);                /* To avoid link attacks */

    int dd = safe_open_create_perms(dest, O_WRONLY | O_CREAT | O_TRUNC | O_EXCL | O_BINARY, mode);
    if (dd == -1)
    {
        Log(LOG_LEVEL_ERR,
            "Copy from server '%s' to destination '%s' failed (open: %s)",
            conn->this_server, dest, GetErrorStr());
        unlink(dest);
        return false;
    }

    workbuf[0] = '\0';
    int tosend = snprintf(workbuf, CF_BUFSIZE, "GET %d %s", buf_size, source);
    if (tosend <= 0 || tosend >= CF_BUFSIZE)
    {
        Log(LOG_LEVEL_ERR, "Failed to compose GET command for file %s",
            source);
        close(dd);
        return false;
    }

    /* Send proposition C0 */

    if (SendTransaction(conn->conn_info, workbuf, tosend, CF_DONE) == -1)
    {
        Log(LOG_LEVEL_ERR, "Couldn't send GET command");
        close(dd);
        return false;
    }

    buf = xmalloc(CF_BUFSIZE + sizeof(int));    /* Note CF_BUFSIZE not buf_size !! */

    Log(LOG_LEVEL_VERBOSE, "Copying remote file '%s:%s', expecting %jd bytes",
          conn->this_server, source, (intmax_t)size);

    int n_wrote_total = 0;
    bool last_write_made_hole = false;
    while (n_wrote_total < size)
    {
        int toget = MIN(size - n_wrote_total, buf_size);

        assert(toget > 0);

        /* Stage C1 - receive */
        int n_read;

        const ProtocolVersion version = conn->conn_info->protocol;

        if (ProtocolIsClassic(version))
        {
            n_read = RecvSocketStream(conn->conn_info->sd, buf, toget);
        }
        else if (ProtocolIsTLS(version))
        {
            n_read = TLSRecv(conn->conn_info->ssl, buf, toget);
        }
        else
        {
            UnexpectedError("CopyRegularFileNet: ProtocolVersion %d!",
                            conn->conn_info->protocol);
            n_read = -1;
        }

        /* TODO what if 0 < n_read < toget? Might happen with TLS. */

        if (n_read <= 0)
        {
            /* This may happen on race conditions, where the file has shrunk
             * since we asked for its size in SYNCH ... STAT source */

            Log(LOG_LEVEL_ERR,
                "Error in client-server stream, has %s:%s shrunk? (code %d)",
                conn->this_server, source, n_read);

            close(dd);
            free(buf);
            return false;
        }

        /* If the first thing we get is an error message, break. */

        if (n_wrote_total == 0
            && strncmp(buf, CF_FAILEDSTR, strlen(CF_FAILEDSTR)) == 0)
        {
            Log(LOG_LEVEL_INFO, "Network access to '%s:%s' denied",
                conn->this_server, source);
            close(dd);
            free(buf);
            return false;
        }

        if (strncmp(buf, cfchangedstr, strlen(cfchangedstr)) == 0)
        {
            Log(LOG_LEVEL_INFO, "Source '%s:%s' changed while copying",
                conn->this_server, source);
            close(dd);
            free(buf);
            return false;
        }

        /* Check for mismatch between encryption here and on server. */

        int value = -1;
        sscanf(buf, "t %d", &value);

        if ((value > 0) && (strncmp(buf + CF_INBAND_OFFSET, "BAD: ", 5) == 0))
        {
            Log(LOG_LEVEL_INFO, "Network access to cleartext '%s:%s' denied",
                conn->this_server, source);
            close(dd);
            free(buf);
            return false;
        }

        bool w_ok = FileSparseWrite(dd, buf, n_read,
                                    &last_write_made_hole);
        if (!w_ok)
        {
            Log(LOG_LEVEL_ERR,
                "Local disk write failed copying '%s:%s' to '%s'",
                conn->this_server, source, dest);
            free(buf);
            unlink(dest);
            close(dd);
            FlushFileStream(conn->conn_info->sd, size - n_wrote_total - n_read);
            conn->error = true;
            return false;
        }

        n_wrote_total += n_read;
    }

    const bool do_sync = false;

    bool ret = FileSparseClose(dd, dest, do_sync,
                               n_wrote_total, last_write_made_hole);
    if (!ret)
    {
        unlink(dest);
        free(buf);
        FlushFileStream(conn->conn_info->sd, size - n_wrote_total);
        return false;
    }

    free(buf);
    return true;
}
