/*
GS natneg client 0.1.1
by Luigi Auriemma
e-mail: aluigi@autistici.org
web:    aluigi.org


INTRODUCTION
============
Gamespy natneg is a way used by Gamespy to allow the people behind router
and NAT (so those who can't receive connections from internet) to create
public servers and at the same time allows clients to query and join
these servers.

The function in this code is very easy to use and can be implemented in a
trasparent way in any program for adding the client-side support to the
Gamespy natneg.


HOW TO USE
==========
Exists only one function to use and it's gsnatneg() which returns 0 if
success or a negative value in case of errors.

The following are the arguments of the function:
  int sd        the UDP socket you have created in your client, this
                parameter is required since the master server will create
                a direct UDP connection between the port used by that
                socket and the server
  u8 *gamename  the Gamespy name of the game, it's needed since the
                Gamespy master server needs to locate the IP of the
                server you want to contact in its big database, the full
                list of Gamespy gamenames is available here:
                http://aluigi.org/papers/gslist.cfg
  u8 *host      the hostname or the IP address in text format of the
                server to contact (like "1.2.3.4" or "host.example.com")
  in_addr_t ip  as above but it's directly the IP in the inet_addr
                format, is required to choose between the first and this
                argument
  u16 port      the port of the server to contact


EXAMPLES
========
    int sd;
    sd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
    if(gsnatneg(sd, "halor", "1.2.3.4", 0, 2302) < 0) exit(1);
    // or
    if(gsnatneg(sd, "halor", NULL, inet_addr("1.2.3.4"), 2302) < 0) exit(1);

    // if you want you can also terminate and free the gsnatneg resources using:
    gsnatneg(-1, NULL, NULL, 0, 0);


LICENSE
=======
    Copyright 2008 Luigi Auriemma

    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 2 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, write to the Free Software
    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA

    http://www.gnu.org/licenses/gpl.txt
*/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <ctype.h>
#include <time.h>

#ifdef WIN32
    #include <winsock.h>

    #define close   closesocket
    #define sleep   Sleep
    #define ONESEC  1000
    #define in_addr_t   uint32_t
#else
    #include <unistd.h>
    #include <sys/socket.h>
    #include <sys/types.h>
    #include <arpa/inet.h>
    #include <netinet/in.h>
    #include <netdb.h>

    #define ONESEC  1
    #define stricmp strcasecmp
#endif

typedef uint8_t     u8;
typedef uint16_t    u16;
typedef uint32_t    u32;



#define BUFFSZ          0xffff
#define GSNATNEGPORT    27901
#define GSMSXPORT       28910
#define MAXRETRY        2



int gsnatneg_make_tcp(void);
int gsnatneg_make_udp(void);
in_addr_t gsnatneg_get_sock_ip_port(int sd, u16 *port, struct sockaddr_in *ret_peer);
int gsnatneg_putrr(u8 *data, int len);
int gsnatneg_putxx(u8 *data, u32 num, int bits);
int gsnatneg_putss(u8 *buff, u8 *data);
int gsnatneg_putmm(u8 *buff, u8 *data, int len);
int gsnatneg_send(int sd, in_addr_t ip, u16 port, u8 *buff, int len);
int gsnatneg_sendto(int sd, in_addr_t ip, u16 port, u8 *buff, int len);
int gsnatneg_recv(int sd, u8 *buff);
int gsnatneg_tcprecv(int sd, u8 *buff, int len);
int gsnatneg_recvfrom(int sd, struct sockaddr_in *peer, u8 *buff, int size);
in_addr_t gsnatneg_gsresolv(u8 *gamename, int num);
int gsnatneg_timeout(int sock, int secs);
in_addr_t gsnatneg_resolv(u8 *host);



static int gsnatneg_verbose     = 0;



