Click here to Skip to main content
15,891,513 members
Articles / Programming Languages / C++

Webio - An embedded web server

Rate me:
Please Sign up or sign in to vote.
4.88/5 (33 votes)
20 Jul 2008BSD3 min read 197.2K   3.8K   108  
A C-language web server with embedded files and CGI.
/* webutils.c
 *
 * Part of the Webio Open Source lightweight web server.
 *
 * Copyright (c) 2007 by John Bartas
 * All rights reserved.
 *
 * Use license: Modified from standard BSD license.
 * 
 * Redistribution and use in source and binary forms are permitted
 * provided that the above copyright notice and this paragraph are
 * duplicated in all such forms and that any documentation, advertising 
 * materials, Web server pages, and other materials related to such
 * distribution and use acknowledge that the software was developed
 * by John Bartas. The name "John Bartas" may not be used to 
 * endorse or promote products derived from this software without 
 * specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND WITHOUT ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
 * WARRANTIES OF MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE.
 *
 */

#include "websys.h"     /* port dependant system files */
#include "webio.h"
#include "webfs.h"


char hdrbuf[HDRBUFSIZE];   /* For building HTTP headers */

int   (*wi_execfunc)(wi_sess * sess, char * args) = NULL;


/* This file contins utility functions for parsing HTTP header items.
 */

char * wi_servername = "Webio Embedded server v1.0";

struct httperror {
   int      errcode;
   char *   errtext;
} httperrors[] = 
{
   400,  "Bad HTTP request",
   401,  "Authentication required",
   402,  "Payment required",
   404,  "File not found",
   501,  "Server error",
};

/* wi_senderr()
 * 
 * This is called when a session needs to send an error to the client..
 *
 * Returns: 0 if a;ll went OK, else negative WIE_ error code.
 */

int
wi_senderr(wi_sess * sess, int httpcode )
{
   int      i;
   char *   cp;
   char *   errortext = "Unknown HTTP Error";

   for(i = 0; i < (sizeof(httperrors)/sizeof(struct httperror)); i++)
   {
      if(httperrors[i].errcode == httpcode)
      {
         errortext = httperrors[i].errtext;
         break;
      }
   }

   /* Build a header */
   sprintf(hdrbuf, "HTTP/1.1 %d %s\r\n", httpcode, errortext);
   cp = hdrbuf + strlen(hdrbuf);
   sprintf(cp, "Date: %s GMT\r\n", wi_getdate(sess) );
   cp += strlen(cp);
   if(httpcode == 401)
   {
      sprintf(cp, "WWW-Authenticate: Basic realm=\"%s\"\r\n", sess->ws_uri );
      cp += strlen(cp);
   }
   sprintf(cp, "Server: %s\r\n", wi_servername );
   cp += strlen(cp);
   sprintf(cp, "Connection: close\r\n\r\n");
   cp += strlen(cp);

   /* Add some text for browser to display */
   sprintf(cp, "<html><head><title>Error %d</title></head>\r\n", httpcode);
   cp += strlen(cp);
   sprintf(cp, "<body><h2>Error %d: %s<br></h2>\r\n", 
      httpcode, errortext);
   cp += strlen(cp);
   if(sess->ws_uri)
   {
      sprintf(cp, "File: %s<br>\r\n", sess->ws_uri);
      cp += strlen(cp);
   }

   sprintf(cp, "</body></html>\r\n");
   cp += strlen(cp);

   send(sess->ws_socket, hdrbuf, strlen(hdrbuf), 0);

   /* Close socket and mark session for deletion */
   closesocket(sess->ws_socket);
   sess->ws_socket = INVALID_SOCKET;
   sess->ws_state = WI_ENDING;

   return 0;      /* OK Return */
}


