captcha.cc

/*
	C++ Arbeitsgemeinschaft AG
	Captcha.cc
	C++ Implementation eines CAPTCHA unter Verwendung der GD-Library
	(c) 2009 Gottfried Rudorfer
	rudorfer@a1.net
*/	
	
#include <iostream>
#include <iomanip>
#include <vector>
#include <string>
using namespace std;
#include "ReadParse.h"

extern "C" {
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/file.h> // für flock()
#include <errno.h> // für perror()
#include "gd.h" // LIBGB Grafikheader
#include "gdfonts.h"
#include "gdfontl.h"
#include "time.h"
#include "math.h" // M_PI
#include <sys/types.h> //opendir
#include <dirent.h> //opendir
}

#define X 200 // Größe der Grafik in Pixel
#define Y 150

// Web-Page-Strings
static char h_captcha_id[] = "captcha_id";
static char h_captcha_answer[] = "captcha_antwort";
static char h_url[] = "http://rudorfer.homedns.org/cgi-bin/captcha.cgi";

// Verzeichnisse und Dateien
static char captchafile[] = "/tmp/captcha.txt";
static char fontdir[] = "/usr/local/share/ttf"; /* User supplied font dir */

// Erzeuege einen ausgefülltes 2D-Objekt mit p Eckpunkten
void gdImageFilledCircle(gdImagePtr im, int x, int y, int r, int color, int p) {
	gdPoint *points;
	points = (gdPoint*) malloc(sizeof(gdPoint) * p);
	if (!points)
		return;
	for (int c=0; c < p; c++) {
		points[c].x = (int) (x + r * sin(2 * M_PI * c / p));
		points[c].y = (int) (y + r * cos(2 * M_PI * c / p));
	}	
	gdImageFilledPolygon(im, points, p, color); 
	free(points);
}

int min(int a, int b) {
	int r;
	if (a < b) 
		r = a;
	else 
		r = b;
	return r;
}

int max(int a, int b) {
	int r;
	if (a > b) 
		r = a;
	else 
		r = b;
	return r;
}

// Erzeuge eine GIF-Datei und gebe sie auf STDOUT aus
void createImage(string secret)
{
  gdImagePtr im,il;
  FILE *out; 
  int num=2+rand()%4;
  int maxr = min(X,Y)/2;
  int numcol = max(num,3);
  int col[2]; 
  int p[5][3];
  int star=rand()%5;

  numcol = min(numcol,5);


  // Grafik erzeugen
  im=gdImageCreate(X,Y);

  col[0]=gdImageColorAllocate(im,255,255,255);
  col[1]=gdImageColorAllocate(im,0,0,0);
  gdImageColorTransparent(im,col[0]);

  // Erzeuge gefüllte Kreise
  for(int c=0; c<num; c++) {
	int x=1+rand()%X;
	int y=1+rand()%Y;
	int r=1+rand()%maxr;
	int s=3+rand()%8;
        int rc = gdImageColorAllocate(im,rand()%255,rand()%255,rand()%255);
  	gdImageFilledCircle(im,x,y,r,rc,s);
	p[c][0]=x;
	p[c][1]=y;
	p[c][2]=r;
  }

	// Erzeuge zufällig erstellte Linien
	for(int d=0; d<num; d++) {
		if ( (p[star][2] < abs(p[star][0]-p[d][0])) && (p[star][2] < abs(p[star][1]-p[d][1]))   ) {
		  int size = 1+rand()%4;
		  il=gdImageCreate(size,size);
  		  gdImageColorAllocate(il,0,0,0);   	
		  gdImageSetBrush(im,il); 
		  gdImageLine(im, p[star][0],  p[star][1], p[d][0],  p[d][1], gdBrushed); 
		}
	}
	
	// Erzeuge willkürlich verstreute Buchstaben
	int bf[8];
	int xp = X / secret.length();
	double sz = 30.0;
	char cZ[3];
	char *err;
	int x = (int)sz;
	int y = (int)sz;

	typedef vector<string> VectorOfStrings;
  	VectorOfStrings vTTF;       

	DIR *dh;
	struct dirent *de;
	dh = opendir(fontdir);
	while(de = readdir(dh)) {
		string file = de->d_name;
		int loc = file.rfind(".ttf", file.length()); 		
		int locB = file.rfind(".TTF", file.length()); 
		if ((loc != string::npos) || (locB != string::npos)) {
  			vTTF.push_back(file); // Erzeuge einen Vektor von Truetype-Fontnamen, die sich im Verezeichnis "fontdir" befinden
		}
	}
	closedir(dh);

	for (int ci=0; ci<secret.length(); ci++) {
		cZ[0] = secret[ci];
		cZ[1] = '.';
		cZ[2] = '\n';
		double an = M_PI * rand()/RAND_MAX;
		int indexFont = rand()%vTTF.size();
		string pathFont = fontdir;
		pathFont += "/" + vTTF[indexFont];

		x += (int)sz/1.5;
		y += (int)sz/3;
		
		int Fcolor = gdImageColorResolve(im, rand()%255,rand()%255,rand()%255);
		err = gdImageStringFT(im,&bf[0],Fcolor,(char*)pathFont.c_str(),sz,an,x,y,cZ); // Erzeuge zufällig plazierten Buchstaben
		if (err) {
			fprintf(stderr,err);
		}
	}
	  
	out = stdout;
	printf("Content-type: image/gif\n\n"); // zuerst HTML-Header für GIF-Grafik ausgeben
	gdImageGif(im,out); // Grafik ausgeben
	fclose(out);
}

