/* -*-  mode:c; tab-width:8; c-basic-offset:8; indent-tabs-mode:nil;  -*- */
/* 
   Copyright (C) by Ronnie Sahlberg <ronniesahlberg@gmail.com> 2015
   
   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; either version 3 of the License, or
   (at your option) any later version.
   
   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, see <http://www.gnu.org/licenses/>.
*/

/* This is a very incomplete portmapper that only implements
 * a subset of version 2 and threee of the protocol.
 * A proper portmapper needs to implement these two versions fully
 * as well as version 4.
 *
 * See this as an example of how to build a simple RPC service
 * that supports both UDP and TCP using libnfs.
 */

#define _FILE_OFFSET_BITS 64
#define _GNU_SOURCE

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#ifdef AROS
#include "aros_compat.h"
#endif


#ifdef WIN32
#include <win32/win32_compat.h>
#pragma comment(lib, "ws2_32.lib")
WSADATA wsaData;
#else
#include <sys/stat.h>
#include <string.h>
#endif
 
#ifdef HAVE_POLL_H
#include <poll.h>
#endif

#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif

#include <arpa/inet.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <time.h>
#include "libnfs.h"
#include "libnfs-raw.h"
#include "libnfs-raw-portmap.h"

#include <event2/event.h>

struct event_base *base;

struct server {
        struct rpc_context *rpc;
        struct event *read_event;
        struct event *write_event;
};

/* Socket where we listen for incomming rpc connections */
struct event *listen_event;
int listen_socket = -1;

/* Socket used for UDP server */
struct server udp_server;
int udp_socket = -1;

struct mapping {
        struct mapping *next;
        u_int prog;
        u_int vers;
        int port;
        char *netid;
        char *addr;
        char *owner;
};
struct mapping *map;


void free_map_item(struct mapping *item)
{
        free(item->netid);
        free(item->addr);
        free(item->owner);
        free(item);
}

static void free_server(struct server *server)
{
        if (server->rpc) {
                rpc_disconnect(server->rpc, NULL);
                rpc_destroy_context(server->rpc);
        }
        if (server->read_event) {
                event_free(server->read_event);
        }
        if (server->write_event) {
                event_free(server->write_event);
        }

        free(server);
}

/*
 * Based on the state of libnfs and its context, update libevent
 * accordingly regarding which events we are interested in.
 */
static void update_events(struct rpc_context *rpc, struct event *read_event,
                          struct event *write_event)
{
        int events = rpc_which_events(rpc);

        if (read_event) {
                if (events & POLLIN) {
                        event_add(read_event, NULL);
                } else {
                        event_del(read_event);
                }
        }
        if (write_event) {
                if (events & POLLOUT) {
                        event_add(write_event, NULL);
                } else {
                        event_del(write_event);
                }
        }
}

/*
 * Add a registration for program,version,netid.
 */
int pmap_register(int prog, int vers, char *netid, char *addr,
                  char *owner)
{
        struct mapping *item;
        char *str;
        int count = 0;

        item = malloc(sizeof(struct mapping));
        item->prog  = prog;
        item->vers  = vers;
        item->netid = netid;
        item->addr  = addr;
        item->owner = owner;

        /* The port are the last two dotted decimal fields in the address */
        for (str = item->addr + strlen(item->addr) - 1; str >= item->addr; str--) {
                if (*str != '.') {
                        if (*str < '0' || *str > '9') {
                                break;
                        }
                        continue;
                }

                count++;
                if (count == 2) {
                        int high, low;

                        sscanf(str, ".%d.%d", &high, &low);
                        item->port = high * 256 + low;
                        break;
                }
        }

        item->next  = map;
        map = item;
}

/*
 * Find and return a registration matching program,version,netid.
 */
struct mapping *map_lookup(int prog, int vers, char *netid)
{
        struct mapping *tmp;

        for (tmp = map; tmp; tmp = tmp->next) {
                if (tmp->prog != prog) {
                        continue;
                }
                if (tmp->vers != vers) {
                        continue;
                }
                if (strcmp(tmp->netid, netid)) {
                        continue;
                }

                return tmp;
        }