int
wi_replyhdr(wi_sess * sess, int contentlen)
{
   char *   cp;
   int      hdrlen;
   int      error;

   sprintf(hdrbuf, "HTTP/1.1 200 OK\r\n");
   cp = hdrbuf + strlen(hdrbuf);
   sprintf(cp, "Date: %s GMT\r\n", wi_getdate(sess) );
   cp += strlen(cp);
   sprintf(cp, "Server: %s\r\n", wi_servername );
   cp += strlen(cp);
   sprintf(cp, "Connection: close\r\n");
   cp += strlen(cp);
   sprintf(cp, "Content-Type: %s\r\n", sess->ws_ftype );
   cp += strlen(cp);
   sprintf(cp, "Content-Length: %d\r\n\r\n", contentlen );
   cp += strlen(cp);

   hdrlen = strlen(hdrbuf);
   error = send(sess->ws_socket, hdrbuf, hdrlen, 0);
   if(error < hdrlen)
   {
      dtrap();    /* Does this ever happen? */
      error = errno;
      return WIE_SOCKET;
   }
   sess->ws_flags |= WF_HEADERSENT;
   return 0;
}

/* wi_movebinary()
 * 
 * This is called, often iterativly, to send a binary file to a socket.
 * It does no processing or scanning of the file contents. 
 * It sends until the socket returns EWOULDBLOCK or file reaches EOF.
 * After EWOULDBLOCK the socket blocks on the select call in webio.c
 * until the socket can send again, then this routine is called again.
 * 
 * Returns 0 if OK, else negative error code. 
 */

int
wi_movebinary(wi_sess * sess, wi_file * fi)
{
   int   filelen;
   int   error;

   if((sess->ws_flags & WF_HEADERSENT) == 0)   /* header sent yet? */
   {
      int   current;
      current = wi_ftell(fi);
      wi_fseek(fi, 0, SEEK_END);
      filelen = wi_ftell(fi);
      wi_fseek(fi, current, SEEK_SET);
      wi_replyhdr(sess, filelen);
   }

   while(sess->ws_state == WI_SENDDATA)
   {
      /* see if we need to get another block from the file */
      if(fi->wf_inbuf == 0)
      {
         fi->wf_inbuf = wi_fread(fi->wf_data, 1, sizeof(fi->wf_data), fi );

         if(fi->wf_inbuf < 0)
            return WIE_BADFILE;
      }
      error = send(sess->ws_socket, fi->wf_data, fi->wf_inbuf, 0);
      if(error < 0)
      {
         error = errno;
         if(error == EWOULDBLOCK)
            return 0;      /* try again later */
         else
            return WIE_SOCKET;
      }
      if(fi->wf_inbuf < sizeof(fi->wf_data))  /* end of file? */
      {
         wi_fclose(fi);
         wi_txdone(sess);     /* will cause break from while() loop */
      }
      else
      {
         fi->wf_inbuf = 0;    /* clear buffer for another file read */
      }
   }

   return 0;   /* OK return */
}

int
wi_txdone(wi_sess * sess)
{

	/* If connection is persistent change the state to read the next file  */
   if(sess->ws_flags & WF_PERSIST)
   {
      dtrap();
      sess->ws_state = WI_HEADER;
	  return 0;
   }
   else if(sess->ws_flags & WF_SVRPUSH)
   {
 	  int	error;
	  WI_FILE * fi;

      dtrap();

      /* poll server push routine */
	  fi = sess->ws_filelist;
	  if(fi->wf_routines->wfs_push == NULL)
		  return WIE_BADFILE;
	  error = fi->wf_routines->wfs_push(fi->wf_fd, sess);
	  return error;
   }
   else
   {
	   /* done with normaal connection - close the socket and mark 
	   * session for deletion; */

      closesocket(sess->ws_socket);
      sess->ws_socket = INVALID_SOCKET;
      sess->ws_state = WI_ENDING;
   }
   return 0;
}


/* wi_nextarg()
 * 
 * This is returns the next space-delimited field in an HTTP header.
 * If there are no more args in the line (i.e. it encounters a CR
 * before any more text) it returns a NULL.
 *
 * Returns: pointer to arg, or NULL if no more args.
 * 
 */

