/*
 * Copyright (c) 2016 Cloudbase Solutions Srl
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include <config.h>
#include <errno.h>
#include <sys/types.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "poll-loop.h"
#include "dirs.h"
#include "util.h"
#include "stream-provider.h"
#include "openvswitch/vlog.h"

VLOG_DEFINE_THIS_MODULE(stream_windows);

static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(10, 25);

static void maybe_unlink_and_free(char *path);

/* Suggested buffer size at the creation of the named pipe for reading and
 * and writing operations. */
#define BUFSIZE 65000

/* Default prefix of a local named pipe. */
#define LOCAL_PREFIX "\\\\.\\pipe\\"

/* This function has the purpose to remove all the slashes received in s. */
static char *
remove_slashes(char *s)
{
    char *p1, *p2;
    p1 = p2 = s;

    while (*p1) {
        if ((*p1) == '\\' || (*p1) == '/') {
            p1++;
        } else {
            *p2 = *p1;
            p2++;
            p1++;
        }
    }
    *p2 = '\0';
    return s;
}

/* Active named pipe. */
struct windows_stream
{
    struct stream stream;
    HANDLE fd;
    /* Overlapped operations used for reading/writing. */
    OVERLAPPED read;
    OVERLAPPED write;
    /* Flag to check if a reading/writing operation is pending. */
    bool read_pending;
    bool write_pending;
    /* Flag to check if fd is a server HANDLE.  In the case of a server handle
     * we have to issue a disconnect before closing the actual handle. */
    bool server;
    bool retry_connect;
    char *pipe_path;
};

static struct windows_stream *
stream_windows_cast(struct stream *stream)
{
    stream_assert_class(stream, &windows_stream_class);
    return CONTAINER_OF(stream, struct windows_stream, stream);
}

static HANDLE
create_snpipe(char *path)
{
    return CreateFile(path, GENERIC_READ | GENERIC_WRITE, 0, NULL,
                      OPEN_EXISTING,
                      FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED |
                      FILE_FLAG_NO_BUFFERING,
                      NULL);
}

/* Active named pipe open. */
static int
windows_open(const char *name, char *suffix, struct stream **streamp,
             uint8_t dscp OVS_UNUSED)
{
    char *connect_path;
    HANDLE npipe;
    DWORD mode = PIPE_READMODE_BYTE;
    char *path;
    FILE *file;
    bool retry = false;
    /* If the path does not contain a ':', assume it is relative to
     * OVS_RUNDIR. */
    if (!strchr(suffix, ':')) {
        path = xasprintf("%s/%s", ovs_rundir(), suffix);
    } else {
        path = xstrdup(suffix);
    }

    /* In case of "unix:" argument, the assumption is that there is a file
     * created in the path (name). */
    file = fopen(path, "r");
    if (!file) {
        free(path);
        VLOG_DBG_RL(&rl, "%s: could not open %s (%s)", name, suffix,
                    ovs_strerror(errno));
        return ENOENT;
    } else {
        fclose(file);
    }

    /* Valid pipe names do not have slashes.  The assumption is that the named
     * pipe was created with the name "path", with slashes removed and the
     * default prefix \\.\pipe\ appended.
     * Strip the slashes from the parameter name and append the default prefix.
     */
    connect_path = xasprintf("%s%s", LOCAL_PREFIX, remove_slashes(path));
    free(path);

    /* Try to connect to the named pipe.  In case all pipe instances are
     * busy we set the retry flag to true and retry again during the
     * connect function.  Use overlapped flag and file no buffering to ensure
     * asynchronous operations. */
    npipe = create_snpipe(connect_path);

    if (npipe == INVALID_HANDLE_VALUE && GetLastError() == ERROR_PIPE_BUSY) {
        retry = true;
    }

    if (!retry && npipe == INVALID_HANDLE_VALUE) {
        VLOG_ERR_RL(&rl, "Could not connect to named pipe: %s",
                    ovs_lasterror_to_string());
        free(connect_path);
        return ENOENT;
    }
    if (!retry && !SetNamedPipeHandleState(npipe, &mode, NULL, NULL)) {
        VLOG_ERR_RL(&rl, "Could not set named pipe options: %s",
                    ovs_lasterror_to_string());
        free(connect_path);
        CloseHandle(npipe);
        return ENOENT;
    }
    struct windows_stream *s = xmalloc(sizeof *s);
    stream_init(&s->stream, &windows_stream_class, 0, name);
    s->pipe_path = connect_path;
    s->fd = npipe;
    /* This is an active stream. */
    s->server = false;
    /* Create events for reading and writing to be signaled later. */
    memset(&s->read, 0, sizeof(s->read));
    memset(&s->write, 0, sizeof(s->write));
    s->read.hEvent = CreateEvent(NULL, TRUE, TRUE, NULL);
    s->write.hEvent = CreateEvent(NULL, TRUE, TRUE, NULL);
    /* Initial read and write operations are not pending. */
    s->read_pending = false;
    s->write_pending = false;
    s->retry_connect = retry;
    *streamp = &s->stream;
    return 0;
}

