/*
 * The contents of this file are subject to the AOLserver Public License
 * Version 1.1 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://aolserver.com/.
 *
 * Software distributed under the License is distributed on an "AS IS"
 * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
 * the License for the specific language governing rights and limitations
 * under the License.
 *
 * The Original Code is AOLserver Code and related documentation
 * distributed by AOL.
 * 
 * The Initial Developer of the Original Code is America Online,
 * Inc. Portions created by AOL are Copyright (C) 1999 America Online,
 * Inc. All Rights Reserved.
 *
 * Alternatively, the contents of this file may be used under the terms
 * of the GNU General Public License (the "GPL"), in which case the
 * provisions of GPL are applicable instead of those above.  If you wish
 * to allow use of your version of this file only under the terms of the
 * GPL and not to allow others to use your version of this file under the
 * License, indicate your decision by deleting the provisions above and
 * replace them with the notice and other provisions required by the GPL.
 * If you do not delete the provisions above, a recipient may use your
 * version of this file under either the License or the GPL.
 */

/* 
 * urlopen.c --
 *
 *	Make outgoing HTTP requests.
 */

static const char *RCSID = "@(#) $Header: /cvsroot/aolserver/aolserver/nsd/urlopen.c,v 1.7 2001/01/16 23:03:05 jgdavidson Exp $, compiled: " __DATE__ " " __TIME__;

#include "nsd.h"

#define BUFSIZE 2048

typedef struct Stream {
    SOCKET	sock;
    int		error;
    int		cnt;
    char       *ptr;
    char	buf[BUFSIZE+1];
} Stream;

/*
 * Local functions defined in this file
 */

static int GetLine(Stream *sPtr, Ns_DString *dsPtr);
static int FillBuf(Stream *sPtr);


/*
 *----------------------------------------------------------------------
 *
 * Ns_FetchPage --
 *
 *	Fetch a page off of this very server. Url must reference a 
 *	file in the filesystem. 
 *
 * Results:
 *	NS_OK or NS_ERROR.
 *
 * Side effects:
 *	The file contents will be put into the passed-in dstring.
 *
 *----------------------------------------------------------------------
 */

int
Ns_FetchPage(Ns_DString *dsPtr, char *url, char *server)
{
    Ns_DString path;
    int        fd;
    int        nread;
    char       buf[1024];

    Ns_DStringInit(&path);
    Ns_UrlToFile(&path, server, url);
    fd = open(path.string, O_RDONLY|O_BINARY);
    Ns_DStringFree(&path);
    if (fd < 0) {
        return NS_ERROR;
    }
    while ((nread = read(fd, buf, sizeof(buf))) > 0) {
        Ns_DStringNAppend(dsPtr, buf, nread);
    }
    close(fd);
    return NS_OK;
}


/*
 *----------------------------------------------------------------------
 *
 * Ns_FetchURL --
 *
 *	Open up an HTTP connection to an arbitrary URL. 
 *
 * Results:
 *	NS_OK or NS_ERROR.
 *
 * Side effects:
 *	Page contents will be appended to the passed-in dstring.
 *	Headers returned to us will be put into the passed-in Ns_Set.
 *	The set name will be changed to a copy of the HTTP status line.
 *
 *----------------------------------------------------------------------
 */

int
Ns_FetchURL(Ns_DString *dsPtr, char *url, Ns_Set *headers)
{
    SOCKET  	    sock;
    char    	   *p;
    Ns_DString      ds;
    Stream  	    stream;
    Ns_Request     *request;
    int     	    status, tosend, n;

    status = NS_ERROR;    
    sock = INVALID_SOCKET;
    Ns_DStringInit(&ds);

    /*
     * Parse the URL and open a connection.
     */

    Ns_DStringVarAppend(&ds, "GET ", url, " HTTP/1.0", NULL);
    request = Ns_ParseRequest(ds.string);
    if (request == NULL || request->protocol == NULL ||
	!STREQ(request->protocol, "http") || request->host == NULL) {
        Ns_Log(Notice, "urlopen: invalid url '%s'", url);
        goto done;
    }
    if (request->port == 0) {
        request->port = 80;
    }
    sock = Ns_SockConnect(request->host, request->port);    
    if (sock == INVALID_SOCKET) {
	Ns_Log(Error, "urlopen: failed to connect to '%s': '%s'",
	       url, ns_sockstrerror(ns_sockerrno));
	goto done;
    }

    /*
     * Send a simple HTTP GET request.
     */
     
    Ns_DStringTrunc(&ds, 0);
    Ns_DStringVarAppend(&ds, "GET ", request->url, NULL);
    if (request->query != NULL) {
        Ns_DStringVarAppend(&ds, "?", request->query, NULL);
    }
    Ns_DStringAppend(&ds, " HTTP/1.0\r\nAccept: */*\r\n\r\n");
    p = ds.string;
    tosend = ds.length;
    while (tosend > 0) {
        n = send(sock, p, tosend, 0);
        if (n == SOCKET_ERROR) {
            Ns_Log(Error, "urlopen: failed to send data to '%s': '%s'",
		   url, ns_sockstrerror(ns_sockerrno));
	    ns_sockclose(sock);
            goto done;
        }
        tosend -= n;
	p += n;
    }

    /*
     * Buffer the socket and read the response line and then
     * consume the headers, parsing them into any given header set.
     */

    stream.cnt = 0;
    stream.error = 0;
    stream.ptr = stream.buf;
    stream.sock = sock;
    if (!GetLine(&stream, &ds)) {
	goto done;
    }
    if (headers != NULL && strncmp(ds.string, "HTTP", 4) == 0) {
	if (headers->name != NULL) {
	    ns_free(headers->name);
	}
	headers->name = Ns_DStringExport(&ds);
    }
    do {
	if (!GetLine(&stream, &ds)) {
	    goto done;
	}
	if (ds.length > 0
	    && headers != NULL
	    && Ns_ParseHeader(headers, ds.string, Preserve) != NS_OK) {
	    goto done;
	}
    } while (ds.length > 0);

    /*
     * Without any check on limit or total size, foolishly read
     * the remaining content into the dstring.
     */

    do {
    	Ns_DStringNAppend(dsPtr, stream.ptr, stream.cnt);
    } while (FillBuf(&stream));
    if (!stream.error) {
    	status = NS_OK;
    }
    
 done:
    if (request != NULL) {
        Ns_FreeRequest(request);
    }
    if (sock != INVALID_SOCKET) {
        ns_sockclose(sock);
    }
    Ns_DStringFree(&ds);
    return status;
}