        return NULL;
}

/*
 * Remove a registration from our map or registrations.
 */
void map_remove(int prog, int vers, char *netid)
{
        struct mapping *prev = NULL;
        struct mapping *tmp;

        for (tmp = map; tmp; prev = tmp, tmp = tmp->next) {
                if (tmp->prog != prog) {
                        continue;
                }
                if (tmp->vers != vers) {
                        continue;
                }
                if (strcmp(tmp->netid, netid)) {
                        continue;
                }
                break;
        }
        if (tmp == NULL) {
                return;
        }
        if (prev) {
                prev->next = tmp->next;
        } else {
                map = tmp->next;
        }

        free_map_item(tmp);
        return;
}

/*
 * The NULL procedure. All protocols/versions must provide a NULL procedure
 * as index 0.
 * It is used by clients, and rpcinfo, to "ping" a service and verify that
 * the service is available and that it does support the indicated version.
 */
static int pmap2_null_proc(struct rpc_context *rpc, struct rpc_msg *call, void *opaque)
{
        rpc_send_reply(rpc, call, NULL, (zdrproc_t)zdr_void, 0);

        return 0;
}

/*
 * v2 GETPORT.
 * This is the lookup function for portmapper version 2.
 * A client provides program, version and protocol (tcp or udp)
 * and portmapper returns which port that service is available on,
 * (or 0 if no such program is registered.)
 */
static int pmap2_getport_proc(struct rpc_context *rpc, struct rpc_msg *call, void *opaque)
{
        PMAP2GETPORTargs *args = call->body.cbody.args;
        struct mapping *tmp;
        char *netid;
        uint32_t port = 0;

        if (args->prot == IPPROTO_TCP) {
                netid = "tcp";
        } else {
                netid = "udp";
        }

        tmp = map_lookup(args->prog, args->vers, netid);
        if (tmp) {
                port = tmp->port;
        }

        rpc_send_reply(rpc, call, &port, (zdrproc_t)zdr_uint32_t, sizeof(uint32_t));

        return 0;
}

/*
 * v2 DUMP.
 * This RPC returns a list of all endpoints that are registered with
 * portmapper.
 */
static int pmap2_dump_proc(struct rpc_context *rpc, struct rpc_msg *call, void *opaque)
{
        PMAP2DUMPres reply;
        struct mapping *tmp;

        reply.list = NULL;
        for (tmp = map; tmp; tmp = tmp->next) {
                struct pmap2_mapping_list *tmp_list;
                int proto;

                /* pmap2 only support ipv4 */
                if (!strcmp(tmp->netid, "tcp")) {
                        proto = IPPROTO_TCP;
                } else if (!strcmp(tmp->netid, "udp")) {
                        proto = IPPROTO_UDP;
                } else {
                        continue;
                }
                      
                tmp_list = malloc(sizeof(struct pmap2_mapping_list));
                tmp_list->map.prog  = tmp->prog;
                tmp_list->map.vers  = tmp->vers;
                tmp_list->map.prot  = proto;
                tmp_list->map.port  = tmp->port;
                
                tmp_list->next = reply.list;

                reply.list = tmp_list;
        }

        rpc_send_reply(rpc, call, &reply,
                       (zdrproc_t)zdr_PMAP2DUMPres, sizeof(PMAP2DUMPres));

        while (reply.list) {
                struct pmap2_mapping_list *tmp_list = reply.list->next;
                free(reply.list);
                reply.list = tmp_list;
        }
        
        return 0;
}

/*
 * v2 SET
 * This procedure is used to register and endpoint with portmapper.
 */