// Ausgabe der Eingabemaske    
void print_userform(string id, int len)
{
  cout << "<img border=0 src=\"" << h_url << "?mode=gif&" << h_captcha_id << "=" << id << "\"><BR>" << endl
	   << "Bitte geben Sie die im obigen Bild gezeigten " << len << " Buchstaben und Ziffern ein! <BR>" << endl
	   << "Achtung: Die Buchstaben sind wild durcheinander gewürfelt. Als Hilfe befindet sich hinter jedem Buchstaben ein \".\"<BR>" << endl
	   << "Achten Sie auf die Groß- und Kleinschreibung!<BR>" << endl
       << "<FORM METHOD=\"POST\" ACTION=\"" << h_url << "\">" << '\n'
       << "<INPUT type=hidden name=\"" << h_captcha_id << "\" VALUE=\"" << id << "\">" << '\n'
       << "<INPUT type=text name=\"" << h_captcha_answer << "\">" << '\n'
       << "<INPUT type=submit NAME=\"Validierungsantwort\" VALUE=\"Antwort\">" << '\n'
       << "</FORM>" << '\n'
	   << "<A HREF=\"" << h_url << "\">Neues Bild erzeugen lassen...</A><BR>" << endl
       << "<A HREF=\"http://rudorfer.homedns.org/cppag\">Zurück zur Homepage der C++ AG</A>" << endl;
}

// Erzeuge Captcha-ID und Captcha-Geheim-ID und lege sie in der Datei captchafile ab.
string gen_captcha_id(int len) {
	string letters = "abcdefghjkmnpqrstxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789"; // Lasse i1IloO0 wegen vertauschungsgefahr weg
	string id = "";
	string secret = "";
	char letter;
  ofstream ofile;
  int fd;


	int l = letters.length();
	for (int i = 0; i<10; i++) {
		id += letters[random() %l];
	}

	int i = 0;
	while (i < len) {
		letter = letters[random() %l];
		int found = 0;
		for (int j = 0; j < secret.length(); j++) { // Wenn das Zeichen nochmals zufällig gewhählt wird, dann soll dieses ignoriert werden 
			if (letter == secret[j]) {
				found = 1;
				break;
			}
		}
		
		if (!found) {
			secret += letter;
			i++;
		}
	}

  // fFile-locking ist bei CGI-Programmen immer notwendig.
  // Wir verwenden eine eigene Lock-Datei
  string lockfile = (string)captchafile + ".lck";
  if (-1 == (fd = open(lockfile.c_str(), O_WRONLY | O_CREAT, S_IWUSR | S_IRUSR))) {
        perror(lockfile.c_str());
        exit(1);
  }
  // Beim Lesen schreiben: Exklusiver lock
  if ((flock(fd, LOCK_EX)) == -1) {
      perror(lockfile.c_str());
      exit(1);
  }

  ofile.open(captchafile, ios::out | ios::app);
  if (ofile) {
	ofile << id << " " << secret << endl;
  }
  ofile.close();

  if ((flock(fd, LOCK_UN)) == -1) {
    perror(lockfile.c_str());
    exit(1);
  }
  close(fd);

  return id;
}

