/* $Id: j6p.c,v 1.1.1.1 2003/07/02 22:55:22 tgr Exp $ */

/*
 * Copyright (C) 2003 Thomas Graf <tgr@kaosu.ch>
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. Neither the name of the project nor the names of its contributors
 *    may be used to endorse or promote products derived from this software
 *    without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE PROJECT AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE PROJECT OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 */

#include <stdio.h>
#include <string.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <stdlib.h>
#include <netdb.h>
#include <netinet/icmp6.h>
#include <arpa/inet.h>
#include <getopt.h>
#include <signal.h>
#include <errno.h>

#define VERSION "1.0"

static char *usage_text =
"Usage: j6p [OPTION]...\n" \
"\n" \
"Options\n" \
"   -n <nick>       Nickname (default is your ip address)\n" \
"   -d <node>       Destination address\n" \
"   -m <msg>        message\n" \
"   -M              Use multicast messaging\n" \
"   -s              Just send the message, do not listen for messages\n" \
"   -h              Print this help text\n" \
"   -V              Print version string.\n";


static int sock = 0;
static struct icmp6_filter filter;
static struct addrinfo *dst = NULL, *mc = NULL;
static int quit = 0;
static char *nick = NULL;
static int use_multicast = 0;
static int only_send = 0;
static char *msg = NULL;

static struct addrinfo *
resolv_addr(const char *addr, const char *service)
{
    int r;
    struct addrinfo *result, hint = { 0, AF_INET6, SOCK_STREAM, 0, 0,
        NULL, NULL, NULL };

    if ((r = getaddrinfo(addr, service, &hint, &result) < 0))
        gai_strerror(r);

    return result;
}
    

static int
init_sock(void)
{
    int fd;
    int on = 1;

    if ((fd = socket(PF_INET6, SOCK_RAW, IPPROTO_ICMPV6)) < 0)
    {
        perror("socket");
        exit(-1);
    }

    ICMP6_FILTER_SETBLOCKALL(&filter);
	ICMP6_FILTER_SETPASS(ICMP6_ECHO_REPLY, &filter);

	if (setsockopt(fd, IPPROTO_ICMPV6, ICMP6_FILTER, &filter, sizeof(filter)) < 0)
    {
        perror("setsockopt ICMP6_FILTER");
        exit(-1);
    }

    if (setsockopt(fd, IPPROTO_IPV6, IPV6_DSTOPTS, &on, sizeof(on)) < 0)
    {
        perror("setsockopt IPV6_DSTOPTS");
        exit(-1);
    }

    return fd;
}

static struct cmsghdr *
make_exthdr(const char *msg, int *mclen)
{
    struct cmsghdr *chdr;
    int len, mlen = strlen(msg);

    len = inet6_option_space(mlen);

    if (!(chdr = (struct cmsghdr *) calloc(1, len)))
    {
        fprintf(stderr, "Can't allocate memory for cmsghdr");
        exit(-1);
    }

    if (inet6_option_init((void *) chdr, &chdr, IPV6_DSTOPTS) < 0)
    {
        perror("inet6_option_init");
        exit(-1);
    }

    {
        char buf[mlen + 2];

        buf[0] = 0x00 | 0x17; /* skip this option & type == 0x17 */
        buf[1] = mlen;

        memcpy(&buf[2], msg, strlen(msg));

        if (inet6_option_append(chdr, buf, 1, 0) < 0)
        {
            perror("inet6_option_append");
            exit(-1);
        }
    }

    *mclen = chdr->cmsg_len;

    return chdr;
}

static void
send_msg(const char *msg, struct sockaddr_in6 *addr)
{
    struct msghdr mhdr;
    struct iovec iov[1];
    struct icmp6_hdr *hdr = calloc(1, sizeof(struct icmp6_hdr) + 42);

    memset((void *) &mhdr, 0, sizeof(mhdr));
    hdr->icmp6_type = 129;
    hdr->icmp6_code = 0;

    mhdr.msg_name = (caddr_t) addr;
    mhdr.msg_namelen = sizeof(struct sockaddr_in6);

    mhdr.msg_iov = iov;
    mhdr.msg_iovlen = 1;

    iov[0].iov_base = hdr;
    iov[0].iov_len = sizeof(struct icmp6_hdr) + 42;

    mhdr.msg_control = (caddr_t) make_exthdr(msg, &mhdr.msg_controllen);

    if (sendmsg(sock, &mhdr, 0) < 0)
    {
        perror("sendmsg");
        exit(-1);
    }

    if (!use_multicast)
        printf("%s", msg);
}

static void
dump(void)
{
    struct msghdr mhdr = {0};
    struct cmsghdr *cmsg;
    char buf[CMSG_SPACE(1024)];
    char *text, *p;

    mhdr.msg_control = buf;
    mhdr.msg_controllen = sizeof(buf);

    if (recvmsg(sock, &mhdr, 0) < 0)
    {
        perror("recvmsg");
        exit(-1);
    }

    if (mhdr.msg_controllen > 0)
    {
        cmsg = CMSG_FIRSTHDR(&mhdr);
        text = CMSG_DATA(cmsg) + 4;

        for (p = text; *p != '\n'; p++);
        *p = '\0';

        printf("%s\n", text);
    }
}