static int pmap2_set_proc(struct rpc_context *rpc, struct rpc_msg *call, void *opaque)
{
        PMAP2GETPORTargs *args = call->body.cbody.args;
        char *prot;
        char *addr;
        uint32_t response = 1;

        if (args->prot == IPPROTO_TCP) {
                prot = "tcp";
        } else {
                prot = "udp";
        }

        /* Don't update if we already have a mapping */
        if (map_lookup(args->prog, args->vers, prot)) {
                response = 0;
                rpc_send_reply(rpc, call, &response, (zdrproc_t)zdr_uint32_t, sizeof(uint32_t));
                return 0;
        }
        
        asprintf(&addr, "0.0.0.0.%d.%d", args->port >> 8, args->port & 0xff);
        pmap_register(args->prog, args->vers, strdup(prot), addr,
                      strdup("<unknown>"));

        rpc_send_reply(rpc, call, &response, (zdrproc_t)zdr_uint32_t, sizeof(uint32_t));
        return 0;
}

/*
 * v2 UNSET
 * This procedure is used to remove a registration from portmappers
 * list of endpoints.
 */
static int pmap2_unset_proc(struct rpc_context *rpc, struct rpc_msg *call, void *opaque)
{
        PMAP2GETPORTargs *args = call->body.cbody.args;
        char *prot;
        char *addr;
        uint32_t response = 1;

        if (args->prot == IPPROTO_TCP) {
                prot = "tcp";
        } else {
                prot = "udp";
        }

        map_remove(args->prog, args->vers, prot);

        rpc_send_reply(rpc, call, &response, (zdrproc_t)zdr_uint32_t, sizeof(uint32_t));
        return 0;
}

/*
 * Service table for portmapper v2.
 *
 * Service management is table driven in libnfsand this is the table
 * that defines which procedures we implement for portmapper v2.
 * If clients try to connect to the not-yet-implemented procedures here
 * libnfs will automatically respond with an RPC layer error that flags
 * PROCEDURE UNAVAILABLE.
 *
 * This table contains the procedure number, the callback function to implement
 * this procedure, the unmarshalling function that libnfs should use to unppack
 * the client payload as well as its size.
 *
 * Version 2 does not support ipv6 so this version of portmapper is
 * not too commonly used any more.
 */
struct service_proc pmap2_pt[] = {
        {PMAP2_NULL, pmap2_null_proc,
            (zdrproc_t)zdr_void, 0},
        {PMAP2_SET, pmap2_set_proc,
            (zdrproc_t)zdr_PMAP2SETargs, sizeof(PMAP2SETargs)},
        {PMAP2_UNSET, pmap2_unset_proc,
            (zdrproc_t)zdr_PMAP2UNSETargs, sizeof(PMAP2UNSETargs)},
        {PMAP2_GETPORT, pmap2_getport_proc,
            (zdrproc_t)zdr_PMAP2GETPORTargs, sizeof(PMAP2GETPORTargs)},
        {PMAP2_DUMP, pmap2_dump_proc,
            (zdrproc_t)zdr_void, 0},
        //{PMAP2_CALLIT, pmap2_...},
};

/*
 * The NULL procedure. All protocols/versions must provide a NULL procedure
 * as index 0.
 * It is used by clients, and rpcinfo, to "ping" a service and verify that
 * the service is available and that it does support the indicated version.
 */
static int pmap3_null_proc(struct rpc_context *rpc, struct rpc_msg *call, void *opaque)
{
        rpc_send_reply(rpc, call, NULL, (zdrproc_t)zdr_void, 0);

        return 0;
}

/*
 * v3 DUMP.
 * This RPC returns a list of all endpoints that are registered with
 * portmapper.
 */