int main(int argc, char **argv)
{
  struct sigaction sa; 
  sa.sa_handler = SIG_IGN;
  sigaction(SIGINT, &sa, NULL);
  sigaction(SIGHUP, &sa, NULL);
  sigaction(SIGQUIT, &sa, NULL);
  sigaction(SIGTERM, &sa, NULL); 

  ifstream ifile;
  string id="", secret="";
  int fd;

  alarm(10); // maximale Zeit, die dieser Prozess benötigen darf    
  srand(time(NULL));
  
  ReadParse mycgi;

  if (!(mycgi.in.find("mode") == mycgi.in.end()) && (mycgi.in[h_captcha_id] != "") ) { // captcha  Bild ausgeben für die angegebene h_captcha_id ausgeben

	  // Mittels  h_captcha_id die Captcha-Secret-ID aus der Datei "captchafile" auslesen
	  
	  // File-locking ist bei CGI-Programmen immer notwendig.
	  // Wir verwenden eine eigene Lock-Datei
	  string lockfile = (string)captchafile + ".lck";
	  if (-1 == (fd = open(lockfile.c_str(), O_WRONLY | O_CREAT, S_IWUSR | S_IRUSR))) {
			perror(lockfile.c_str());
			exit(1);
	  }
	  // Beim Lesen schreiben: Exklusiver lock
	  if ((flock(fd, LOCK_SH)) == -1) {
		  perror(lockfile.c_str());
		  exit(1);
	  }

	  ifile.open(captchafile, ios::in);
	  while(ifile) {
		string s;
		ifile >> id;
		ifile >> s;
		
		if (id == mycgi.in[h_captcha_id]) {
			secret = s;
			break;
		}
	  }
	  ifile.close();

	  if ((flock(fd, LOCK_UN)) == -1) {
		perror(lockfile.c_str());
		exit(1);
	  }
	  close(fd);
	  
	  // Erzeugen des GIF-Image 
	  createImage(secret);
  } else if ((mycgi.in.find(h_captcha_id) == mycgi.in.end()) || (mycgi.in[h_captcha_id] == "")) { // Startseite ausgeben
    mycgi.PrintHeader();
	int numChars = 4+rand()%3;
    string captcha_id = gen_captcha_id(numChars);
    print_userform(captcha_id, numChars); // Zuerst muß die ID ausgegeben werden. 
  } else if ((mycgi.in[h_captcha_id] != "") && (mycgi.in[h_captcha_answer] != "")) { // Benutzereingabe auswerten
	  int fd;
	  mycgi.PrintHeader();

	  // Mittels  h_captcha_id die Captcha-Secret-ID aus der Datei "captchafile" auslesen
	  
	  // File-locking ist bei CGI-Programmen immer notwendig.
	  // Wir verwenden eine eigene Lock-Datei
	  string lockfile = (string)captchafile + ".lck";
	  if (-1 == (fd = open(lockfile.c_str(), O_WRONLY | O_CREAT, S_IWUSR | S_IRUSR))) {
			perror(lockfile.c_str());
			exit(1);
	  }
	  // Beim Lesen schreiben: Exklusiver lock
	  if ((flock(fd, LOCK_SH)) == -1) {
		  perror(lockfile.c_str());
		  exit(1);
	  }

	  ifile.open(captchafile, ios::in);
	  while(ifile) {
		string s;
		ifile >> id;
		ifile >> s;
		
		if (id == mycgi.in[h_captcha_id]) {
			secret = s;
			break;
		}
	  }
	  ifile.close();

	  if ((flock(fd, LOCK_UN)) == -1) {
		perror(lockfile.c_str());
		exit(1);
	  }
	  close(fd);
	  
	  // Ergebnis ausgeben
	  cout << "Entered secret: \"" << mycgi.in[h_captcha_answer] << "\" <p>" << endl;
	  cout << "Actual secret: \"" << secret << "\" <p>" << endl;
	  
	  // Vergleich des vom Benutzer eingegebenen String  "mycgi.in[h_captcha_answer]"  mit
	  // "secret", wobei wir die Reihenfolge der Buchstaben ignorieren wollen
	  // Wir ignorieren "."-Eingaben durch den Benutzer
	  int checkedAnswer = 0;
	  int answerNotOk = 0;
	  string g_answer = mycgi.in[h_captcha_answer];
	  for (int i=0; i<g_answer.length(); i++) {
		char answer_char = g_answer[i];
		int foundLetter = 0;
		for (int j=0; j<secret.length(); j++) {
			if (answer_char == secret[j]) {
				checkedAnswer++;
				foundLetter = 1;
				break;
			}
		}
		
		// wenn keine Übereinstimmung und kein ".", war die Antwort nicht richtig
		if (!foundLetter) {
			if (answer_char != '.') {
				answerNotOk++; // Anzahl der Zeichen, die der Benutzer falsch eingegeben hat. Wir ignorieren den "."
			}
		}
	}
	  
	  // für eine richtige Antwort muss ebenso die Anzahl der eingegebenen Zeichen der Länge der Captcha-Secret-ID entsprechen
	  if ((checkedAnswer == secret.length()) && (!answerNotOk))  {
		cout << "Okay <p>" << endl;
	  } else {
		cout << "Not okay <p>" << endl;
	  }
  } else {
	mycgi.PrintHeader();
	cout << "Error" << endl;
  }
  return 0;
}