char * 
wi_nextarg( char * argbuf )
{
   while(*argbuf > ' ')    /* move to next space or CR */
      argbuf++;
   /* if we are at the end of a line, return null (no more args) */
   if(*argbuf != ' ')
      return NULL;
   while(*argbuf == ' ')   /* move to next arg */
      argbuf++;
   if(*argbuf < ' ')
      return NULL;
   else
      return argbuf;
}

/* wi_argncpy()
 * 
 * This copies the space-delimited field in the first arg into the 
 * buffer in the second arg. Copy size is limited to the size passed. 
 * 
 * Returns: number of bytes copied. This may be 0 the first arg is 
 * malformed.
 * 
 */

int 
wi_argncpy(char * buf, char * arg, int size )
{
   int   count = 0;
   while((++count < size) && (*arg > ' '))
      *buf++ = *arg++;
   return count;
}


/* wi_tagcmp()
 *
 * Compare two html tags. This differs from stricmp() in that tags are
 * terminated by any unprintable char, not just null.
 *
 * Returns 0 if tags match, -1 if not.
 */

int
wi_tagcmp(char * tag1, char * tag2)
{
   while(*tag1 > ' ')
   {
      if((*tag1++ | 0x20) != (*tag2++ | 0x20))
         return -1;
   }
   /* tags match for length of tag1 - make sure tag2 terminates here. */
   if(*tag2 > ' ')
      return -1;     /* both tags terminated */
   else
      return 0;      /* tag 2 is longer */
}


/* wi_getline()
 * 
 * This is returns The first line of the passed type in an HTML header.
 * For example if you pass "Referer" and a text pointer, it will search 
 * the passsed text for the "Referer: " line and return a pointer to the 
 * first non-space text following "Referer:".
 *
 * This allows for zeros (placed by wi_argterm) embedded in the HTTP 
 * header. If considers double CRLF to mark the end of header.
 *
 * Returns: pointer to text, or NULL if no type not found.
 * 
 */

char * 
wi_getline( char * linetype, char * httphdr )
{
   char *   cp;
   int      typelen;

   typelen = strlen(linetype);
   for(cp = httphdr; cp < (httphdr + WI_RXBUFSIZE); cp++)
   {
      if(*cp == *linetype)    /* Got a match for first char? */
      {
         if(strnicmp(cp, linetype, typelen) == 0)
         {
            cp = wi_nextarg(cp);
            if(!cp)
               return NULL;
            wi_argterm(cp);
            return(cp);
         }
      }
      if(strncmp(cp, "\r\n\r\n", 4) == 0)
         return NULL;
   }
//   dtrap();       /* Didn't find end OR field??? */
   return NULL;
}

/* wi_argterm()
 * 
 * Terminates the passed string, which is assumed to be in an HTML 
 * header. String usually ends in space or CR - this is replaced 
 * with a null for C language routines
 *
 * Returns: pointer to next text string after arg passed text, 
 * or NULL if no more strings are in the passed buffer.
 * 
 */

char * 
wi_argterm( char * arg )
{
   while(*arg > ' ' )
      arg++;
   *(arg++) = 0;
   while((*arg <= ' ') && (*arg > 0))
      arg++;
   if(*arg)
      return arg;
   else
      return NULL;
}

/* atocode() - return a code for a 2 byte hex calue */

unsigned
atocode(char * cp)
{
    unsigned   value = 0;
    unsigned   digit;
    int        i;

    for(i = 0; i <2; i++)
    {
        if (*cp >= '0' && *cp <= '9')
            digit = (unsigned) (*cp - '0');
        else if (*cp >= 'a' && *cp <= 'f')
            digit = (unsigned) (*cp - 'a') + 10;
        else if (*cp >= 'A' && *cp <= 'F')
            digit = (unsigned) (*cp - 'A') + 10;
        else
            break;

        value = (value << 4) + digit;
        cp++;
    }

    return value;
}


/* wi_urldecode(char * utext)
 * 
 * Decode the % and + characters in URLs. Decoding is done in place
 * in the buffer, since it never grows the strong
 * 
 */

