A Place Where No Dreams Come True...

Current Project: I was having trouble with my NNTP client and wasn't sure what was wrong with the results I was receiving so I wrote a simple NNTP client interface for the sole purpose of seeing the low-level protocol responses for the group and article queries I was requesting. It's sad that my client program wasn't capable of exposing the low level client/server dialogs.

NNTP Is Yet Another 'Thankfully' Text Based Protocol...

There was some ambiguity for a while between my NNTP client and server. Apparently because of certain changes by my ISP coincidental with a large scale NNTP storage failure I was having service issues. Eventually they cleared up but in the meantime my curiosity led me to write a simple command-line interface to excersize the protocol manually to see what the actual server responses were.

NNTP as well as all legacy (DARPA) protocols are quite specific. You send a request. The server responds with a status line followed maybe by data which is terminated by a '.' on a line by itself.

Sounds simple. Huh! Well... Because the data comes entirely from subscribers (users) it can contain almost anything. The rules are not really specific on the data itself. So you always have to be on guard when parsing public data sources.

//
// client.c
//
//  socket nntp client implementation.
//
// Copyright (c) 2014 - Dysfunctional Farms A.K.A. www.smegware.com
//
//  All Smegware software is free; 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 software 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.
//
//-----------------------------------------------------------------------------
//
// History...
//
//   $Source$
//   $Author$
// $Revision$
//
// $Log$
//-----------------------------------------------------------------------------

#include <time.h>
#include <stdio.h>
#include <stdarg.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include "report.h"

//-----------------------------------------------------------------------------

#define NNTP_MAXQUE 1024
#define NNTP_MAXLINE 4096

typedef struct nntpque {
  int sock;
  size_t point;
  size_t queued;
  char que[NNTP_MAXQUE];
  char line[NNTP_MAXLINE];
} newsque, *pnewsque;

//-----------------------------------------------------------------------------

static void nntp_make_date(char *date, size_t size)
{
  time_t rawtime;
  struct tm * timeinfo;

  time(&rawtime);
  timeinfo = gmtime(&rawtime);

  strftime(date, size, "%a, %d %b %Y %H:%M:%S GMT\r\n", timeinfo);
}

//-----------------------------------------------------------------------------

static int nntp_getline(newsque *que)
{
  int result = 0;
  int i;

  for(i = 0; i < NNTP_MAXLINE; i++)
  {
    if(que->point >= que->queued)
    {
      que->queued = recv(que->sock, que->que, NNTP_MAXQUE - 1, 0);
      if(que->queued <= 0)
      {
	if(que->queued == 0)
	{
	  report(dbglevl_none, "error : nntp_getline() recv() failed %i - Connection reset by peer\n", que->queued);
	}
	else
	{
	  report_error("error : nntp_getline() recv() failed %i - ", que->queued);
	}
	break;
      }
      que->point = 0;
    }
    que->line[i] = que->que[que->point++];
    if((que->line[i] == '\r') || (que->line[i] == '\n'))
    {
      que->line[i] = '\0';
      que->point++;
      result = 1;
      break;
    }
  }
  return result;
}

//-----------------------------------------------------------------------------

