/*
 * Copyright (C) 2017 Michael Neuling <mikey@neuling.org>, IBM
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * 	http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
 * implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * Compile with:
 *   gcc -static -O2 mambo-socket-proxy.c -o mambo-socket-proxy -pthread
 * Run inside the simulator:
 *   - to forward host ssh connections to sim ssh server
 *     ./mambo-socket-proxy -h 10022 -s 22
 *        Then connect to port 10022 on your host
 *        ssh -p 10022 localhost
 *   - to allow http proxy access from inside the sim to local http proxy
 *     ./mambo-socket-proxy -b proxy.mynetwork -h 3128 -s 3128
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <errno.h>
#include <pthread.h>
#include <getopt.h>

#define CALL_TCL		       86
#define BOGUS_SOCKET_CONN_PROBE_CODE  224
#define BOGUS_SOCKET_CONN_SEND_CODE   225
#define BOGUS_SOCKET_CONN_RECV_CODE   226

static inline int callthru2(int command, unsigned long arg1, unsigned long arg2)
{
	register int c asm("r3") = command;
	register unsigned long a1 asm("r4") = arg1;
	register unsigned long a2 asm("r5") = arg2;
	asm volatile (".long 0x000eaeb0":"=r" (c):"r"(c), "r"(a1), "r"(a2));
	return (c);
}

static inline int callthru3(int command, unsigned long arg1, unsigned long arg2,
			    unsigned long arg3)
{
	register int c asm("r3") = command;
	register unsigned long a1 asm("r4") = arg1;
	register unsigned long a2 asm("r5") = arg2;
	register unsigned long a3 asm("r6") = arg3;
	asm volatile (".long 0x000eaeb0":"=r" (c):"r"(c), "r"(a1), "r"(a2),
		      "r"(a3));
	return (c);
}

static inline int callthru4(int command, unsigned long arg1, unsigned long arg2,
                            unsigned long arg3, unsigned long arg4)
{
    register int c asm("r3") = command;
    register unsigned long a1 asm("r4") = arg1;
    register unsigned long a2 asm("r5") = arg2;
    register unsigned long a3 asm("r6") = arg3;
    register unsigned long a4 asm("r7") = arg4;
    asm volatile (".long 0x000eaeb0":"=r" (c):"r"(c),  "r"(a1), "r"(a2),
                                              "r"(a3), "r"(a4));
    return (c);
}

static inline int callthru5(int command, unsigned long arg1, unsigned long arg2,
                            unsigned long arg3, unsigned long arg4, unsigned long arg5)
{
    register int c asm("r3") = command;
    register unsigned long a1 asm("r4") = arg1;
    register unsigned long a2 asm("r5") = arg2;
    register unsigned long a3 asm("r6") = arg3;
    register unsigned long a4 asm("r7") = arg4;
    register unsigned long a5 asm("r8") = arg5;
    asm volatile (".long 0x000eaeb0":"=r" (c):"r"(c),  "r"(a1), "r"(a2),
                                              "r"(a3), "r"(a4), "r"(a5));
    return (c);
}

unsigned long callthru_tcl(const char *str, int strlen)
{
	return callthru2(CALL_TCL, (unsigned long)str,
			 (unsigned long)strlen);
}

unsigned long bogus_socket_conn_probe(int dev, void *addr, int conn)
{
    return callthru3(BOGUS_SOCKET_CONN_PROBE_CODE,
                     (unsigned long)dev,
                     (unsigned long)addr,
                     (unsigned long)conn);
}

unsigned long bogus_socket_conn_recv(int dev, void *addr, int maxlen, int conn)
{
    return callthru4(BOGUS_SOCKET_CONN_RECV_CODE,
                     (unsigned long)dev,
                     (unsigned long)addr,
                     (unsigned long)maxlen,
                     (unsigned long)conn);
}

unsigned long bogus_socket_conn_send(int dev, void *addr, int maxlen, int conn)
{
    return callthru5(BOGUS_SOCKET_CONN_SEND_CODE,
                     (unsigned long)dev,
                     (unsigned long)addr,
                     (unsigned long)maxlen,
                     0,
                     (unsigned long)conn);
}

#define BUF_MAX 1024

struct sock_info {
	char *host;
	int sock;
	int dev;
	int open;
	int conn;
};

void *recv_thread(void *ptr)
{
	struct timeval timeout;
	struct sock_info *si = ptr;
	char buf[BUF_MAX];
	int len;
	fd_set set;

	/* 1 sec */


	while(1) {
		FD_ZERO(&set);
		FD_SET(si->sock, &set);
		timeout.tv_sec = 1;
		timeout.tv_usec = 0;
		/* Set timeout to 1 second */
		len =  select(si->sock+1, &set, NULL, NULL, &timeout);
		if (len <= 0) /* timeout */
			len = -1;
		else 	/* Receive from mambo tcp server */
			len = recv(si->sock, &buf, BUF_MAX, 0);
		if (len == 0) {
			si->open = 0;
			return NULL; /* closed */
		}
		if (len != -1) {
			bogus_socket_conn_send(si->dev, &buf, len, si->conn);
		}
		if (!si->open)
			return NULL;
	}
}

#define POLL_MAX_NS 10000000