static int pmap3_dump_proc(struct rpc_context *rpc, struct rpc_msg *call, void *opaque)
{
        PMAP3DUMPres reply;
        struct mapping *tmp;

        reply.list = NULL;
        for (tmp = map; tmp; tmp = tmp->next) {
                struct pmap3_mapping_list *tmp_list;
                
                tmp_list = malloc(sizeof(struct pmap3_mapping_list));
                tmp_list->map.prog  = tmp->prog;
                tmp_list->map.vers  = tmp->vers;
                tmp_list->map.netid = tmp->netid;
                tmp_list->map.addr  = tmp->addr;
                tmp_list->map.owner = tmp->owner;
                
                tmp_list->next = reply.list;

                reply.list = tmp_list;
        }

        rpc_send_reply(rpc, call, &reply,
                       (zdrproc_t)zdr_PMAP3DUMPres, sizeof(PMAP3DUMPres));

        while (reply.list) {
                struct pmap3_mapping_list *tmp_list = reply.list->next;
                free(reply.list);
                reply.list = tmp_list;
        }
        
        return 0;
}


/*
 * Service table for portmapper v3.
 *
 * Service management is table driven in libnfsand this is the table
 * that defines which procedures we implement for portmapper v3.
 * If clients try to connect to the not-yet-implemented procedures here
 * libnfs will automatically respond with an RPC layer error that flags
 * PROCEDURE UNAVAILABLE.
 *
 * This table contains the procedure number, the callback function to implement
 * this procedure, the unmarshalling function that libnfs should use to unppack
 * the client payload as well as its size.
 */
struct service_proc pmap3_pt[] = {
        {PMAP3_NULL, pmap3_null_proc,
            (zdrproc_t)zdr_void, 0},
        //{PMAP3_SET, pmap3_...},
        //{PMAP3_UNSET, pmap3_...},
        //{PMAP3_GETADDR, pmap3_...},
        {PMAP3_DUMP, pmap3_dump_proc,
            (zdrproc_t)zdr_void, 0},
        //{PMAP3_CALLIT, pmap3_...},
        //{PMAP3_GETTIME, pmap3_...},
        //{PMAP3_UADDR2TADDR, pmap3_...},
        //{PMAP3_TADDR2UADDR, pmap3_...},
};

/*
 * This callback is invoked from the event system when an event we are waiting
 * for has become active.
 */
static void server_io(evutil_socket_t fd, short events, void *private_data)
{
        struct server *server = private_data;
        int revents = 0;

        /*
         * Translate the libevent read/write flags to the corresponding
         * flags that libnfs uses.
         */
        if (events & EV_READ) {
                revents |= POLLIN;
        }
        if (events & EV_WRITE) {
                revents |= POLLOUT;
        }

        /*
         * Let libnfs process the event.
         */
        if (rpc_service(server->rpc, revents) < 0) {
                free_server(server);
                return;
        }

        /*
         * Update which events we are interested in. It might have changed
         * for example if we no longer have any data pending to send
         * we no longer need to wait for the socket to become writeable.
         */
        update_events(server->rpc, server->read_event, server->write_event);
}


/*
 * This callback is invoked when we have a client connecting to our TCP
 * port.
 */
static void do_accept(evutil_socket_t s, short events, void *private_data)
{
        struct sockaddr_storage ss;
        socklen_t len = sizeof(ss);
        struct server *server;
        int fd;
        
        server = malloc(sizeof(struct server));
        if (server == NULL) {
                return;
        }
        memset(server, 0, sizeof(*server));

        if ((fd = accept(s, (struct sockaddr *)&ss, &len)) < 0) {
                free_server(server);
                return;
        }
        evutil_make_socket_nonblocking(fd);

        server->rpc = rpc_init_server_context(fd);
        if (server->rpc == NULL) {
                close(fd);
                free_server(server);
                return;
        }

        /*
         * Register both v2 and v3 of the protocol to the new
         * server context.
         */
        rpc_register_service(server->rpc, PMAP_PROGRAM, PMAP_V2,
                             pmap2_pt, sizeof(pmap2_pt) / sizeof(pmap2_pt[0]));
        rpc_register_service(server->rpc, PMAP_PROGRAM, PMAP_V3,
                             pmap3_pt, sizeof(pmap3_pt) / sizeof(pmap3_pt[0]));

        /*
         * Create events for read and write for this new server instance.
         */
        server->read_event = event_new(base, fd, EV_READ|EV_PERSIST,
                                       server_io, server);
        server->write_event = event_new(base, fd, EV_WRITE|EV_PERSIST,
                                        server_io, server);
        update_events(server->rpc, server->read_event, server->write_event);
}