static int nntp_post(newsque *que)
{
  int result = 0;
  int n;
  size_t s;
  char line[256];
  char *stdline = NULL;

  report(dbglevl_none, "%s\n", &que->line);
  n = sprintf(line, "From: Uncle Mikey <nowhere@everywhere.com>\r\n");
  report(dbglevl_none, "%s", line);
  if(send(que->sock, line, n, 0) > 0)
  {
    n = sprintf(line, "Newsgroups: alt.test.test\r\n");
    report(dbglevl_none, "%s", line);
    if(send(que->sock, line, n, 0) > 0)
    {
      n = sprintf(line, "Subject: Nothing in particular.\r\n");
      report(dbglevl_none, "%s", line);
      if(send(que->sock, line, n, 0) > 0)
      {
	nntp_make_date(line, sizeof(line) - 1);
	report(dbglevl_none, "Date: %s", line);
	if(send(que->sock, line, strlen(line), 0) > 0)
	{
	  if(send(que->sock, "\r\n", 2, 0) > 0)
	  {
	    // Body from stdin.
	    s = 0;
	    report(dbglevl_none, "\n");
	    while(1)
	    {
	      getline(&stdline, &s, stdin);
	      n = strlen(stdline);
	      if(n)
	      {
		// Replace trailing "\n\0" with "\r\n".
		stdline[n - 1] = '\r';
		stdline[n] = '\n';
		if(send(que->sock, stdline, n + 1, 0) < 0)
		{
		  break;
		}
	      }
	      if(strncmp(".\r\n", stdline, 3) == 0)
	      {
		result = 1;
		break;
	      }
	    }
	  }
	}
      }
    }
  }
  if(stdline)
  {
    report(dbglevl_msg, "freeing stdin line buffer.\n");
    free(stdline);
  }
  if(result)
  {
    nntp_getline(que);
    report(dbglevl_none, "%s\n", que->line);
  }
  else
  {
    report_error("error : nntp_post() failed - ");
  }
  return result;
}

//-----------------------------------------------------------------------------

static int nntp_get_list(newsque *que)
{
  int result = 0;

  report(dbglevl_none, "%s\n", que->line);
  while(nntp_getline(que))
  {
    report(dbglevl_none, "%s\n", que->line);
    if(strcmp(que->line, ".") == 0)
    {
      result = 1;
      break;
    }
  }
  return result;
}

//-----------------------------------------------------------------------------

static int nntp_command_help(newsque *que)
{
  int result = 1;

  if(strncmp(que->line, "100 ", 4) == 0)
  {
    result = nntp_get_list(que);
  }
  else
  {
    report(dbglevl_none, "%s\n", que->line);
  }
  return result;
}

//-----------------------------------------------------------------------------

static int nntp_command_group(newsque *que)
{
  int result = 1;

  switch(que->line[2])
  {
    case '1':
      report(dbglevl_none, "%s\n", que->line);
      break;

    case '5':
      result = nntp_get_list(que);
      break;

    default:
      report(dbglevl_none, "unrecognized group command : %s\n", &que->line);
      break;
  }
  return result;
}

//-----------------------------------------------------------------------------

static int nntp_command_article(newsque *que)
{
  int result = 1;

  switch(que->line[2])
  {
    case '0':
    case '1':
    case '2':
    case '4':
      result = nntp_get_list(que);
      break;

    case '3':
      report(dbglevl_none, "%s\n", que->line);
      break;

    default:
      report(dbglevl_none, "unrecognized article command : %s\n", &que->line);
      break;
  }
  return result;
}

//-----------------------------------------------------------------------------

static int nntp_command_post(newsque *que)
{
  int result = 1;

  switch(que->line[2])
  {
    case '0':
      result = nntp_post(que);
      break;

    default:
      report(dbglevl_none, "unrecognized post command : %s\n", &que->line);
      break;
  }
  return result;
}

//-----------------------------------------------------------------------------

static int nntp_command_new(newsque *que)
{
  int result = 1;

  switch(que->line[2])
  {
    case '0':
    case '1':
      result = nntp_get_list(que);
      break;

    default:
      report(dbglevl_none, "unrecognized new command : %s\n", &que->line);
      break;
  }
  return result;
}

//-----------------------------------------------------------------------------

static int nntp_command(newsque *que)
{
  int result = 1;

  switch(que->line[1])
  {
    case '0':
      report(dbglevl_none, "%s\n", que->line);
      break;

    case '1':
      result = nntp_command_group(que);
      break;

    case '2':
      result = nntp_command_article(que);
      break;

    case '3':
      result = nntp_command_new(que);
      break;

    default:
      report(dbglevl_none, "unrecognized command : %s\n", &que->line);
      break;
  }
  return result;
}

//-----------------------------------------------------------------------------

static int nntp_command_continue(newsque *que)
{
  int result = 1;

  switch(que->line[1])
  {
    case '4':
      result = nntp_command_post(que);
      break;

    default:
      report(dbglevl_none, "unrecognized command : %s\n", &que->line);
      break;
  }
  return result;
}