void     
wi_urldecode(char * utext)
{
   char * cp = utext;
   u_char code;

   while(*cp > ' ')
   {
      if(*cp == '+')    /* plus signs always convert to space */
         *cp++ = ' ';
      else if(*cp == '%')  /* Get hex code followinf percent */
      {
         code = (u_char)atocode(cp + 1);
         /* Make sure code was valid (nonzero) */
         if(code)
         {
            *cp++ = (char)code;
            memmove(cp, cp+2, strlen(cp)-1);
         }
      }
      else
         cp++;
   }

   return;
}


/* wi_buildform()
 * 
 * Extract the name/value pairs from the second parameter, build a form
 * structure with them, and attach the form to the passed session.
 *
 * Returns: 0 if OK else negative WIE_ error code.
 * 
 */


int
wi_buildform(wi_sess * sess, char * pairs)
{
   char *      cp;
   wi_form *   form;
   int         i;
   int         pairct = 0;

   /* first, count the name/value pairs */
   for(cp = pairs; *cp; cp++)
   {
      if(*cp == '=')
         pairct++;
      if(*cp <= ' ')
         break;
   }

   /* get a buffer big enough for the form, including all the 
    * name/value pair pointers. 
    */
   form = (wi_form*)wi_alloc( sizeof(wi_form) + (pairct * sizeof(wi_pair)));
   if(!form)
      return WIE_MEMORY;

   form->paircount = pairct;
   cp = pairs;
   for(i = 0; i < pairct; i++)
   {
      form->pairs[i].name = cp;
      cp = strchr(cp, '=');
      if(!cp)
         return WIE_CLIENT;
      *cp++ = 0;
      form->pairs[i].value = cp;
      cp = strchr(cp, '&');
      if(!cp)
         wi_argterm(form->pairs[i].value);
      else
         *cp++ = 0;
      /* Decode spaces ("+") and %20 type inserts */
      wi_urldecode(form->pairs[i].name);
      wi_urldecode(form->pairs[i].value);
   }

   /* Add form to head of sesison's form list */
   form->next = sess->ws_formlist;
   sess->ws_formlist = form;

   return 0;
}

#define FT_BINARY    0x01

struct wi_ftype {
   u_long   ext;        /* encoded 1st four chars of extension */
   char *   mimetype;   /* Mime description */
   int      flags;      /* bitmask of the FT_ flags */
} wi_ftypes[] = 
{
   0x4A504700, "image/jpeg", FT_BINARY,   /* JPG */
   0x4A504547, "image/jpeg", FT_BINARY,   /* JPEG */
   0x504E4700, "image/png",  FT_BINARY,   /* PNG */
   0x47494600, "image/gif",  FT_BINARY,   /* GIF */
   0x57415600, "audio/wav", FT_BINARY,    /* WAV */
   0x4D503300, "audio/mp3", FT_BINARY,    /* MP3 */
   0x574D5600, "video/x-ms-wmv", FT_BINARY,     /* WMV */
   0x50444600, "application/pdf", FT_BINARY,    /* PDF */
   0x53574600, "application/x-shockwave-flash", FT_BINARY,    /* SWF */
};


int
wi_setftype(wi_sess * sess)
{
   int      i;
   char *   lastdot;
   u_long   type;

   lastdot = strrchr(sess->ws_uri, '.');
   if(!lastdot)
      return FALSE;  /* no dot, assume not binary */

   lastdot++;
   type = 0;

   /* find the last dot & get the next 4 chars after it. */
   for(i = 0;i < 4; i++)
   {
      type <<= 8;
      /* get next char */
      type |= (u_long)((*lastdot) & 0x000000FF);     
      type &= ~0x20;    /* Make uppercase */
      if(*lastdot >= ' ')  /* bump pointer if not at end */
         lastdot++;
   }

   /* see if the file is one of the binary types */
   for(i = 0;i < sizeof(wi_ftypes)/sizeof(struct wi_ftype); i++)
   {
      if(wi_ftypes[i].ext == type)
      {
         if(wi_ftypes[i].flags & FT_BINARY)
            sess->ws_flags |= WF_BINARY;
         sess->ws_ftype = wi_ftypes[i].mimetype;;
         return TRUE;
      }
   }
   sess->ws_flags &= ~WF_BINARY; /* not listed as binary type */
   sess->ws_ftype = "text/html"; /* default for unknown types */
   return FALSE;
}