/* Active named pipe close. */
static void
windows_close(struct stream *stream)
{
    struct windows_stream *s = stream_windows_cast(stream);
    /* Disconnect the named pipe in case it was created from a passive stream.
     */
    if (s->server) {
        DisconnectNamedPipe(s->fd);
    }
    CloseHandle(s->fd);
    CloseHandle(s->read.hEvent);
    CloseHandle(s->write.hEvent);
    if (s->pipe_path) {
        free(s->pipe_path);
    }
    free(s);
}

/* Active named pipe connect. */
static int
windows_connect(struct stream *stream)
{
    struct windows_stream *s = stream_windows_cast(stream);

    if (!s->retry_connect) {
        return 0;
    } else {
        HANDLE npipe;
        npipe = create_snpipe(s->pipe_path);
        if (npipe == INVALID_HANDLE_VALUE) {
            if (GetLastError() == ERROR_PIPE_BUSY) {
                return EAGAIN;
            } else {
                s->retry_connect = false;
                return ENOENT;
            }
        }
        s->retry_connect = false;
        s->fd = npipe;
        return 0;
    }
}

/* Active named pipe receive. */
static ssize_t
windows_recv(struct stream *stream, void *buffer, size_t n)
{
    struct windows_stream *s = stream_windows_cast(stream);
    ssize_t retval = 0;
    boolean result = false;
    DWORD last_error = 0;
    LPOVERLAPPED  ov = NULL;
    ov = &s->read;

    /* If the read operation was pending, we verify its result. */
    if (s->read_pending) {
        if (!GetOverlappedResult(s->fd, ov, &(DWORD)retval, FALSE)) {
            last_error = GetLastError();
            if (last_error == ERROR_IO_INCOMPLETE) {
                /* If the operation is still pending, retry again. */
                s->read_pending = true;
                return -EAGAIN;
            } else if (last_error == ERROR_PIPE_NOT_CONNECTED
                       || last_error == ERROR_BAD_PIPE
                       || last_error == ERROR_NO_DATA
                       || last_error == ERROR_BROKEN_PIPE) {
                /* If the pipe was disconnected, return 0. */
                return 0;
            } else {
                VLOG_ERR_RL(&rl, "Could not receive data on named pipe. Last "
                            "error: %s", ovs_lasterror_to_string());
                return -EINVAL;
            }
        }
        s->read_pending = false;
        return retval;
    }

    result = ReadFile(s->fd, buffer, n, &(DWORD)retval, ov);

    if (!result && GetLastError() == ERROR_IO_PENDING) {
        /* Mark the read operation as pending. */
        s->read_pending = true;
        return -EAGAIN;
    } else if (!result) {
        last_error = GetLastError();
        if (last_error == ERROR_PIPE_NOT_CONNECTED
            || last_error == ERROR_BAD_PIPE
            || last_error == ERROR_NO_DATA
            || last_error == ERROR_BROKEN_PIPE) {
            /* If the pipe was disconnected, return 0. */
            return 0;
        }
        VLOG_ERR_RL(&rl, "Could not receive data synchronous on named pipe."
                    "Last error: %s", ovs_lasterror_to_string());
        return -EINVAL;
    }

    return retval;
}

/* Active named pipe send. */
static ssize_t
windows_send(struct stream *stream, const void *buffer, size_t n)
{
    struct windows_stream *s = stream_windows_cast(stream);
    ssize_t retval = 0;
    boolean result = false;
    DWORD last_error = 0;
    LPOVERLAPPED  ov = NULL;
    ov = &s->write;

    /* If the send operation was pending, we verify the result. */
    if (s->write_pending) {
        if (!GetOverlappedResult(s->fd, ov, &(DWORD)retval, FALSE)) {
            last_error = GetLastError();
            if (last_error == ERROR_IO_INCOMPLETE) {
                /* If the operation is still pending, retry again. */
                s->write_pending = true;
                return -EAGAIN;
            } else if (last_error == ERROR_PIPE_NOT_CONNECTED
                       || last_error == ERROR_BAD_PIPE
                       || last_error == ERROR_NO_DATA
                       || last_error == ERROR_BROKEN_PIPE) {
                /* If the pipe was disconnected, return connection reset. */
                return -WSAECONNRESET;
            } else {
                VLOG_ERR_RL(&rl, "Could not send data on named pipe. Last "
                            "error: %s", ovs_lasterror_to_string());
                return -EINVAL;
            }
        }
        s->write_pending = false;
        return retval;
    }

    result = WriteFile(s->fd, buffer, n, &(DWORD)retval, ov);
    last_error = GetLastError();
    if (!result && GetLastError() == ERROR_IO_PENDING) {
        /* Mark the send operation as pending. */
        s->write_pending = true;
        return -EAGAIN;
    } else if (!result && (last_error == ERROR_PIPE_NOT_CONNECTED
                           || last_error == ERROR_BAD_PIPE
                           || last_error == ERROR_NO_DATA
                           || last_error == ERROR_BROKEN_PIPE)) {
        /* If the pipe was disconnected, return connection reset. */
        return -WSAECONNRESET;
    } else if (!result) {
        VLOG_ERR_RL(&rl, "Could not send data on synchronous named pipe. Last "
                    "error: %s", ovs_lasterror_to_string());
        return -EINVAL;
    }
    return (retval > 0 ? retval : -EAGAIN);
}