//-----------------------------------------------------------------------------

static void interpret(int sock)
{
  int result = 0;
  int n;
  size_t size = 0;
  char *line = NULL;

  pnewsque que = malloc(sizeof(struct nntpque));
  if(que != NULL)
  {
    que->point  = 0;
    que->queued = 0;
    que->sock = sock;

    while(nntp_getline(que))
    {
      switch(que->line[0])
      {
        case '1':
	  result= nntp_command_help(que);
	  break;

        case '2':
	  result = nntp_command(que);
	  break;

        case '3':
	  result = nntp_command_continue(que);
	  break;

        case '4':
        case '5':
	  report(dbglevl_none, "%s\n", que->line);
	  break;

        default:
	  report(dbglevl_none, "unrecognized server response : %s\n", &que->line);
	  break;
      }
      // Catch interpreting error...
      if(result == 0)
      {
	break;
      }
      // Catch if the client "quit" (only graceful exit).
      if(strncmp(que->line, "205 ", 4) == 0)
      {
	break;
      }
      getline((char**)&line, &size, stdin);
      n = strlen(line);
      if(n)
      {
	// Replace trailing "\n\0" with "\r\n".
	line[n - 1] = '\r';
	line[n] = '\n';
	result = send(sock, line, n + 1, 0);
	if(result <= 0)
	{
	  report_error("error : Connection broken - ");
	  break;
	}
      }
    }
    if(line)
    {
      report(dbglevl_msg, "freeing stdin line buffer.\n");
      free(line);
    }
    report(dbglevl_msg, "freeing news que.\n");
    free(que);
  }
}

//-----------------------------------------------------------------------------

int main(int argc, char **argv)
{
  int i;
  int x;
  int rtn = 0;

  int sock;
  int port;
  struct hostent *server;
  struct sockaddr_in saddr;

  report_init(stdout);
  if(argc == 3)
  {
    port = atoi(argv[2]);
    report(dbglevl_none, "client started host=%s port=%i...\n", argv[1], port);
    sock = socket(AF_INET, SOCK_STREAM, 0);
    report(dbglevl_none, "opened socket = %i.\n", sock);
    if(sock > 0)
    {
      server = gethostbyname(argv[1]);
      if(server != NULL)
      {
	report(dbglevl_none, "gethostbyname(%s) returned...\n", argv[1]);
	report(dbglevl_none, " name    : %s.\n", server->h_name);
	for(i = 0; server->h_aliases[i] != NULL; i++)
	{
	  report(dbglevl_none, "  alias  : %s.\n", server->h_aliases[i]);
	}
	report(dbglevl_none, " type    : %i.\n", server->h_addrtype);
	report(dbglevl_none, " length  : %i.\n", server->h_length);
	//for(x = 0; x < server->h_length / server->h_length; x++)
	for(x = 0; server->h_addr_list[x]; x++)
	{
	  report(dbglevl_none, " address : %s.\n", inet_ntoa(*(struct in_addr*)(server->h_addr_list[x])));
	}
	saddr.sin_family = AF_INET;
	saddr.sin_port = htons(port);

	// FIXME: ipv6?
	bcopy((char*)server->h_addr, (char*)&saddr.sin_addr.s_addr, server->h_length);

	report(dbglevl_none, "connecting %s (%s)...\n", inet_ntoa(saddr.sin_addr), server->h_name);
	if(connect(sock, (struct sockaddr*)&saddr, sizeof(saddr)) != -1)
	{
	  interpret(sock);
	}
	else
	{ 
	  report_error("error : connect() failed - ");
	}
      }
      else
      {
	report_error("error : gethostbyname(%s) failed - ", argv[1]);
      }
      close(sock);
      report(dbglevl_none, "closed socket = %i.\n", sock);
    }
    else
    {
      report_error("error : socket() returned %i - ", sock);
    }
  }
  else
  {
    report(dbglevl_none, "usage %s hostname port.\n", argv[0]);
  }
  return rtn;
}

//-----------------------------------------------------------------------------
// end: client.c

  

The Code Demonstrates The Following Techniques...

3128