/* wi_ssi()
 *
 * Handle an SSI request. This is called from wi_readfiles(), and for
 * simple SSI files it will recurse back into wi_readfiles(). This is
 * also where the SSI file is checked to see if it is implemented
 * in C code, and if so calls the C routine associated with the file.
 *
 * Returns 0 if OK, else negative error code
 */

int
wi_ssi(wi_sess * sess)
{
   char *      ssitext;
   char *      ssifname;      /* name of SSI file */
   char *      endname;
   char *      pairs;
   char *      args;
   wi_file *   wrapper;       /* info about containing file */
   int         error;
   char        paren;
#ifdef USE_EMFILES
   wi_file *   ssi;           /* info about SSI file */
#endif

   wrapper = sess->ws_filelist;
   ssitext = &wrapper->wf_data[wrapper->wf_nextbuf];
   ssifname = strstr(ssitext, "file=");
   if(!ssifname)
   {
      dtrap();
      return WIE_FORMAT;
   }

   ssifname += 5;    /* poinst past file=" text */
   paren = *ssifname++;
   endname = strchr(ssifname, paren);
   if(!endname)   /* no terminating quote sign? */
   {
      dtrap();
      return WIE_FORMAT;
   }
   *endname = 0;     /* ssifname (and optional name/value args) now a C string */
   pairs = strchr(ssifname, '?');
   if(pairs)
   {
      error = wi_buildform(sess, pairs + 1);  /* best effort... */
      *pairs = 0;
   }
   args = strchr(ssifname, ' ');
   if(args)
      *args = 0;

   error = wi_fopen(sess, ssifname, "r");
   if(args)
      *args = ' ';
   if(error)
   {
      dtrap();       /* No SSI file? Probably a bug; Tell programmer */
      return error;
   }

#ifdef USE_EMFILES
   /* See if the SSI file is a code-based embedded file */
   ssi = sess->ws_filelist;
   if(ssi->wf_routines == &emfs)
   {
      EOFILE * eofile = (EOFILE*)ssi->wf_fd;
      if(eofile->eo_emfile->em_flags & EMF_CEXP)
      {
         /* SSI is a CVAR - em_size is overloaded with the token */
         error = wi_cvariables(sess, eofile->eo_emfile->em_size);
      }
      else if(eofile->eo_emfile->em_flags & EMF_SSI)
      {
         /* SSI is an EFS routine */
         SSI_ROUTINE *  ssifunc;

         if(!eofile->eo_emfile->em_routine)
         {
            dtrap();
            return 0;
         }
         ssifunc = (SSI_ROUTINE*)eofile->eo_emfile->em_routine;
         ssifunc(sess, eofile);
      }
      else     /* "normal" EFS read */
         return(wi_readfile(sess));

      /* If we layered on a form, release it now */
      if(pairs && (sess->ws_formlist->next))
      {
         wi_free(sess->ws_formlist);
         sess->ws_formlist = sess->ws_formlist->next;
      }

      wi_fclose(ssi);
      return 0;
   }
#endif /* USE_EMFILES */

   error = wi_readfile(sess);
   return error;
}

/* wi_ssi()
 *
 * Crude version of SSI request. This is called from wi_readfiles(), 
 * This calls the optional C routine to "exec" the "cmd" - what it
 * actually does it totally up to the port. The C routine should 
 * write any output to fi->wf_data at fi->wf_nextbuf.
 *
 * Returns 0 if OK, else negative error code
 */