int gsnatneg(int sd, u8 *gamename, u8 *host, in_addr_t ip, u16 port) {
    static const u8 natnegx[6]  = "\xfd\xfc\x1e\x66\x6a\xb2";
    struct sockaddr_in  peer;
    static in_addr_t
                gsmsx           = INADDR_NONE,
                gsnatneg1       = INADDR_NONE,
                gsnatneg2       = INADDR_NONE,
                myip            = INADDR_NONE;  // not needed
    in_addr_t   xip             = INADDR_NONE;
    static u32  seed            = 0;
    static int  su              = -1,
                st              = -1;
    int         len,
                retry           = MAXRETRY;     // in case of packets lost and timed out connections
    u16         myport          = 0,
                xport           = 0;
    static u8   gamenamex[128]  = "",
                *buff           = NULL;
    u8          *p;

    if((sd <= 0) || !gamename) {    // free resources
        if(buff) {
            free(buff);
            buff = NULL;
        }
        goto quit;
    }

    if(host) {
        ip = gsnatneg_resolv(host);
        if(ip == INADDR_NONE) goto quit;
    } else {
        if(ip == INADDR_NONE) goto quit;
    }
    if(!gamenamex[0] || stricmp(gamename, gamenamex)) {
        gsmsx     = gsnatneg_gsresolv(gamename, 0); // gamename.ms?.gamespy.com
        if(gsmsx     == INADDR_NONE) goto quit;
        gsnatneg1 = gsnatneg_gsresolv(gamename, 1); // gamename.natneg1.gamespy.com
        if(gsnatneg1 == INADDR_NONE) goto quit;
        gsnatneg2 = gsnatneg_gsresolv(gamename, 2); // gamename.natneg2.gamespy.com
        if(gsnatneg2 == INADDR_NONE) goto quit;
        strncpy(gamenamex, gamename, sizeof(gamenamex));
        gamenamex[sizeof(gamenamex) - 1] = 0;

redo_step1:
        if(st > 0) close(st);
        st = gsnatneg_make_tcp();   // connect to gamename.ms?.gamespy.com
        if(st < 0) goto quit;
        if(gsnatneg_send(st, gsmsx, GSMSXPORT, NULL, 0) < 0) goto quit;

        if(su <= 0) {
            su = gsnatneg_make_udp();
            if(su < 0) goto quit;
        }

        if(!buff) {
            buff = malloc(BUFFSZ);
            if(!buff) goto quit;
        }

        p = buff;
        p += gsnatneg_putxx(p, 0,           8);
        p += gsnatneg_putxx(p, 1,           8);
        p += gsnatneg_putxx(p, 3,           8);
        p += gsnatneg_putxx(p, 0,           32);
        p += gsnatneg_putss(p, gamename);
        p += gsnatneg_putss(p, gamename);
        p += gsnatneg_putrr(p, 8);
        p += gsnatneg_putss(p, ""); // parameters filter
        *p++ = 0;
        *p++ = 0;
        *p++ = 0;
        *p++ = 2;
        if(gsnatneg_send(st, 0, 0, buff, p - buff) < 0) goto quit;
        if(!gsnatneg_timeout(st, 5)) {
            if(recv(st, (void *)buff, BUFFSZ, 0) <= 0) goto quit;
        }
    }

    seed = time(NULL);  // needed

    p = buff;
    p += gsnatneg_putxx(p, 2,           8);
    p += gsnatneg_putxx(p, ip,          32);
    p += gsnatneg_putxx(p, htons(port), 16);
    p += gsnatneg_putmm(p, (u8*)natnegx,sizeof(natnegx));
    p += gsnatneg_putxx(p, seed,        32);
    if(gsnatneg_send(st, 0, 0, buff, p - buff) < 0) {
        if(--retry) goto redo_step1;    // in case we were using an old st connection
        goto quit;
    }

    myip = gsnatneg_get_sock_ip_port(st, NULL, NULL);   // probably not needed but it's for compatibility with the protocol

    retry = MAXRETRY;
redo_step2:
    p = buff;
    p += gsnatneg_putmm(p, (u8*)natnegx,sizeof(natnegx));
    p += gsnatneg_putxx(p, 2,           8);     // natneg version
    p += gsnatneg_putxx(p, 0,           8);     // step, buff[12]
    p += gsnatneg_putxx(p, seed,        32);    // id for tracking the reply
    p += gsnatneg_putxx(p, 0,           8);     // natneg number, buff[12]
    p += gsnatneg_putxx(p, 0,           8);     // ???
    p += gsnatneg_putxx(p, 1,           8);     // ???
    p += gsnatneg_putxx(p, myip,        32);    // client's IP
    p += gsnatneg_putxx(p, 0,           16);    // client's port, buff + 19
    p += gsnatneg_putss(p, gamename);

    buff[12] = 0;
    if(gsnatneg_sendto(sd, gsnatneg1, GSNATNEGPORT, buff, p - buff) < 0) goto quit;

    buff[12] = 1;
    if(gsnatneg_sendto(su, gsnatneg1, GSNATNEGPORT, buff, p - buff) < 0) goto quit;

    buff[12] = 2;
    gsnatneg_get_sock_ip_port(sd, &myport, NULL);   // retrieve the port assigned to the socket used in the first packet
    gsnatneg_putxx(buff + 19, htons(myport), 16);
    if(gsnatneg_sendto(su, gsnatneg2, GSNATNEGPORT, buff, p - buff) < 0) goto quit;

    for(;;) {
        len = gsnatneg_recvfrom(su, &peer, buff, BUFFSZ);
        if(len < 0) {
            if(--retry) goto redo_step2;
            goto quit;
        }

        if((len >= 18) && (buff[7] == 5)) {// && (peer.sin_addr.s_addr == gsnatneg1)) {
            xip   = *(in_addr_t *)(buff + 12);
            xport = ntohs(*(u16 *)(buff + 16));
            if(gsnatneg_verbose) printf("  %s:%hu\n", inet_ntoa(*(struct in_addr *)&xip), xport);
            break;
            if((xip == ip) && (xport == port)) break;   // not needed
        }
    }

    for(;;) {
        len = gsnatneg_recvfrom(sd, &peer, buff, BUFFSZ);
        if(len < 0) break;
        if((peer.sin_addr.s_addr == ip)  && (peer.sin_port == htons(port)))  break;
        if((peer.sin_addr.s_addr == xip) && (peer.sin_port == htons(xport))) break;
    }

    p = buff;
    p += gsnatneg_putmm(p, (u8*)natnegx,sizeof(natnegx));
    p += gsnatneg_putxx(p, 2,           8);
    p += gsnatneg_putxx(p, 7,           8);
    p += gsnatneg_putxx(p, seed,        32);
    p += gsnatneg_putxx(p, ip,          32);
    p += gsnatneg_putxx(p, htons(port), 16);
    p += gsnatneg_putxx(p, 1,           8);
    p += gsnatneg_putxx(p, 0,           8);
    if(gsnatneg_sendto(sd, ip, port, buff, p - buff) < 0) goto quit;
    gsnatneg_recvfrom(sd, &peer, buff, BUFFSZ);

    return(0);

quit:
    if(su > 0) {
        close(su);
        su = -1;
    }
    if(st > 0) {
        close(st);
        st = -1;
    }
    return(-1);
}