Makefile

# defines
CC=g++
CXX=g++
CXXFLAGS=-g -Igd-2.0.33
CFLAGS=-g -Igd-2.0.33
#LDFLAGS=-static -s
LDFLAGS=-s
LIBS=-Lgd-2.0.33/.libs -lm -lgd -lfreetype -lpthread
BIN=captcha.cgi
BINDIR=/var/html/cgi-bin



# Programs
SRC=	captcha.cc ReadParse.cc


OBJS=	captcha.o ReadParse.o

all:	${BIN}


clean:	
		-@rm *.o *.dat *~ 2> /dev/null
		@echo "The filesystem thanks you for your kindness"

copy:
		-mkdir ${HOME}/pub
		cp * ${HOME}/pub
		echo "done"


# dependencies

# linking
${BIN}:	${OBJS} ${SRC}
		${CC} ${LDFLAGS} ${OBJS} -o ${BIN} ${LIBS}


install: ${BIN}
	@if [ ! -d ${BINDIR} ]; \
	then \
	echo "Erzeuge Verzeichnis ${BINDIR}..."; \
	mkdir -p ${BINDIR}; \
	fi; \
	strip ${BIN} && \
	cp -p ${BIN} ${BINDIR}/${BIN} && \
	chmod 4711 ${BINDIR}/${BIN} && \
	echo "Die Programm-Datei wurde in das Verzeichnis ${BINDIR} kopiert."; 

ReadParse.h

//******************************************************************************
// ReadParse.h
// Copyright 1999 by Gottfried Rudorfer
//
// E-Mail: Gottfried.Rudorfer@ai.wu-wien.ac.at
//
// All rights reserved.
//******************************************************************************

// Documentation: ***************************************************************
// VARIABLES:
// in .... map of key/value pairs
//******************************************************************************


#include <iostream>
#include <fstream>
#include <string>
#include <map>
#include <new>

using namespace std;

extern "C"
{
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <ctype.h>
}

// characters that we accept (according to CERT)
const char _ok_chars[] = "abcdefghijklmnopqrstuvwxyz\
ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890_-+.@%=&";

typedef map<string,string> StringStringMap;

void CgiNoSpace();

class ReadParse 
{
public:
  ReadParse();
  ~ReadParse();
  void PrintHeader();
  string EvalString(const string);
  string EvalFile(const string);
  void Mygetenv(char*);
  StringStringMap in;
  int strnicmp(const char *s1, const char *s2, const int count);
  int HexToChar(char *strptr, char &thechar);
private:
  bool header;
  void Parse();
  char *_html_string;
  int _has_parsed;
  string Line(const string& text, const string::size_type pos);
  char *_env_string;
  char *CertFix(char*); 
};






ReadParse.cc

