/*
 * socklib.c
 *
 * Handy functions for IPv4 socket connections.
 *
 * Author: Dario Rodriguez dario@softhome.net
 * This file is licensed under the terms of the GNU LGPL v2+
 */

#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <netdb.h>
#include <errno.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/select.h>
#include <sys/ioctl.h>

#include "socklib.h"

typedef struct _sselect {
        fd_set readset;
        fd_set writeset;
        int maxfd;
        int numfd;
        fd_set readresult;
        fd_set writeresult;
        int sizeresult;
        int readresultreturned;
        int writeresultreturned;
        int sizeuserptr;
        void **userptr;
} _sselect;


#ifndef FILLTV
#define FILLTV(tv,sec,usec) (tv).tv_sec=(sec),(tv).tv_usec=(usec)
#endif

#ifndef FILLIPV4ADDR
#define FILLIPV4ADDR(a,family,addr,port) \
                memset(&(a),0,sizeof(struct sockaddr_in)),\
                s.sin_family=family,\
                s.sin_addr.s_addr=addr,\
                s.sin_port=htons(port)
#endif

#define USERPTRBLOCKSIZE 1024

static int sselect_adduserptr(_sselect *ssel, int fd, void *userptr);
static int sselect_clearuserptr(_sselect *ssel, int fd);

char *
ipv4_genip(char *hostname, long *resulthostsize)
{
        struct addrinfo hints,*ai;
        struct sockaddr_in *in;
        char *host;
        memset(&hints,0,sizeof(hints));
        hints.ai_family=AF_INET;
        if(getaddrinfo(hostname,NULL,&hints,&ai)!=0)
                return(NULL);
        in=(struct sockaddr_in *)ai->ai_addr;
        if(in->sin_family!=AF_INET) {
                freeaddrinfo(ai),ai=NULL;
                return(NULL);
        }
        *resulthostsize=4;
        if(in==NULL || (host=malloc(*resulthostsize))==NULL) {
                freeaddrinfo(ai),ai=NULL;
                return(NULL);
        }
        memcpy(host,&(in->sin_addr),*resulthostsize);
        freeaddrinfo(ai),ai=NULL;
        return(host);
}

int
ipv4_genport(char *portname, int fallback)
{
        struct addrinfo hints,*ai;
        struct sockaddr_in *in;
        int port;
        if(*portname>='0' && *portname<='9')
                return(atoi(portname));
        memset(&hints,0,sizeof(hints));
        hints.ai_family=AF_INET;
        if(getaddrinfo(NULL,portname,&hints,&ai)!=0)
                return(fallback);
        if((in=((struct sockaddr_in *)ai->ai_addr))==NULL) {
                freeaddrinfo(ai),ai=NULL;
                return(fallback);
        }
	port=htons(in->sin_port);
        freeaddrinfo(ai),ai=NULL;
        return(port);
}

int
ipv4_preconnect(char *host, long hostsize, int port) /* setup socket, set non-blocking, connect(2) call */
{
        struct sockaddr_in c,s;
        int fd;
        int res,err;
        FILLIPV4ADDR(c,AF_INET,htonl(INADDR_ANY),0);
        FILLIPV4ADDR(c,AF_INET,htonl(INADDR_ANY),port);
        memcpy(&(s.sin_addr.s_addr),host,hostsize);
        if((fd=socket(AF_INET,SOCK_STREAM,0 /* any protocol */))==-1)
                return(-1);
        if(bind(fd,(struct sockaddr *)&c,sizeof(c))!=0) {
                close(fd),fd=-1;
                return(-1);
        }
        sock_setblocking(fd,0);
        res=connect(fd,(struct sockaddr *)&s,sizeof(s));
        err=errno;
        if(res==-1 && err!=EINPROGRESS) {
                close(fd),fd=-1;
                return(-1);
        }
        return(fd);
}

int
ipv4_connect(int fd, long timeoutmsec) /* tests writeability */
{
        struct timeval tv;
        fd_set wset;
        FILLTV(tv,timeoutmsec/1000L,(timeoutmsec%(1000L))*1000L);
        FD_ZERO(&wset);
        FD_SET(fd,&wset);
        if(select(fd+1,NULL,&wset,NULL,&tv)>0)
                return(0);
        return(-1);
}