int gsnatneg_make_tcp(void) {
    struct  linger  ling = {1,1};
    int     sd;

    sd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if(sd < 0) return(-1);
    setsockopt(sd, SOL_SOCKET, SO_LINGER, (char *)&ling, sizeof(ling));
    return(sd);
}



int gsnatneg_make_udp(void) {
    struct  linger  ling = {1,1};
    int     sd;

    sd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
    if(sd < 0) return(-1);
    setsockopt(sd, SOL_SOCKET, SO_LINGER, (char *)&ling, sizeof(ling));
    return(sd);
}



in_addr_t gsnatneg_get_sock_ip_port(int sd, u16 *port, struct sockaddr_in *ret_peer) {
    struct sockaddr_in  peer;
    int     psz;

    psz = sizeof(struct sockaddr_in);
    if(getsockname(sd, (struct sockaddr *)&peer, &psz)
      < 0) return(INADDR_NONE);
    if(port) *port = ntohs(peer.sin_port);
    if(ret_peer) memcpy(ret_peer, &peer, sizeof(struct sockaddr_in));
    return(peer.sin_addr.s_addr);
}



int gsnatneg_putrr(u8 *data, int len) {
    u32     seed;
    int     i;

    seed = time(NULL);
    for(i = 0; i < len; i++) {
        seed = (seed * 0x343FD) + 0x269EC3;
        data[i] = (((seed >> 16) & 0x7fff) % 93) + 33;
    }
    data[i++] = 0;
    return(i);
}



int gsnatneg_putxx(u8 *data, u32 num, int bits) {
    int     i,
            bytes;

    bytes = bits >> 3;
    for(i = 0; i < bytes; i++) {
        data[i] = (num >> (i << 3)) & 0xff;
    }
    return(bytes);
}



int gsnatneg_putss(u8 *buff, u8 *data) {
    int     len;

    len = strlen(data) + 1;
    memcpy(buff, data, len);
    return(len);
}



int gsnatneg_putmm(u8 *buff, u8 *data, int len) {
    memcpy(buff, data, len);
    return(len);
}



int gsnatneg_send(int sd, in_addr_t ip, u16 port, u8 *buff, int len) {
    struct  sockaddr_in peer;
    u8      tmp[2];

    if(!buff) {
        peer.sin_addr.s_addr = ip;
        peer.sin_port        = htons(port);
        peer.sin_family      = AF_INET;

        if(gsnatneg_verbose) printf("- TCP connection to %s\n", inet_ntoa(peer.sin_addr));
        if(connect(sd, (struct sockaddr *)&peer, sizeof(struct sockaddr_in))
          < 0) return(-1);
        return(0);
    }
    peer.sin_addr.s_addr = gsnatneg_get_sock_ip_port(sd, NULL, NULL);
    if(gsnatneg_verbose) printf("- TCP send %d bytes to %s\n", len, inet_ntoa(peer.sin_addr));

    /* fast sending solution, but requires more memory
    static u8   *tmp = NULL;
    if(!tmp) {
        tmp = malloc(2 + 0xffff);
        if(!tmp) return(-1);
    }
    len += 2;
    gsnatneg_putxx(tmp, htons(len), 16);
    memcpy(tmp + 2, buff, len - 2);
    if(send(sd, tmp,  len, 0) != len) return(-1);
    */
    gsnatneg_putxx(tmp, htons(len + 2), 16);
    if(send(sd, tmp,  2,   0) != 2)   return(-1);
    if(send(sd, buff, len, 0) != len) return(-1);
    return(0);
}