/* Active named pipe wait. */
static void
windows_wait(struct stream *stream, enum stream_wait_type wait)
{
    struct windows_stream *s = stream_windows_cast(stream);
    switch (wait) {
    case STREAM_SEND:
        poll_wevent_wait(s->write.hEvent);
        break;

    case STREAM_CONNECT:
        poll_immediate_wake();
        break;

    case STREAM_RECV:
        poll_wevent_wait(s->read.hEvent);
        break;

    default:
        OVS_NOT_REACHED();
    }
}

/* Passive named pipe. */
const struct stream_class windows_stream_class = {
    "unix",                     /* name */
    false,                      /* needs_probes */
    windows_open,               /* open */
    windows_close,              /* close */
    windows_connect,            /* connect */
    windows_recv,               /* recv */
    windows_send,               /* send */
    NULL,                       /* run */
    NULL,                       /* run_wait */
    windows_wait,               /* wait */
};

struct pwindows_pstream
{
    struct pstream pstream;
    HANDLE fd;
    /* Unlink path to be deleted during close. */
    char *unlink_path;
    /* Overlapped operation used for connect. */
    OVERLAPPED connect;
    /* Flag to check if an operation is pending. */
    bool pending;
    /* String used to create the named pipe. */
    char *pipe_path;
};

const struct pstream_class pwindows_pstream_class;

static struct pwindows_pstream *
pwindows_pstream_cast(struct pstream *pstream)
{
    pstream_assert_class(pstream, &pwindows_pstream_class);
    return CONTAINER_OF(pstream, struct pwindows_pstream, pstream);
}

/* Create a named pipe with read/write access, overlapped, message mode for
 * writing, byte mode for reading and with a maximum of 64 active instances. */
static HANDLE
create_pnpipe(char *name)
{
    SECURITY_ATTRIBUTES sa;
    sa.nLength = sizeof(sa);
    sa.lpSecurityDescriptor = NULL;
    sa.bInheritHandle = TRUE;
    if (strlen(name) > 256) {
        VLOG_ERR_RL(&rl, "Named pipe name too long.");
        return INVALID_HANDLE_VALUE;
    }
    return CreateNamedPipe(name, PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED,
                           PIPE_TYPE_MESSAGE | PIPE_READMODE_BYTE | PIPE_WAIT,
                           64, BUFSIZE, BUFSIZE, 0, &sa);
}

/* Passive named pipe connect.  This function creates a new named pipe and
 * passes the old handle to the active stream. */