int
ipv4_postconnect(int fd) /* hopefully connect(2) suceeded, set blocking */
{
        sock_setblocking(fd,1);
        return(0);
}

int
ipv4_server(int port)
{
        return(ipv4_serverbinded(NULL,0,port));
}

int
ipv4_serverbinded(char *host, long hostsize, int port)
{
        struct sockaddr_in s;
        int fd;
        if((fd=socket(AF_INET,SOCK_STREAM,0 /* any protocol */))==-1)
                return(-1);
        FILLIPV4ADDR(s,AF_INET,htonl(INADDR_ANY),port);
        if(host!=NULL)
                memcpy(&(s.sin_addr.s_addr),host,hostsize);
        if(bind(fd,(struct sockaddr *)&s,sizeof(s))!=0) {
                close(fd),fd=-1;
                return(-1);
        }
        if(listen(fd,4)==-1) {
                close(fd),fd=-1;
                return(-1);
        }
        return(fd);
}

int
sock_accept(int fd)
{
        int newfd;
        struct sockaddr_in s;
        socklen_t slen;
        slen=sizeof(s);
        if(fd==-1 || (newfd=accept(fd,(struct sockaddr *)&s,&slen))==-1)
                return(-1);
        return(newfd);
}

int
sock_getinfo(int fd, int *iplen, char *ip, int *port) /* ip must be at least 16 bytes to have room for an ipv6 address */
{
        struct sockaddr c;
        struct sockaddr_in *c4;
        struct sockaddr_in6 *c6;
        socklen_t clen;
        if(fd==-1)
                return(-1);
        clen=sizeof(c);
        if(getsockname(fd,(struct sockaddr *)&c,&clen)==-1)
                return(-1);
        if(c.sa_family!=AF_INET && c.sa_family!=AF_INET6)
                return(-1);
        if(c.sa_family==AF_INET) {
                c4=(struct sockaddr_in *) &c;
                *iplen=sizeof(c4->sin_addr.s_addr);
                memcpy(ip,&(c4->sin_addr.s_addr),*iplen);
                *port=ntohs(c4->sin_port);
        } else {
                c6=(struct sockaddr_in6 *) &c;
                *iplen=sizeof(c6->sin6_addr);
                memcpy(ip,&(c6->sin6_addr),*iplen);
                *port=ntohs(c6->sin6_port);
        }
        return(0);
}

int
sock_queued(int fd)
{
        int n;
        if(ioctl(fd,FIONREAD,&n)!=0)
                return(-1);
        return(n);
}

int
sock_setblocking(int fd, int block)
{
        int fl;
        if((fl=fcntl(fd,F_GETFL,0))==-1)
                return(-1);
        fl=(block)?(fl&(~O_NONBLOCK)):(fl|O_NONBLOCK);
        return(fcntl(fd,F_SETFL,fl));
}

int
sock_readable(int fd) /* tests readability */
{
        struct timeval tv;
        fd_set rset;
        FILLTV(tv,0,0);
        FD_ZERO(&rset);
        FD_SET(fd,&rset);
        if(select(fd+1,&rset,NULL,NULL,&tv)>0)
                return(0);
        return(-1);
}

sselect *
sselect_init(void)
{
        _sselect *ssel;
        if((ssel=malloc(sizeof(_sselect)))==NULL)
                return(NULL);
        memset(ssel,0,sizeof(_sselect));
        sselect_reset((sselect *)ssel);
        return((sselect *)ssel);
}

void
sselect_free(sselect *paramssel)
{
        _sselect *ssel=(_sselect *)paramssel;
        if(ssel==NULL)
                return;
        if(ssel->userptr!=NULL) {
                ssel->sizeuserptr=0;
                free(ssel->userptr),ssel->userptr=NULL;
        }
        free(ssel),ssel=NULL;
}


