/*
   Copyright (C) 2009 Red Hat, Inc.

   This library is free software; you can redistribute it and/or
   modify it under the terms of the GNU Lesser General Public
   License as published by the Free Software Foundation; either
   version 2.1 of the License, or (at your option) any later version.

   This library 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
   Lesser General Public License for more details.

   You should have received a copy of the GNU Lesser General Public
   License along with this library; if not, see <http://www.gnu.org/licenses/>.
*/

#include "common.h"
#include "named_pipe.h"
#include "security.h"
#include "utils.h"
#include "debug.h"
#include <sddl.h>

#define PIPE_TIMEOUT 5000
#define PIPE_MAX_NAME_LEN 256
#define PIPE_PREFIX TEXT("\\\\.\\pipe\\")

enum SystemVersion {
    SYS_VER_UNSUPPORTED,
    SYS_VER_WIN_XP,
    SYS_VER_WIN_7,
};

static int get_system_version()
{
    OSVERSIONINFOEX osvi;

    ZeroMemory(&osvi, sizeof(OSVERSIONINFOEX));
    osvi.dwOSVersionInfoSize = sizeof(OSVERSIONINFOEX);
    if (!GetVersionEx((OSVERSIONINFO*)&osvi)) {
        DBG(0, "GetVersionEx() failed %u", GetLastError());
        return 0;
    }
    if (osvi.dwMajorVersion == 5 && osvi.dwMinorVersion == 1) {
        return SYS_VER_WIN_XP;
    } else if (osvi.dwMajorVersion == 6 && osvi.dwMinorVersion == 1 &&
                               osvi.wProductType == VER_NT_WORKSTATION) {
        return SYS_VER_WIN_7;
    }
    return 0;
}

static bool set_low_integrity(HANDLE handle)
{
    PSECURITY_DESCRIPTOR psd = NULL;
    PACL psacl = NULL;
    BOOL sacl_present = FALSE;
    BOOL sacl_defaulted = FALSE;

    if (!ConvertStringSecurityDescriptorToSecurityDescriptor(L"S:(ML;;NW;;;LW)",
                                                     SDDL_REVISION_1, &psd, NULL)) {
        DBG(0, "ConvertStringSecurityDescriptor() failed %u", GetLastError());
        return false;
    }
    if (!GetSecurityDescriptorSacl(psd, &sacl_present, &psacl, &sacl_defaulted)) {
        DBG(0, "GetSecurityDescriptorSacl() failed %u", GetLastError());
        return false;
    }
    DWORD err = SetSecurityInfo(handle, SE_KERNEL_OBJECT, LABEL_SECURITY_INFORMATION,
                                NULL, NULL, NULL, psacl);
    if (err != ERROR_SUCCESS) {
        DBG(0, "SetSecurityInfo() failed %u", err);
        return false;
    }
    return true;
}

PipeBuffer::PipeBuffer(HANDLE pipe, ProcessLoop& process_loop)
    : _handler (NULL)
    , _pipe (pipe)
    , _start (0)
    , _end (0)
    , _pending (false)
    , _process_loop(process_loop)
{
    ZeroMemory(&_overlap, sizeof(_overlap));
    _overlap.hEvent = this->get_handle();
    _process_loop.add_handle(*this);
}

PipeBuffer::~PipeBuffer()
{
    _process_loop.remove_handle(*this);
}

DWORD PipeBuffer::get_overlapped_bytes()
{
    DWORD bytes = 0;

    if (!GetOverlappedResult(_pipe, &_overlap, &bytes, FALSE) || bytes == 0) {
        _pending = false;
        _handler->on_data();
    }
    return bytes;
}

int32_t PipeReader::read(uint8_t *buf, int32_t size)
{
    ASSERT(buf && size >= 0);

    if (_start < _end) {
        int32_t bytes_read = 0;
        bytes_read = MIN(_end - _start, (uint32_t)size);
        CopyMemory(buf, _data + _start, bytes_read);
        _start += bytes_read;
        if (_start == _end) {
            _start = _end = 0;
        }
        return bytes_read;
    }
    if (_pending) {
        return 0;
    }
    if (!ReadFile(_pipe, _data + _end, sizeof(_data) - _end, NULL, &_overlap) &&
                                              GetLastError() != ERROR_IO_PENDING) {
        DBG(0, "ReadFile() failed %u", GetLastError());
        return -1;
    }
    _pending = true;
    return 0;
}

void PipeReader::on_event()
{
    ASSERT(_pending);
    DWORD bytes = get_overlapped_bytes();

    if (!bytes) {
        return;
    }
    _end += bytes;
    _pending = false;
    _handler->on_data();
}

int32_t PipeWriter::write(const uint8_t *buf, int32_t size)
{
    int32_t bytes_written = 0;
    ASSERT(buf && size >= 0);

    if (!_pending && _start == _end) {
        _start = _end = 0;
    }
    if (_end < sizeof(_data)) {
        bytes_written = MIN(sizeof(_data) - _end, (uint32_t)size);
        CopyMemory(_data + _end, buf, bytes_written);
        _end += bytes_written;
    }
    if (!_pending && _start < _end) {
        if (!WriteFile(_pipe, _data + _start, _end - _start, NULL, &_overlap) &&
                                              GetLastError() != ERROR_IO_PENDING) {
            DBG(0, "WriteFile() failed %u", GetLastError());
            return -1;
        }
        _pending = true;
    }
    return bytes_written;
}