void *send_thread(void *ptr)
{
	struct sock_info *si = ptr;
	char buf[BUF_MAX];
	int len;
	struct timespec t;
	int fault_retry = 16;

	t.tv_sec = 0;
	t.tv_nsec = POLL_MAX_NS;

	while(1) {
		/* Send to mambo tcp server */
		len = bogus_socket_conn_recv(si->dev, &buf, BUF_MAX, si->conn);
		if (len == -3 && fault_retry--) {
			 /* Page fault.  Touch the buf and try again */
			memset(buf, 0, BUF_MAX);
			continue;
		}
		fault_retry = 16;

		if (len == -1) /* EAGAIN */
			nanosleep(&t , NULL);
		else if (len > 0)
			send(si->sock, &buf, len, 0);
		else {
			si->open = 0;
			return NULL; /* closed */
		}
		if (!si->open)
			return NULL;
	}

}

void *connect_sockets(void *ptr)
{
	struct sock_info *si = ptr;
	pthread_t recv, send;
	unsigned long rc = 0;

	if (pthread_create(&recv, NULL, recv_thread, si) ||
	    pthread_create(&send, NULL, send_thread, si)) {
		rc = -1;
		goto out;
	}

	if (pthread_join(recv, NULL) || pthread_join(send, NULL)) {
		rc = -1;
		goto out;
	}

out:
	/* FIXME: Do shutdown better */
	shutdown(si->sock, SHUT_WR);
	si->open = 0;
	free(si);
	return (void *)rc;
}

void print_usage() {
	printf("Usage:\n");
	printf("     mambo-socket-proxy [-b <host>] -h <host port> -s <sim port>\n");
	printf("\n");
	printf("             -h <host port>     : Port on the host to forward\n");
	printf("             -s <host port>     : Port in the sim to forward\n");
	printf("             -b <host machine>  : Connect sim port to host network\n");
	printf("\n");
}

int main (int argc, char *argv[])
{
	char cmd[128];
	struct sockaddr_in ser, client;
	pthread_t sockets_thread;
	struct sock_info *si;
	int sock, conn, rc = 0, option = 0, one_shot = 0, c, sock_desc = 0;
	char *host = NULL;
	int host_port = -1, sim_port = -1;
	int dev = 1; /* backwards starts at 1 so forwards can use 0 */

	while ((option = getopt(argc, argv,"rb:h:s:")) != -1) {
		switch (option) {
		case 'b' :
			host = optarg;
			break;
		case 'h' :
			host_port = atoi(optarg);
			break;
		case 's' :
			sim_port = atoi(optarg);
			break;
		default:
			print_usage();
			exit(1);
		}
	}

	if (host_port == -1 || sim_port ==-1) {
		print_usage();
		exit(EXIT_FAILURE);
	}

	/*
	 * A host/backwards connection will use dev=0 and conn >= 0.
	 * The forwards connection will use dev >= 1 and conn=0
	 */
	if (host) {
		sock_desc = socket(PF_INET, SOCK_STREAM, 0);
		ser.sin_family = AF_INET;
		ser.sin_addr.s_addr = INADDR_ANY;
		ser.sin_port = htons(sim_port);

		if (bind(sock_desc, (struct sockaddr *) &ser, sizeof(ser)) < 0) {
			perror("Can't connect to sim port");
			rc = -1;
			goto out;
		}

		listen(sock_desc, 3);
	} else {
		/*
		 * Cleaning up old bogus socket.
		 */
		sprintf(cmd, "mysim bogus socket cleanup");
		callthru_tcl(cmd, strlen(cmd));
		sleep(1); /* Cleanup takes a while */
		sprintf(cmd, "mysim bogus socket init 0 server "
			"127.0.0.1 %i poll 0 nonblock",	host_port);
		callthru_tcl(cmd, strlen(cmd));
	}

	while (1) {

		if (host) {
			sock = accept(sock_desc, (struct sockaddr *)&client, (socklen_t*)&c);
			if (sock < 0) {
				perror("accept failed");
				rc = -1;
				goto out;
			}

			sprintf(cmd, "mysim bogus socket init %i client %s %i poll 0",
				dev, host, host_port);
			callthru_tcl(cmd, strlen(cmd));
			while (bogus_socket_conn_probe(dev, NULL, 0) == -1)
				sleep(1);
		} else {
			struct timespec t;
			t.tv_sec = 0;
			t.tv_nsec = 10000000;
			do {
				conn = bogus_socket_conn_probe(0, NULL, -1);
				nanosleep(&t , NULL);
			} while (conn == -1);

			sock = socket(PF_INET, SOCK_STREAM, 0);
			ser.sin_family = AF_INET;
			ser.sin_port = htons(sim_port);
			ser.sin_addr.s_addr = inet_addr("127.0.0.1");
			memset(ser.sin_zero, '\0', sizeof ser.sin_zero);

			if (connect(sock, (struct sockaddr *) &ser, sizeof(ser))) {
				perror("Can't connect to sim port");
				rc = -1;
				goto out;
			}
		}

		si = malloc(sizeof(struct sock_info));
		si->host = host;
		si->sock = sock;
		si->dev = host?dev:0;
		si->open = 1;
		si->conn = host?0:conn;

		if (pthread_create(&sockets_thread, NULL, connect_sockets, si)) {
				rc = -1;
				goto out;
		}

		if (one_shot)
			break;
		++dev; // FIXME: do a real allocator
	}
out:
	exit(rc);
}