static int
pwindows_accept(struct pstream *pstream, struct stream **new_streamp)
{
    struct pwindows_pstream *p = pwindows_pstream_cast(pstream);
    DWORD last_error = 0;
    DWORD cbRet;
    HANDLE npipe;

    /* If the connect operation was pending, verify the result. */
    if (p->pending) {
        if (!GetOverlappedResult(p->fd, &p->connect, &cbRet, FALSE)) {
            last_error = GetLastError();
            if (last_error == ERROR_IO_INCOMPLETE) {
                /* If the operation is still pending, retry again. */
                p->pending = true;
                return EAGAIN;
            } else {
                VLOG_ERR_RL(&rl, "Could not connect named pipe. Last "
                            "error: %s", ovs_lasterror_to_string());
                DisconnectNamedPipe(p->fd);
                return EINVAL;
            }
        }
        p->pending = false;
    }

    if (!p->pending && !ConnectNamedPipe(p->fd, &p->connect)) {
        last_error = GetLastError();
        if (last_error == ERROR_IO_PENDING) {
            /* Mark the accept operation as pending. */
            p->pending = true;
            return EAGAIN;
        } else if (last_error != ERROR_PIPE_CONNECTED) {
            VLOG_ERR_RL(&rl, "Could not connect synchronous named pipe. Last "
                        "error: %s", ovs_lasterror_to_string());
            DisconnectNamedPipe(p->fd);
            return EINVAL;
        } else {
            /* If the pipe is connected, signal an event. */
            SetEvent(&p->connect.hEvent);
        }
    }

    npipe = create_pnpipe(p->pipe_path);
    if (npipe == INVALID_HANDLE_VALUE) {
        VLOG_ERR_RL(&rl, "Could not create a new named pipe after connect. ",
                    ovs_lasterror_to_string());
        return ENOENT;
    }

    /* Give the handle p->fd to the new created active stream and specify it
     * was created by an active stream. */
    struct windows_stream *p_temp = xmalloc(sizeof *p_temp);
    stream_init(&p_temp->stream, &windows_stream_class, 0, "unix");
    p_temp->fd = p->fd;
    /* Specify it was created by a passive stream. */
    p_temp->server = true;
    /* Create events for read/write operations. */
    memset(&p_temp->read, 0, sizeof(p_temp->read));
    memset(&p_temp->write, 0, sizeof(p_temp->write));
    p_temp->read.hEvent = CreateEvent(NULL, TRUE, TRUE, NULL);
    p_temp->write.hEvent = CreateEvent(NULL, TRUE, TRUE, NULL);
    p_temp->read_pending = false;
    p_temp->write_pending = false;
    p_temp->retry_connect = false;
    p_temp->pipe_path = NULL;
    *new_streamp = &p_temp->stream;

    /* The passive handle p->fd will be the new created handle. */
    p->fd = npipe;
    memset(&p->connect, 0, sizeof(p->connect));
    p->connect.hEvent = CreateEvent(NULL, TRUE, TRUE, NULL);
    p->pending = false;
    return 0;
}

/* Passive named pipe close. */
static void
pwindows_close(struct pstream *pstream)
{
    struct pwindows_pstream *p = pwindows_pstream_cast(pstream);
    DisconnectNamedPipe(p->fd);
    CloseHandle(p->fd);
    CloseHandle(p->connect.hEvent);
    maybe_unlink_and_free(p->unlink_path);
    free(p->pipe_path);
    free(p);
}

/* Passive named pipe wait. */
static void
pwindows_wait(struct pstream *pstream)
{
    struct pwindows_pstream *p = pwindows_pstream_cast(pstream);
    poll_wevent_wait(p->connect.hEvent);
}

/* Passive named pipe. */
static int
pwindows_open(const char *name OVS_UNUSED, char *suffix,
              struct pstream **pstreamp, uint8_t dscp OVS_UNUSED)
{
    char *bind_path;
    int error;
    HANDLE npipe;
    char *orig_path;

    char *path;
    if (!strchr(suffix, ':')) {
        path = xasprintf("%s/%s", ovs_rundir(), suffix);
    } else {
        path = xstrdup(suffix);
    }

    /* Try to create a file under the path location. */
    FILE *file = fopen(path, "w");
    if (!file) {
        free(path);
        error = errno;
        VLOG_DBG_RL(&rl, "could not open %s (%s)", path, ovs_strerror(error));
        return error;
    } else {
        fclose(file);
    }

    orig_path = xstrdup(path);
    /* Strip slashes from path and create a named pipe using that newly created
     * string. */
    bind_path = xasprintf("%s%s", LOCAL_PREFIX, remove_slashes(path));
    free(path);

    npipe = create_pnpipe(bind_path);

    if (npipe == INVALID_HANDLE_VALUE) {
        VLOG_ERR_RL(&rl, "Could not create named pipe. Last error: %s",
                    ovs_lasterror_to_string());
        return ENOENT;
    }

    struct pwindows_pstream *p = xmalloc(sizeof *p);
    pstream_init(&p->pstream, &pwindows_pstream_class, name);
    p->fd = npipe;
    p->unlink_path = orig_path;
    memset(&p->connect, 0, sizeof(p->connect));
    p->connect.hEvent = CreateEvent(NULL, TRUE, TRUE, NULL);
    p->pending = false;
    p->pipe_path = bind_path;
    *pstreamp = &p->pstream;
    return 0;
}

const struct pstream_class pwindows_pstream_class = {
    "punix",
    false,                   /* probes */
    pwindows_open,           /* open */
    pwindows_close,          /* close */
    pwindows_accept,         /* accept */
    pwindows_wait,           /* wait */
};

/* Helper functions. */
static void
maybe_unlink_and_free(char *path)
{
    if (path) {
        fatal_signal_unlink_file_now(path);
        free(path);
    }
}