void PipeWriter::on_event()
{
    ASSERT(_pending);
    DWORD bytes = get_overlapped_bytes();
    if (!bytes) {
        return;
    }
    _start += bytes;
    _pending = false;
    if (_start == sizeof(_data)) {
        _handler->on_data();
    }
}

WinConnection::WinConnection(HANDLE pipe, ProcessLoop& process_loop)
    : _pipe (pipe)
    , _writer (pipe, process_loop)
    , _reader (pipe, process_loop)
{
}

WinConnection::~WinConnection()
{
    if (!DisconnectNamedPipe(_pipe)) {
        DBG(0, "DisconnectNamedPipe failed %d", GetLastError());
    }
    CloseHandle(_pipe);
    //FIXME: notify WinListener about disconnetion, for creating new pipe
}

int32_t WinConnection::read(uint8_t *buf, int32_t size)
{
    return _reader.read(buf, size);
}

int32_t WinConnection::write(const uint8_t *buf, int32_t size)
{
    return _writer.write(buf, size);
}

void WinConnection::set_handler(NamedPipe::ConnectionInterface* handler)
{
    _reader.set_handler(handler);
    _writer.set_handler(handler);
}

WinListener::WinListener(const char *name, NamedPipe::ListenerInterface &listener_interface,
                         ProcessLoop& process_loop)
    : _listener_interface (listener_interface)
    , _pipe (0)
    , _process_loop (process_loop)
    , _allow_only_first_instance (true)
{
    _pipename = new TCHAR[PIPE_MAX_NAME_LEN];
    swprintf_s(_pipename, PIPE_MAX_NAME_LEN, L"%s%S", PIPE_PREFIX, name);
    ZeroMemory(&_overlap, sizeof(_overlap));
    _overlap.hEvent = this->get_handle();
    _process_loop.add_handle(*this);
    create_pipe();
}

WinListener::~WinListener()
{
    CancelIo(_pipe);
    _process_loop.remove_handle(*this);
    delete[] _pipename;
}

void WinListener::on_event()
{
    DWORD bytes;

    if (!GetOverlappedResult(_pipe, &_overlap, &bytes, FALSE)) {
        DBG(0, "GetOverlappedResult() failed %u", GetLastError());
        return;
    }
    DBG(0, "Pipe connected 0x%p", _pipe);
    WinConnection *con = new WinConnection(_pipe, _process_loop);
    NamedPipe::ConnectionInterface &con_interface = _listener_interface.create();
    con->set_handler(&con_interface);
    con_interface.bind((NamedPipe::ConnectionRef)con);
    if (!_allow_only_first_instance) {
        create_pipe();
    }
}

void WinListener::create_pipe()
{
    SECURITY_ATTRIBUTES sa;
    SECURITY_DESCRIPTOR sd;
    PACL dacl = NULL;

    // Allow access only to current user
    if (DWORD err = get_security_attributes(&sa, &sd, &dacl)) {
        THROW("Failed getting security attributes %u", err);
    } else {
        LOG_INFO("Created user-only security attribute");
    }
    LOG_INFO("Trying to create named pipe %S", _pipename);
    _pipe = CreateNamedPipe(_pipename,
                            PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED |
                            (_allow_only_first_instance ? FILE_FLAG_FIRST_PIPE_INSTANCE : 0),
                            PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT,
                            (_allow_only_first_instance ? 1 : PIPE_UNLIMITED_INSTANCES),
                            PIPE_BUF_SIZE, PIPE_BUF_SIZE, PIPE_TIMEOUT, &sa);
    LocalFree(dacl);
    if (_pipe == INVALID_HANDLE_VALUE) {
        THROW("CreateNamedPipe() failed %u", GetLastError());
    }
    // On Win7 only, set named pipe to low integrity to allow a connection
    // from ActiveX on untrusted website 
    if (get_system_version() == SYS_VER_WIN_7) {
        LOG_INFO("Set named pipe to low integrity");
        if (!set_low_integrity(_pipe)) {
            THROW("Failed setting named pipe to low integrity");
        }
    }
    if (ConnectNamedPipe(_pipe, &_overlap)) {
        THROW("ConnectNamedPipe() is not pending");
    }
    switch (GetLastError()) {
    case ERROR_IO_PENDING:
        DBG(0, "Pipe waits for connection");
        break;
    case ERROR_PIPE_CONNECTED: {
        DBG(0, "Pipe already connected");
        WinConnection *con = new WinConnection(_pipe, _process_loop);
        NamedPipe::ConnectionInterface &con_interface = _listener_interface.create();
        con->set_handler(&con_interface);
        con_interface.bind((NamedPipe::ConnectionRef)con);
        if (!_allow_only_first_instance) {
            create_pipe();
        }
        break;
    }
    default:
        THROW("ConnectNamedPipe() failed %u", GetLastError());
    }
}