int
sselect_reset(sselect *paramssel)
{
        _sselect *ssel=(_sselect *)paramssel;
        if(ssel==NULL)
                return(-1);
        FD_ZERO(&(ssel->readset));
        FD_ZERO(&(ssel->writeset));
        ssel->maxfd=0;
        ssel->numfd=0;
        FD_ZERO(&(ssel->readresult));
        FD_ZERO(&(ssel->writeresult));
        memset(ssel->userptr,0,sizeof(void *)*ssel->sizeuserptr);
        return(0);
}

int
sselect_addread(sselect *paramssel, int fd, void *userptr)
{
        _sselect *ssel=(_sselect *)paramssel;
        if(ssel==NULL || fd<0 || FD_ISSET(fd,&(ssel->readset)))
                return(-1);
        if(userptr!=NULL) {
                if(sselect_adduserptr(ssel, fd, userptr)!=0)
                        return(-1);
        } else
                sselect_clearuserptr(ssel,fd);
        FD_SET(fd,&(ssel->readset));
        if(fd>ssel->maxfd)
                ssel->maxfd=fd;
        ssel->numfd++;
        return(0);
}


int
sselect_addwrite(sselect *paramssel, int fd, void *userptr)
{
        _sselect *ssel=(_sselect *)paramssel;
        if(ssel==NULL || fd<0 || FD_ISSET(fd,&(ssel->writeset)))
                return(-1);
        if(userptr!=NULL) {
                if(sselect_adduserptr(ssel, fd, userptr)!=0)
                        return(-1);
        } else
                sselect_clearuserptr(ssel,fd);
        FD_SET(fd,&(ssel->writeset));
        if(fd>ssel->maxfd)
                ssel->maxfd=fd;
        ssel->numfd++;
        return(0);
}

int
sselect_delread(sselect *paramssel, int fd)
{
        _sselect *ssel=(_sselect *)paramssel;
        if(ssel==NULL || fd<0 || !FD_ISSET(fd,&(ssel->readset)))
                return(-1);
        FD_CLR(fd,&(ssel->readset));
        if(fd>0 && fd==ssel->maxfd && !FD_ISSET(fd,&(ssel->writeset)))
                ssel->maxfd=fd-1;
        ssel->numfd--;
        return(0);
}


int
sselect_delwrite(sselect *paramssel, int fd)
{
        _sselect *ssel=(_sselect *)paramssel;
        if(ssel==NULL || fd<0 || !FD_ISSET(fd,&(ssel->writeset)))
                return(-1);
        FD_CLR(fd,&(ssel->writeset));
        if(fd>0 && fd==ssel->maxfd && !FD_ISSET(fd,&(ssel->readset)))
                ssel->maxfd=fd-1;
        ssel->numfd--;
        return(0);
}

int
sselect_wait(sselect *paramssel, int ms)
{
        struct timeval tv;
        _sselect *ssel=(_sselect *)paramssel;
        if(ssel==NULL || ms<0)
                return(-1);
        memcpy(&(ssel->readresult),&(ssel->readset),sizeof(fd_set));
        memcpy(&(ssel->writeresult),&(ssel->writeset),sizeof(fd_set));
        ssel->readresultreturned=0;
        ssel->writeresultreturned=0;
        FILLTV(tv,(ms/1000),(ms%1000)*1000);
        if((ssel->sizeresult=select(
          ssel->maxfd+1,&(ssel->readresult),
          &(ssel->writeresult),NULL,&tv))<0)
                return(-1);
        return(ssel->sizeresult);
}

int
sselect_getread(sselect *paramssel, int *fds, int sizefds)
{
        int i,n;
        _sselect *ssel=(_sselect *)paramssel;
        if(ssel==NULL || fds==NULL || sizefds<1)
                return(-1);
        for(n=0,i=ssel->readresultreturned;i<=ssel->maxfd && n<sizefds;i++) {
                if(FD_ISSET(i,&(ssel->readresult))) {
                        fds[n++]=i;
                        FD_CLR(i,&(ssel->readresult));
                }
        }
        ssel->readresultreturned=i;
        return(n);
}