static void
join_mcast_grp(struct sockaddr_in6 *addr)
{
    struct ipv6_mreq mreq6;

    memcpy(&mreq6.ipv6mr_multiaddr, &addr->sin6_addr, sizeof(struct in6_addr));
    mreq6.ipv6mr_interface = 0;

    if (setsockopt(sock, IPPROTO_IPV6, IPV6_ADD_MEMBERSHIP, &mreq6, sizeof(mreq6)) < 0)
    {
        perror("setsockopt IPV6_ADD_MEMBERSHIP");
        exit(-1);
    }
}

static void
read_input(void)
{
    unsigned char buf[1024];

    memset(buf, 0, sizeof(buf));
    if (fgets(buf, sizeof(buf), stdin) < 0)
    {
        perror("fgets");
        exit(-1);
    }
    
    {
        char out[strlen(buf) + strlen(nick) + 3];
        sprintf(out, "<%s> %s", nick, buf);
        if (mc)
            send_msg(out, (struct sockaddr_in6 *) mc->ai_addr);
        else
            send_msg(out, (struct sockaddr_in6 *) dst->ai_addr);
    }
}

static void
print_version(void)
{
    printf("j6p - joe 6 pack %s\n", VERSION);
    printf("Copyright (C) 2003 Thomas Graf <tgr@kaosu.ch>\n");
}


void
sig_int(int unused)
{
    quit = 1;
}

void
at_exit(void)
{
    if (mc && sock > 0)
    {
        struct ipv6_mreq mreq6;

        memcpy(&mreq6.ipv6mr_multiaddr,
            &((struct sockaddr_in6 *) mc->ai_addr)->sin6_addr,
            sizeof(struct in6_addr));
        
        mreq6.ipv6mr_interface = 0;

        if (setsockopt(sock, IPPROTO_IPV6, IPV6_LEAVE_GROUP,
            &mreq6, sizeof(mreq6)) < 0)
            perror("setsockopt(IPV6_LEAVE_GROUP)");
    }

    if (sock > 0)
        close(sock);

    if (dst)
        freeaddrinfo(dst);

    if (mc)
        freeaddrinfo(mc);
}

int
main(int argc, char *argv[])
{
    fd_set fdset;

    (void) atexit(&at_exit);

    (void) signal(SIGTERM, &sig_int);

    for (;;)
    {
        int c;

        if ((c = getopt(argc, argv, "n:d:sm:MhV")) == -1)
            break;

        switch (c)
        {
            case 'n':
                nick = strdup(optarg);
                break;

            case 'd':
                if (!(dst = resolv_addr(optarg, "58")))
                {
                    fprintf(stderr, "j6p: unknown host %s\n", argv[1]);
                    return -1;
                }
                break;

            case 's':
                only_send = 1;
                break;

            case 'm':
                msg = strdup(optarg);
                break;

            case 'M':
                use_multicast = 1;
                break;

            case 'V':
                print_version();
                exit(0);

            case 'h':
                print_version();
                printf("%s", usage_text);
                exit(0);
        }
    }

    if (!nick)
    {
        fprintf(stderr, "No nickname given.\n");
        exit(-1);
    }

    if (use_multicast)
        mc = resolv_addr("ff02::1", "58");

    if (!use_multicast && !dst)
    {
        fprintf(stderr, "No destination given.\n");
        exit(-1);
    }
    else
    {
        char s[INET6_ADDRSTRLEN];

        if (dst)
        {
            inet_ntop(AF_INET6, &((struct sockaddr_in6 *) dst->ai_addr)->sin6_addr,
                s, INET6_ADDRSTRLEN);
            printf("Destination Host: %s\n", s);
        }
        else if (mc)
        {
            inet_ntop(AF_INET6, &((struct sockaddr_in6 *) mc->ai_addr)->sin6_addr,
                s, INET6_ADDRSTRLEN);
            printf("Multicast Mode: %s\n", s);
        }
    }

    sock = init_sock();

    if (use_multicast)
        join_mcast_grp((struct sockaddr_in6 *) mc->ai_addr);

    if (msg)
    {
        char out[strlen(msg) + strlen(nick) + 4];
        sprintf(out, "<%s> %s\n", nick, msg);

        if (mc)
            send_msg(out, (struct sockaddr_in6 *) mc->ai_addr);
        else
            send_msg(out, (struct sockaddr_in6 *) dst->ai_addr);

        free(msg);
        msg = NULL;
    }

    while (!quit && !only_send)
    {
        int r;

        FD_ZERO(&fdset);

        FD_SET(sock, &fdset);
        FD_SET(0, &fdset);

        if (!(r = select(sock + 1, &fdset, NULL, NULL, NULL)))
            continue;

        if (r < 0)
        {
            if (errno != EINTR)
            {
                perror("select");
                exit(-1);
            }
            else
                quit = 1;
        }

        if (FD_ISSET(sock, &fdset))
            dump();
        if (FD_ISSET(0, &fdset))
            read_input();
    }

    exit(0);
}