/*
 *----------------------------------------------------------------------
 *
 * NsTclGetUrlCmd --
 *
 *	Implements ns_geturl. 
 *
 * Results:
 *	Tcl result.
 *
 * Side effects:
 *	See docs. 
 *
 *----------------------------------------------------------------------
 */

int
NsTclGetUrlCmd(ClientData dummy, Tcl_Interp *interp, int argc, char **argv)
{
    Ns_DString  ds;
    Ns_Set     *headers;
    int         status;

    if ((argc != 3) && (argc != 2)) {
        Tcl_AppendResult(interp, "wrong # of args:  should be \"",
                         argv[0], " url ?headersSetIdVar?", NULL);
        return TCL_ERROR;
    }
    if (argc == 2) {
        headers = NULL;
    } else {
        headers = Ns_SetCreate(NULL);
    }
    Ns_DStringInit(&ds);
    if (*argv[1] == '/') {
        if (Ns_FetchPage(&ds, argv[1], Ns_TclInterpServer(interp)) != NS_OK) {
            Tcl_AppendResult(interp, "Could not get contents of URL \"",
                             argv[1], "\"", NULL);
            status = TCL_ERROR;
            goto done;
        }
    } else {
        if (Ns_FetchURL(&ds, argv[1], headers) != NS_OK) {
            Tcl_AppendResult(interp, "Could not get contents of URL \"",
                             argv[1], "\"", NULL);
            if (headers != NULL) {
                Ns_SetFree(headers);
            }
            status = TCL_ERROR;
            goto done;
        }
    }
    if (argc == 3) {
        Ns_TclEnterSet(interp, headers, 1);
        Tcl_SetVar(interp, argv[2], interp->result, 0);
    }
    Tcl_SetResult(interp, ds.string, TCL_VOLATILE);
    status = TCL_OK;
    
  done:
    Ns_DStringFree(&ds);
    
    return status;
}


/*
 *----------------------------------------------------------------------
 *
 * FillBuf --
 *
 *	Fill the socket stream buffer.
 *
 * Results:
 *	1 if fill ok, 0 otherwise.
 *
 * Side effects:
 *	None.
 *
 *----------------------------------------------------------------------
 */

static int
FillBuf(Stream *sPtr)
{
    int n;
    
    n = recv(sPtr->sock, sPtr->buf, BUFSIZE, 0);
    if (n <= 0) {
    	if (n < 0) {
	    Ns_Log(Error, "urlopen: "
		   "failed to fill socket stream buffer: '%s'", strerror(errno));
	    sPtr->error = 1;
	}
    	return 0;
    }
    sPtr->buf[n] = '\0';
    sPtr->ptr = sPtr->buf;
    sPtr->cnt = n;
    return 1;
}


/*
 *----------------------------------------------------------------------
 *
 * GetLine --
 *
 *	Copy the next line from the stream to a dstring, trimming
 *	the \n and \r.
 *
 * Results:
 *	1 or 0.
 *
 * Side effects:
 *	The dstring is truncated on entry.
 *
 *----------------------------------------------------------------------
 */

static int
GetLine(Stream *sPtr, Ns_DString *dsPtr)
{
    char *eol;
    int n;

    Ns_DStringTrunc(dsPtr, 0);
    do {
	if (sPtr->cnt > 0) {
	    eol = strchr(sPtr->ptr, '\n');
	    if (eol == NULL) {
		n = sPtr->cnt;
	    } else {
		*eol++ = '\0';
		n = eol - sPtr->ptr;
	    }
	    Ns_DStringNAppend(dsPtr, sPtr->ptr, n - 1);
	    sPtr->ptr += n;
	    sPtr->cnt -= n;
	    if (eol != NULL) {
		n = dsPtr->length;
		if (n > 0 && dsPtr->string[n-1] == '\r') {
		    Ns_DStringTrunc(dsPtr, n-1);
		}
		return 1;
	    }
	}
    } while (FillBuf(sPtr));
    return 0;
}