int main(int argc, char *argv[])
{
        struct sockaddr_in in;
        int one = 1;

#ifdef WIN32
        if (WSAStartup(MAKEWORD(2,2), &wsaData) != 0) {
                printf("Failed to start Winsock2\n");
                return 10;
        }
#endif

#ifdef AROS
        aros_init_socket();
#endif

        base = event_base_new();
        if (base == NULL) {
                printf("Failed create event context\n");
                exit(10);
        }

        
        /*
         * Portmapper listens on port 111, any address.
         * Just initialize it for now as we will need it several times below.
         */
        in.sin_family = AF_INET;
        in.sin_port = htons(111);
        in.sin_addr.s_addr = htonl (INADDR_ANY);


        /* This is the portmapper protocol itself which we obviously
         * support.
         */
        pmap_register(100000, 2, strdup("tcp"), strdup("0.0.0.0.0.111"),
                      strdup("portmapper-service"));
        pmap_register(100000, 2, strdup("udp"), strdup("0.0.0.0.0.111"),
                      strdup("portmapper-service"));
        pmap_register(100000, 3, strdup("tcp"), strdup("0.0.0.0.0.111"),
                      strdup("portmapper-service"));
        pmap_register(100000, 3, strdup("udp"), strdup("0.0.0.0.0.111"),
                      strdup("portmapper-service"));

        
        /*
         * TCP: Set up a listening socket for incoming TCP connections.
         * Once clients connect, inside do_accept() we will create a proper
         * libnfs server context for each connection.
         */
        listen_socket = socket(AF_INET, SOCK_STREAM, 0);
        if (listen_socket == -1) {
                printf("Failed to create listening socket\n");
                exit(10);
        }
        evutil_make_socket_nonblocking(listen_socket);
        setsockopt(listen_socket, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));

        if (bind(listen_socket, (struct sockaddr *)&in, sizeof(in)) < 0) {
                printf("Failed to bind listening socket\n");
                exit(10);
        }
        if (listen(listen_socket, 16) < 0) {
                printf("failed to listen to socket\n");
                exit(10);
        }
        listen_event = event_new(base,
                                 listen_socket,
                                 EV_READ|EV_PERSIST,
                                 do_accept, NULL);
        event_add(listen_event, NULL);


        /*
         * UDP: Create and bind to the socket we want to use for the UDP server.
         */
        udp_socket = socket(AF_INET, SOCK_DGRAM, 0);
        if (udp_socket == -1) {
                printf("Failed to create udp socket\n");
                exit(10);
        }
        evutil_make_socket_nonblocking(udp_socket);
        setsockopt(udp_socket, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));

        if (bind(udp_socket, (struct sockaddr *)&in, sizeof(in)) < 0) {
                printf("Failed to bind udp socket\n");
                exit(10);
        }

        /*
         * UDP: Create a libnfs server context for this socket.
         */
        memset(&udp_server, 0, sizeof(udp_server));
        udp_server.rpc = rpc_init_server_context(udp_socket);
        
        /*
         * UDP: Register both v2 and v3 of the protocol to the
         * UDP server context.
         */
        rpc_register_service(udp_server.rpc, PMAP_PROGRAM, PMAP_V2,
                             pmap2_pt, sizeof(pmap2_pt) / sizeof(pmap2_pt[0]));
        rpc_register_service(udp_server.rpc, PMAP_PROGRAM, PMAP_V3,
                             pmap3_pt, sizeof(pmap3_pt) / sizeof(pmap3_pt[0]));

        udp_server.read_event = event_new(base,
                                          udp_socket,
                                          EV_READ|EV_PERSIST,
                                          server_io, &udp_server);
        event_add(udp_server.read_event, NULL);

        
        /*
         * Everything is now set up. Start the event loop.
         */
        event_base_dispatch(base);
        
        return 0;
}