int
wi_exec(wi_sess * sess)
{
   char *   cp;
   char *   args;
   char     paren;
   int      err = 0;
   int      len;
   wi_file *   fi;     /* info about current file */


   /* start loading file to return. */
   fi = sess->ws_filelist;
   len = fi->wf_nextbuf;

   cp = &fi->wf_data[len + 10];
   if( strncmp(cp, "cmd_argument=", 13) != 0)
   {
      dtrap();
      return WIE_FORMAT;
   }
   cp += 13;

   /* see if there is an opening paren */
   paren = *cp;
   if((paren != '\'') && (paren != '\"'))
   {
      dtrap();
      return WIE_FORMAT;
   }
   args = cp + 1;

   /* Find closing paren and replace with null */
   cp = strchr(args, paren);

   if(!cp)
   {
      dtrap();
      return WIE_FORMAT;
   }
   *cp = 0;

   if(wi_execfunc)
      err = (*wi_execfunc)(sess, args);

   return err;
}




/*
//  Taken from NCSA HTTP
//
//  NOTE: These conform to RFC1113, which is slightly different then the Unix
//        uuencode and uudecode!
*/

const int pr2six[256]={
    64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,
    64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,62,64,64,64,63,
    52,53,54,55,56,57,58,59,60,61,64,64,64,64,64,64,64,0,1,2,3,4,5,6,7,8,9,
    10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,64,64,64,64,64,64,26,27,
    28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,
    64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,
    64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,
    64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,
    64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,
    64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,
    64,64,64,64,64,64,64,64,64,64,64,64,64
};


#define BYTE unsigned char

void
wi_uudecode(char * bufcoded, BYTE * pbuffdecoded)
{
    int     nbytesdecoded;
    char *  bufin;
    BYTE *  bufout;
    int     nprbytes;

    /* Strip leading whitespace. */

    while(*bufcoded==' ' || *bufcoded == '\t') bufcoded++;

    /* Figure out how many characters are in the input buffer.
     * If this would decode into more bytes than would fit into
     * the output buffer, adjust the number of input bytes downwards.
     */
    bufin = bufcoded;
    while(pr2six[*(bufin++)] <= 63);
    nprbytes = (int)(bufin - bufcoded - 1);
    nbytesdecoded = ((nprbytes+3)/4) * 3;

    bufout = pbuffdecoded;
    bufin = bufcoded;

    while (nprbytes > 0) {
        *(bufout++) =
            (unsigned char) (pr2six[*bufin] << 2 | pr2six[bufin[1]] >> 4);
        *(bufout++) =
            (unsigned char) (pr2six[bufin[1]] << 4 | pr2six[bufin[2]] >> 2);
        *(bufout++) =
            (unsigned char) (pr2six[bufin[2]] << 6 | pr2six[bufin[3]]);
        bufin += 4;
        nprbytes -= 4;
    }

    if(nprbytes & 03) {
        if(pr2six[bufin[-2]] > 63)
            nbytesdecoded -= 2;
        else
            nbytesdecoded -= 1;
    }

    pbuffdecoded[nbytesdecoded] = '\0';

    return;
}


/* wi_decode_auth()
 *
 * Decodes authentication information from http header into a text 
 * name/password format. 
 *
 * passed name and password are pointers to caller buffers. Length
 * of these buffers is indicated by passed ints. Absurdly long names 
 * and PWs are truncated and probably won't work.
 *
 * Handles errors by returning null strings.
 */

void
wi_decode_auth(wi_sess * sess, char * name, int name_len, char * pass, int pass_len)
{
   char *   authdata;
   char *   divide;
   char     decode[80];

   /* For now, just do basic auth */
   if(wi_tagcmp(sess->ws_auth, "Basic") == 0)
   {
      authdata = sess->ws_auth + strlen("Basic ");
      if(strlen(authdata) > sizeof(decode))
      {
         dtrap();    // crude overflow test failed
         return;
      }
      wi_uudecode(authdata, (u_char*)(&decode[0]));
      divide = strchr(decode, ':');
      if(!divide)
      {
         *name = 0;
         *pass = 0;
         return;
      }
      *divide++ = 0;    /* terminte name, point to password */
      strncpy(name, decode, name_len);
      strncpy(pass, divide, pass_len);
   }
   else  /* Add MD5 here later.... */
   {
      dtrap();    // Unsupported auth type
   }
   return;
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The BSD License


Written By
Chief Technology Officer praemio.com
United States United States
See my bio here:

http://www.bartas.net/resume.htm

Comments and Discussions