int
sselect_getwrite(sselect *paramssel, int *fds, int sizefds)
{
        int i,n;
        _sselect *ssel=(_sselect *)paramssel;
        if(ssel==NULL || fds==NULL || sizefds<1)
                return(-1);
        for(n=0,i=ssel->writeresultreturned;i<=ssel->maxfd && n<sizefds;i++) {
                if(FD_ISSET(i,&(ssel->writeresult))) {
                        fds[n++]=i;
                        FD_CLR(i,&(ssel->writeresult));
                }
        }
        ssel->writeresultreturned=i;
        return(n);
}

/* advanced sselect functions */
int
sselect_getreadfiltered(sselect *paramssel, fd_set *filter, int *fds, int sizefds)
{
        int i,n;
        _sselect *ssel=(_sselect *)paramssel;
        if(ssel==NULL || fds==NULL || sizefds<1)
                return(-1);
        for(n=0,i=0;i<=ssel->maxfd && n<sizefds;i++) {
                if(FD_ISSET(i,&(ssel->readresult)) && FD_ISSET(i,filter)) {
                        fds[n++]=i;
                        FD_CLR(i,&(ssel->readresult));
                }
        }
        return(n);
}

int
sselect_getwritefiltered(sselect *paramssel, fd_set *filter, int *fds, int sizefds)
{
        int i,n;
        _sselect *ssel=(_sselect *)paramssel;
        if(ssel==NULL || fds==NULL || sizefds<1)
                return(-1);
        for(n=0,i=0;i<=ssel->maxfd && n<sizefds;i++) {
                if(FD_ISSET(i,&(ssel->writeresult)) && FD_ISSET(i,filter)) {
                        fds[n++]=i;
                        FD_CLR(i,&(ssel->writeresult));
                }
        }
        return(n);
}

void *
sselect_getuserptr(sselect *paramssel, int fd)
{
        _sselect *ssel=(_sselect *)paramssel;
        if(ssel==NULL || fd<0 || fd>=ssel->sizeuserptr)
                return(NULL);
        return(ssel->userptr[fd]);
}


/* aux functions */
void
sock_setfast(int fd)
{
        int val;
        val=1;
        setsockopt(fd,IPPROTO_TCP,TCP_NODELAY,(void *)&val,sizeof(val));
}

void
sock_setunsafe(int fd)
{
        int val;
        struct linger ltime;
        val=1;
        setsockopt(fd,SOL_SOCKET,SO_REUSEADDR,(void *)&val,sizeof(val));
        ltime.l_onoff=1;
        ltime.l_linger=0;
        setsockopt(fd,SOL_SOCKET,SO_LINGER,(void *)&ltime,sizeof(ltime));
}

void
sock_setsafe(int fd)
{
        int val;
        struct linger ltime;
        val=0;
        setsockopt(fd,SOL_SOCKET,SO_REUSEADDR,(void *)&val,sizeof(val));
        ltime.l_onoff=0;
        ltime.l_linger=1;
        setsockopt(fd,SOL_SOCKET,SO_LINGER,(void *)&ltime,sizeof(ltime));
}


/* local functions */
static int
sselect_adduserptr(_sselect *ssel, int fd, void *userptr)
{
        if(ssel==NULL || fd<0)
                return(-1);
        if(ssel->sizeuserptr<(fd+1)) {
                void **newptr;
                int newsize;
                newsize=(fd+USERPTRBLOCKSIZE)/USERPTRBLOCKSIZE;
                newsize*=USERPTRBLOCKSIZE;
                if((newptr=realloc(ssel->userptr,sizeof(void *)*newsize))==NULL)
                        return(-1);
                ssel->userptr=newptr;
                memset(ssel->userptr+ssel->sizeuserptr,0,sizeof(void *)*(newsize-ssel->sizeuserptr));
                ssel->sizeuserptr=newsize;
        }
        ssel->userptr[fd]=userptr;
        return(0);
}

static int
sselect_clearuserptr(_sselect *ssel, int fd)
{
        if(ssel==NULL || fd<0)
                return(-1);
        if(ssel->sizeuserptr<(fd+1))
                return(0);
        ssel->userptr[fd]=NULL;
        return(0);
}