/*****************************************************************************
// Simple ReadParse.cc for CGI-Programming.
// Distribute freely for non-commercial use.
//
// E-Mail: Gottfried.Rudorfer@wu-wien.ac.at
//
// Bugfixes: 
//   01/98 integrated CERT security fix.
//         
// (c) 1999 by Gottfried Rudorfer
// All rights reserved.
//*****************************************************************************

// Documentation: *************************************************************
// 1. Evaluates the strings passed by the GET and POST method. Uses a STL-Map.
// An Example: This code expands Key1=Value1&Key3&Key2=Value2 pairs or singles
// into the map "in".
/*
#include <iostream>
using namespace std;
#include "ReadParse.h"

int main()
{
  ReadParse mycgi;
  mycgi.PrintHeader();
  map<string,string>::iterator pos;
  for(pos=mycgi.in.begin();  pos != mycgi.in.end(); ++pos) {
    cout << "Name: <B>" << pos->first << "</B>,"
         << "Value: " << pos->second << "<BR>" << endl; 
    cout << "<HR>\n";
  }
}
*/
// 2. The method EvalFile(string) opens the file named in string
// and parses the contents of that file for $vars. If such a string is
// found, it is replaced with the value behind the key stored in the map "in".
// You may have more than one variable substiution i.e.:
// <INPUT type=hidden name="$h_user " value="$$h_user ">
//*****************************************************************************

#include"ReadParse.h"

ReadParse::ReadParse() 
{
  _env_string = _html_string = NULL;
  _has_parsed = 0;
  Parse();
  header = false;
}

ReadParse::~ReadParse() 
{
  if (_html_string)
    delete _html_string;
  if (_env_string)
    delete _env_string;
}


void ReadParse::PrintHeader()
{
  if (!header) {
        header = true;
        cout << "Content-type: text/html\n" << endl;
  }
}

int ReadParse::strnicmp(const char *s1, const char *s2, const int count) {
  char a='\0', b='\0';
  
  for(int n=0; n<count; n++)
  {
    a = toupper(*s1++);
    b = toupper(*s2++);
    if ((a=='\0')||(b=='\0')||(a!=b))
      break;
  }
  return(a - b);
}

char *ReadParse::CertFix(char *s) 
{
  // security fix see ftp.cert.org
  for (char *cp = s; *(cp += strspn(cp, _ok_chars)); /* */)
    *cp = '_';
  return s;
}

void ReadParse::Mygetenv(char *s)
{
  char *strptr;

  if (_env_string) {
    delete _env_string;
    _env_string = NULL;
  }
    
  if ((strptr=getenv(s)))
  {
    _env_string = new char [strlen(strptr)+1];
    strcpy(_env_string, strptr);
  } 
}

//*****************************************************************************

void CgiNoSpace()
{ 
  cerr << "Not enough free memory; "
       << "new failed" <<endl;
  exit(1);  
}

int ReadParse::HexToChar(char *strptr, char &thechar)
{

  char value=0, c, w[]={1, 16};
  
  for(int j=1;j>=0;j--)
  {
    c=*++strptr; //skip %
    if ((c >= '0') && (c <= '9'))
    {
      value+=w[j]*(c-'0');
    }
    else if ((c >= 'a') && (c <= 'f'))
    {
      value+=w[j]*(10+(c-'a'));
    }
    else if ((c >= 'A') && (c <= 'F'))
    {
      value+=w[j]*(10+c-'A');
    }
    else
      return 0;
  }
  thechar=value;
  return 1;
}