int gsnatneg_sendto(int sd, in_addr_t ip, u16 port, u8 *buff, int len) {
    struct  sockaddr_in peer;

    if(ip == INADDR_NONE) return(0);    // or -1?
    peer.sin_addr.s_addr = ip;
    peer.sin_port        = htons(port);
    peer.sin_family      = AF_INET;

    if(!buff) {
        connect(sd, (struct sockaddr *)&peer, sizeof(struct sockaddr_in));
        return(0);
    }
    if(gsnatneg_verbose) printf("- UDP send %d bytes to %s:%hu\n", len, inet_ntoa(peer.sin_addr), ntohs(peer.sin_port));
    if(sendto(sd, buff, len, 0, (struct sockaddr *)&peer, sizeof(struct sockaddr_in))
      != len) return(-1);
    return(0);
}



int gsnatneg_recv(int sd, u8 *buff) {
    int     len;
    u8      tmp[2];

    if(gsnatneg_tcprecv(sd, tmp, 2) < 0) return(-1);
    len = (tmp[0] << 8) | tmp[1];
    len -= 2;
    if(len < 2) return(-1);
    if(gsnatneg_tcprecv(sd, buff, len) < 0) return(-1);
    return(len);
}



int gsnatneg_tcprecv(int sd, u8 *buff, int len) {
    int         t;

    while(len) {
        if(gsnatneg_timeout(sd, 3) < 0) return(-1);
        t = recv(sd, buff, len, 0);
        if(t <= 0) return(-1);
        buff += t;
        len  -= t;
    }
    return(0);
}



int gsnatneg_recvfrom(int sd, struct sockaddr_in *peer, u8 *buff, int size) {
    int     len,
            psz;

    if(gsnatneg_timeout(sd, 3) < 0) return(-1);
    if(peer) {
        psz = sizeof(struct sockaddr_in);
        len = recvfrom(sd, buff, BUFFSZ, 0, (struct sockaddr *)peer, &psz);
    } else {
        len = recvfrom(sd, buff, BUFFSZ, 0, NULL, NULL);
    }
    if(len < 0) return(-1);

    if(gsnatneg_verbose) printf("- recv %d bytes from %s\n", len, peer ? inet_ntoa(peer->sin_addr) : "unknown");
    return(len);
}



in_addr_t gsnatneg_gsresolv(u8 *gamename, int num) {
    in_addr_t   ip;
    u32     i,
            c,
            server_num;
    int     len;
    u8      tmp[256];

    server_num = 0;
    for(i = 0; gamename[i]; i++) {
        c = tolower(gamename[i]);
        server_num = c - (server_num * 0x63306ce7);
    }
    server_num %= 20;

    if(num) {   // natneg
        len = snprintf(tmp, sizeof(tmp), "%s.natneg%d.gamespy.com", gamename, num);
    } else {    // ms?
        len = snprintf(tmp, sizeof(tmp), "%s.ms%d.gamespy.com", gamename, server_num);
    }
    if((len < 0) || (len >= sizeof(tmp))) return(INADDR_NONE);
    if(gsnatneg_verbose) printf("- resolv %s", tmp);
    ip = gsnatneg_resolv(tmp);
    if(gsnatneg_verbose) printf(" --> %s\n", inet_ntoa(*(struct in_addr *)&ip));
    return(ip);
}



int gsnatneg_timeout(int sock, int secs) {
    struct  timeval tout;
    fd_set  fd_read;

    tout.tv_sec  = secs;
    tout.tv_usec = 0;
    FD_ZERO(&fd_read);
    FD_SET(sock, &fd_read);
    if(select(sock + 1, &fd_read, NULL, NULL, &tout)
      <= 0) return(-1);
    return(0);
}



in_addr_t gsnatneg_resolv(u8 *host) {
    struct  hostent *hp;
    in_addr_t     host_ip;

    host_ip = inet_addr(host);
    if(host_ip == INADDR_NONE) {
        hp = gethostbyname(host);
        if(!hp) return(INADDR_NONE);
        host_ip = *(in_addr_t *)hp->h_addr;
    }
    return(host_ip);
}