void ReadParse::Parse()
{
  
  int html_length;
  set_new_handler(CgiNoSpace);
  char *sg = NULL, *sp = NULL;
  
  if(_has_parsed)
  {
    cerr << "Don't Call ReadParse twice !!!!\n";
    return;
  }
  _has_parsed=1;
	
  Mygetenv("QUERY_STRING");
  if (_env_string) {
    int l = strlen(_env_string);
    sg=new char[l+2];
    strcpy(sg,_env_string);
  }
    
  Mygetenv("CONTENT_LENGTH");
  if (_env_string) {
    int l = atoi(_env_string);
    if (l > 0) {
      sp=new char[l+2];
      cin.get(sp,l+1);
    }
  }

  if(!sg && !sp)
  {
    cerr << "ReadParse: Hey boy ... we understand METHOD GET & POST\n";
    cerr << "ReadParse: Define at least the environment variable QUERY_STRING\n";
    
    exit(1);
  }
  
  html_length = 0;
  if (sg) 
    html_length += strlen(sg);
  if (sp) 
    html_length += strlen(sp);
  if(sg && sp)
    html_length++; // '&'

  _html_string = new char [html_length+2];
  _html_string[0] = '\0';
  if (sg) {
    strcat(_html_string, sg);
    delete sg;
  }
  if (sp) {
    if (_html_string[0] != '\0')
      strcat(_html_string, "&");
    strcat(_html_string, sp);
    delete sp;
  }
  
  int rows=html_length+1, cols=2, html_bufcount[] = {0,0}, readparse_flag=0;
  char *ptr, *html_arr=new char[rows*cols], c;
  html_length=strlen(_html_string);
  ptr=_html_string;
  while (true)
    {
      switch(*ptr)
        {
          case '=':
            readparse_flag=1;
            break;
          case '&':
          case '\0':
            *(html_arr+html_bufcount[0])='\0';
            *(html_arr+rows+html_bufcount[1])='\0';
            in[(string)CertFix(html_arr)] = (string)CertFix(html_arr+rows);
            if (*ptr == '\0')
              return;
            readparse_flag=0;
            html_bufcount[0] = 0;       
            html_bufcount[1] = 0;
            break;
          case '%':
            if (strlen(ptr) >=3)
              {
                if (HexToChar(ptr, c))
                  {
                    *(html_arr+readparse_flag*rows+html_bufcount[readparse_flag]++)=c;
                    ptr+=2;
                    break;
                  }
              }
          case '+':
            *ptr=' ';
          default:
            *(html_arr+readparse_flag*rows+html_bufcount[readparse_flag]++) = *ptr;
            break;
        }
      ptr++;
    }
  delete html_arr;
}

string ReadParse::Line(const string& text, const string::size_type pos)
{
  string::size_type nl_pos, cpos = 0;
  unsigned int line=1;
  char buf[10];
  
  while((cpos < pos) && (cpos != string::npos)) {
    nl_pos = text.find_first_of("\n", cpos);
    if (nl_pos == string::npos)
      break;
    if (nl_pos < pos) {
      line++;
      cpos = nl_pos + 1;
    } else
      break;
  }

  sprintf(buf, "%d", line);
  return string(buf);
}


string ReadParse::EvalString(const string text) 
{
  string::size_type dollar, space;
  const string trennzeichen(" \t\n");
  string neu="";
  unsigned int numsub;
  
  for (string::size_type cpos=0; cpos != string::npos; ) {
    dollar = text.find_first_of("$", cpos);
    numsub = 1;
    if (dollar != string::npos) {
      neu += text.substr(cpos, dollar-cpos);
      space=text.find_first_of(trennzeichen,dollar);
      while(text.at(dollar+numsub) == '$') 
        numsub++;
      string name=text.substr(dollar+numsub,space-dollar-numsub);
      
      if (space != string::npos)
        cpos = space + 1;
      else
        cpos = space;

      string subs="";
      bool ok = false;
      
      for(unsigned int j = 0; j < numsub; j++) {
        subs += name + " -> ";
        if ((in.find(name) != in.end()) && (name.length() > 0)) {
          name = in[name];
          ok = true;
        } else {
          neu += " Fehler: Variable \'" + name
            + "\' wurde nicht in der Zeile " + Line(text, dollar)
            + " gefunden! Die Ableitung ist: " + subs + ".";
          ok = false;
          break;
        }
      }
      if (ok)
        neu += name;
    } else {
      neu += text.substr(cpos, dollar-cpos);
      cpos = dollar;
    }
  }
  return neu;
}

string ReadParse::EvalFile(const string file) 
{
  ifstream in;
  string r="", s;
  
  in.open(file.c_str(), ios::in);
  if (!in) {
    cout << "Fehler beim Lesen der Datei \"" << file << "\"" << endl;
  } else {
    while(getline(in, s))
      r += s + '\n';
    in.close();
  }
  return EvalString(r);
}