Check-in [b4a29fac93]
Not logged in
Overview

SHA1 Hash:b4a29fac93a027384bf30d0a9acb851dae03a367
Date: 2009-08-10 02:29:14
User: drh
Comment:Add an ascii-art captcha for anonymous login.
Timelines: ancestors | descendants | both | trunk
Other Links: files | ZIP archive | manifest

Tags And Properties
Changes
[hide diffs]

Added src/captcha.c version [0fc3d0dfef]

@@ -1,1 +1,289 @@
+/*
+** Copyright (c) 2009 D. Richard Hipp
+**
+** This program is free software; you can redistribute it and/or
+** modify it under the terms of the GNU General Public
+** License version 2 as published by the Free Software Foundation.
+**
+** This program 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.
+**
+** You should have received a copy of the GNU General Public
+** License along with this library; if not, write to the
+** Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+** Boston, MA  02111-1307, USA.
+**
+** Author contact information:
+**   drh@hwaci.com
+**   http://www.hwaci.com/drh/
+**
+*******************************************************************************
+**
+** This file contains code to a simple text-based CAPTCHA.  Though eaily
+** defeated by a sophisticated attacker, this CAPTCHA does at least make
+** scripting attacks more difficult.
+*/
+#include <assert.h>
+#include "config.h"
+#include "captcha.h"
+
+#if INTERFACE
+#define CAPTCHA 2  /* Which captcha rendering to use */
+#endif
+
+/*
+** Convert a hex digit into a value between 0 and 15
+*/
+static int hexValue(char c){
+  if( c>='0' && c<='9' ){
+    return c - '0';
+  }else if( c>='a' && c<='f' ){
+    return c - 'a' + 10;
+  }else if( c>='A' && c<='F' ){
+    return c - 'A' + 10;
+  }else{
+    return 0;
+  }
+}
+
+#if CAPTCHA==1
+/*
+** A 4x6 pixel bitmap font for hexadecimal digits
+*/
+static const unsigned int aFont1[] = {
+  0x699996,
+  0x262227,
+  0x69124f,
+  0xf16196,
+  0x26af22,
+  0xf8e196,
+  0x68e996,
+  0xf12244,
+  0x696996,
+  0x699716,
+  0x699f99,
+  0xe9e99e,
+  0x698896,
+  0xe9999e,
+  0xf8e88f,
+  0xf8e888,
+};
+
+/*
+** Render a 32-bit unsigned integer as an 8-digit ascii-art hex number.
+** Space to hold the result is obtained from malloc() and should be freed
+** by the caller.
+*/
+char *captcha_render(const char *zPw){
+  char *z = malloc( 500 );
+  int i, j, k, m;
+
+  k = 0;
+  for(i=0; i<6; i++){
+    for(j=0; j<8; j++){
+      unsigned char v = hexValue(zPw[j]);
+      v = (aFont1[v] >> ((5-i)*4)) & 0xf;
+      for(m=8; m>=1; m = m>>1){
+        if( v & m ){
+          z[k++] = 'X';
+          z[k++] = 'X';
+        }else{
+          z[k++] = ' ';
+          z[k++] = ' ';
+        }
+      }
+      z[k++] = ' ';
+      z[k++] = ' ';
+    }
+    z[k++] = '\n';
+  }
+  z[k] = 0;
+  return z;
+}
+#endif /* CAPTCHA==1 */
+
+
+#if CAPTCHA==2
+static const char *azFont2[] = {
+ /* 0 */
+ "  __  ",
+ " /  \\ ",
+ "| () |",
+ " \\__/ ",
+
+ /* 1 */
+ " _ ",
+ "/ |",
+ "| |",
+ "|_|",
+
+ /* 2 */
+ " ___ ",
+ "|_  )",
+ " / / ",
+ "/___|",
+
+ /* 3 */
+ " ____",
+ "|__ /",
+ " |_ \\",
+ "|___/",
+
+ /* 4 */
+ " _ _  ",
+ "| | | ",
+ "|_  _|",
+ "  |_| ",
+
+ /* 5 */
+ " ___ ",
+ "| __|",
+ "|__ \\",
+ "|___/",
+
+ /* 6 */
+ "  __ ",
+ " / / ",
+ "/ _ \\",
+ "\\___/",
+
+ /* 7 */
+ " ____ ",
+ "|__  |",
+ "  / / ",
+ " /_/  ",
+
+ /* 8 */
+ " ___ ",
+ "( _ )",
+ "/ _ \\",
+ "\\___/",
+
+ /* 9 */
+ " ___ ",
+ "/ _ \\",
+ "\\_, /",
+ " /_/ ",
+
+ /* A */
+ "      ",
+ "  /\\  ",
+ " /  \\ ",
+ "/_/\\_\\",
+
+ /* B */
+ " ___ ",
+ "| _ )",
+ "| _ \\",
+ "|___/",
+
+ /* C */
+ "  ___ ",
+ " / __|",
+ "| (__ ",
+ " \\___|",
+
+ /* D */
+ " ___  ",
+ "|   \\ ",
+ "| |) |",
+ "|___/ ",
+
+ /* E */
+ " ___ ",
+ "| __|",
+ "| _| ",
+ "|___|",
+
+ /* F */
+ " ___ ",
+ "| __|",
+ "| _| ",
+ "|_|  ",
+};
+
+/*
+** Render a 32-bit unsigned integer as an 8-digit ascii-art hex number.
+** Space to hold the result is obtained from malloc() and should be freed
+** by the caller.
+*/
+char *captcha_render(const char *zPw){
+  char *z = malloc( 300 );
+  int i, j, k, m;
+  const char *zChar;
+
+  k = 0;
+  for(i=0; i<4; i++){
+    for(j=0; j<8; j++){
+      unsigned char v = hexValue(zPw[j]);
+      zChar = azFont2[4*v + i];
+      for(m=0; zChar[m]; m++){
+        z[k++] = zChar[m];
+      }
+    }
+    z[k++] = '\n';
+  }
+  z[k] = 0;
+  return z;
+}
+#endif /* CAPTCHA==2 */
+
+/*
+** COMMAND: test-captcha
+*/
+void test_captcha(void){
+  int i;
+  unsigned int v;
+  char *z;
+
+  for(i=2; i<g.argc; i++){
+    char zHex[30];
+    v = (unsigned int)atoi(g.argv[i]);
+    sprintf(zHex, "%x", v);
+    z = captcha_render(zHex);
+    printf("%s:\n%s", zHex, z);
+    free(z);
+  }
+}
+
+/*
+** Compute a seed value for a captcha.  The seed is public and is sent
+** has a hidden parameter with the page that contains the captcha.  Knowledge
+** of the seed is insufficient for determining the captcha without additional
+** information held only on the server and never revealed.
+*/
+unsigned int captcha_seed(void){
+  unsigned int x;
+  sqlite3_randomness(sizeof(x), &x);
+  x &= 0x7fffffff;
+  return x;
+}
+
+/*
+** Translate a captcha seed value into the captcha password string.
+*/
+char *captcha_decode(unsigned int seed){
+  const char *zSecret;
+  const char *z;
+  Blob b;
+  static char zRes[20];
 
+  zSecret = db_get("captcha-secret", 0);
+  if( zSecret==0 ){
+    db_multi_exec(
+      "REPLACE INTO config(name,value)"
+      " VALUES('captcha-secret', lower(hex(randomblob(20))));"
+    );
+    zSecret = db_get("captcha-secret", 0);
+    assert( zSecret!=0 );
+  }
+  blob_init(&b, 0, 0);
+  blob_appendf(&b, "%s-%x", zSecret, seed);
+  sha1sum_blob(&b, &b);
+  z = blob_buffer(&b);
+  memcpy(zRes, z, 8);
+  zRes[8] = 0;
+  return zRes;
+}

Modified src/login.c from [f3badebb6b] to [5c89b898ce].

@@ -79,10 +79,34 @@
     fossil_redirect_home();
   }
 }
 
 /*
+** Check to see if the anonymous login is valid.  If it is valid, return
+** the userid of the anonymous user.
+*/
+static int isValidAnonymousLogin(
+  const char *zUsername,  /* The username.  Must be "anonymous" */
+  const char *zPassword   /* The supplied password */
+){
+  const char *zCS;        /* The captcha seed value */
+  const char *zPw;        /* The correct password shown in the captcha */
+  int uid;                /* The user ID of anonymous */
+
+  if( zUsername==0 ) return 0;
+  if( zPassword==0 ) return 0;
+  if( strcmp(zUsername,"anonymous")!=0 ) return 0;
+  zCS = P("cs");   /* The "cs" parameter is the "captcha seed" */
+  if( zCS==0 ) return 0;
+  zPw = captcha_decode((unsigned int)atoi(zCS));
+  if( strcasecmp(zPw, zPassword)!=0 ) return 0;
+  uid = db_int(0, "SELECT uid FROM user WHERE login='anonymous'"
+                  " AND length(pw)>0 AND length(cap)>0");
+  return uid;
+}
+
+/*
 ** WEBPAGE: /login
 ** WEBPAGE: /logout
 **
 ** Generate the login page
 */
@@ -90,10 +114,11 @@
   const char *zUsername, *zPasswd;
   const char *zNew1, *zNew2;
   const char *zAnonPw = 0;
   int anonFlag;
   char *zErrMsg = "";
+  int uid;                     /* User id loged in user */
 
   login_check_credentials();
   zUsername = P("u");
   zPasswd = P("p");
   anonFlag = P("anon")!=0;
@@ -125,12 +150,32 @@
       );
       redirect_to_g();
       return;
     }
   }
+  uid = isValidAnonymousLogin(zUsername, zPasswd);
+  if( uid>0 ){
+    char *zNow;                  /* Current time (julian day number) */
+    const char *zIpAddr;         /* IP address of requestor */
+    char *zCookie;               /* The login cookie */
+    const char *zCookieName;     /* Name of the login cookie */
+    Blob b;                      /* Blob used during cookie construction */
+
+    zIpAddr = PD("REMOTE_ADDR","nil");
+    zCookieName = login_cookie_name();
+    zNow = db_text("0", "SELECT julianday('now')");
+    blob_init(&b, zNow, -1);
+    blob_appendf(&b, "/%s/%s", zIpAddr, db_get("captcha-secret",""));
+    sha1sum_blob(&b, &b);
+    zCookie = sqlite3_mprintf("anon/%s/%s", zNow, blob_buffer(&b));
+    blob_reset(&b);
+    free(zNow);
+    cgi_set_cookie(zCookieName, zCookie, 0, 6*3600);
+    redirect_to_g();
+  }
   if( zUsername!=0 && zPasswd!=0 && zPasswd[0]!=0 ){
-    int uid = db_int(0,
+    uid = db_int(0,
         "SELECT uid FROM user"
         " WHERE login=%Q AND pw=%Q", zUsername, zPasswd);
     if( uid<=0 || strcmp(zUsername,"nobody")==0 ){
       sleep(1);
       zErrMsg =
@@ -143,21 +188,17 @@
       const char *zCookieName = login_cookie_name();
       const char *zExpire = db_get("cookie-expire","8766");
       int expires = atoi(zExpire)*3600;
       const char *zIpAddr = PD("REMOTE_ADDR","nil");
 
-      if( strcmp(zUsername, "anonymous")==0 ){
-        cgi_set_cookie(zCookieName, "anonymous", 0, expires);
-      }else{
-        zCookie = db_text(0, "SELECT '%d/' || hex(randomblob(25))", uid);
-        cgi_set_cookie(zCookieName, zCookie, 0, expires);
-        db_multi_exec(
-          "UPDATE user SET cookie=%Q, ipaddr=%Q, "
-          "  cexpire=julianday('now')+%d/86400.0 WHERE uid=%d",
-          zCookie, zIpAddr, expires, uid
-        );
-      }
+      zCookie = db_text(0, "SELECT '%d/' || hex(randomblob(25))", uid);
+      cgi_set_cookie(zCookieName, zCookie, 0, expires);
+      db_multi_exec(
+        "UPDATE user SET cookie=%Q, ipaddr=%Q, "
+        "  cexpire=julianday('now')+%d/86400.0 WHERE uid=%d",
+        zCookie, zIpAddr, expires, uid
+      );
       redirect_to_g();
     }
   }
   style_header("Login/Logout");
   @ %s(zErrMsg)
@@ -180,15 +221,10 @@
   @ </tr>
   if( g.zLogin==0 ){
     zAnonPw = db_text(0, "SELECT pw FROM user"
                          " WHERE login='anonymous'"
                          "   AND cap!=''");
-    if( zAnonPw && anonFlag ){
-      @ <tr><td></td>
-      @ <td>The anonymous password is "<b>%h(zAnonPw)</b>".</td>
-      @ </tr>
-    }
   }
   @ <tr>
   @   <td></td>
   @   <td><input type="submit" name="in" value="Login"></td>
   @ </tr>
@@ -201,17 +237,21 @@
   }
   @ enter the user-id and password at the left and press the
   @ "Login" button.  Your user name will be stored in a browser cookie.
   @ You must configure your web browser to accept cookies in order for
   @ the login to take.</p>
-  if( g.zLogin==0 ){
-    if( zAnonPw && !anonFlag ){
-      @ <p>The password for user "anonymous" is "<b>%h(zAnonPw)</b>".</p>
-      @ <p>&nbsp;</p>
-    }else{
-      @ <p>&nbsp;</p><p>&nbsp;</p>
-    }
+  if( zAnonPw ){
+    unsigned int uSeed = captcha_seed();
+    char *zCaptcha = captcha_render(captcha_decode(uSeed));
+
+    @ <input type="hidden" name="cs" value="%u(uSeed)">
+    @ <p>To login as user <b>anonymous</b> use the following
+    @ 8-character hexadecimal password:</p>
+    @ <center><table border="1" cellpadding="10"><tr><td><pre>
+    @ %s(zCaptcha)
+    @ </pre></td></tr></table></center>
+    free(zCaptcha);
   }
   if( g.zLogin ){
     @ <br clear="both"><hr>
     @ <p>To log off the system (and delete your login cookie)
     @  press the following button:<br>
@@ -278,20 +318,44 @@
 
   /* Check the login cookie to see if it matches a known valid user.
   */
   if( uid==0 && (zCookie = P(login_cookie_name()))!=0 ){
     if( isdigit(zCookie[0]) ){
+      /* Cookies of the form "uid/randomness".  There must be a
+      ** corresponding entry in the user table. */
       uid = db_int(0,
             "SELECT uid FROM user"
             " WHERE uid=%d"
             "   AND cookie=%Q"
             "   AND ipaddr=%Q"
             "   AND cexpire>julianday('now')",
             atoi(zCookie), zCookie, zRemoteAddr
          );
-    }else if( zCookie[0]=='a' ){
-      uid = db_int(0, "SELECT uid FROM user WHERE login='anonymous'");
+    }else if( memcmp(zCookie,"anon/",5)==0 ){
+      /* Cookies of the form "anon/TIME/HASH".  The TIME must not be
+      ** too old and the sha1 hash of TIME+IPADDR+SECRET must match HASH.
+      ** SECRET is the "captcha-secret" value in the repository.
+      */
+      double rTime;
+      int i;
+      Blob b;
+      rTime = atof(&zCookie[5]);
+      for(i=5; zCookie[i] && zCookie[i]!='/'; i++){}
+      blob_init(&b, &zCookie[5], i-5);
+      if( zCookie[i]=='/' ){ i++; }
+      blob_append(&b, "/", 1);
+      blob_appendf(&b, "%s/%s", zRemoteAddr, db_get("captcha-secret",""));
+      sha1sum_blob(&b, &b);
+      uid = db_int(0,
+          "SELECT uid FROM user WHERE login='anonymous'"
+          " AND length(cap)>0"
+          " AND length(pw)>0"
+          " AND %f+0.25>julianday('now')"
+          " AND %Q=%Q",
+          rTime, &zCookie[i], blob_buffer(&b)
+      );
+      blob_reset(&b);
     }
     sqlite3_snprintf(sizeof(g.zCsrfToken), g.zCsrfToken, "%.10s", zCookie);
   }
 
   if( uid==0 ){
@@ -474,12 +538,12 @@
 }
 
 /*
 ** Before using the results of a form, first call this routine to verify
 ** that ths Anti-CSRF token is present and is valid.  If the Anti-CSRF token
-** is missing or is incorrect, then this emits and error message and never
-** returns.
+** is missing or is incorrect, that indicates a cross-site scripting attach
+** so emits an error message and abort.
 */
 void login_verify_csrf_secret(void){
   const char *zCsrf;            /* The CSRF secret */
   if( g.okCsrf ) return;
   if( (zCsrf = P("csrf"))!=0 && strcmp(zCsrf, g.zCsrfToken)==0 ){

Modified src/main.mk from [9bd0c8cb97] to [c4a82e07db].

@@ -18,10 +18,11 @@
   $(SRCDIR)/allrepo.c \
   $(SRCDIR)/bag.c \
   $(SRCDIR)/blob.c \
   $(SRCDIR)/branch.c \
   $(SRCDIR)/browse.c \
+  $(SRCDIR)/captcha.c \
   $(SRCDIR)/cgi.c \
   $(SRCDIR)/checkin.c \
   $(SRCDIR)/checkout.c \
   $(SRCDIR)/clearsign.c \
   $(SRCDIR)/clone.c \
@@ -88,10 +89,11 @@
   allrepo_.c \
   bag_.c \
   blob_.c \
   branch_.c \
   browse_.c \
+  captcha_.c \
   cgi_.c \
   checkin_.c \
   checkout_.c \
   clearsign_.c \
   clone_.c \
@@ -158,10 +160,11 @@
   allrepo.o \
   bag.o \
   blob.o \
   branch.o \
   browse.o \
+  captcha.o \
   cgi.o \
   checkin.o \
   checkout.o \
   clearsign.o \
   clone.o \
@@ -261,16 +264,16 @@
 	# noop
 
 clean:
 	rm -f *.o *_.c $(APPNAME) VERSION.h
 	rm -f translate makeheaders mkindex page_index.h headers
-	rm -f add.h admin.h allrepo.h bag.h blob.h branch.h browse.h cgi.h checkin.h checkout.h clearsign.h clone.h comformat.h configure.h construct.h content.h db.h delta.h deltacmd.h descendants.h diff.h diffcmd.h doc.h encode.h file.h http.h http_socket.h http_transport.h info.h login.h main.h manifest.h md5.h merge.h merge3.h my_page.h name.h pivot.h pqueue.h printf.h rebuild.h report.h rss.h rstats.h schema.h setup.h sha1.h shun.h stat.h style.h sync.h tag.h tagview.h th_main.h timeline.h tkt.h tktsetup.h undo.h update.h url.h user.h verify.h vfile.h wiki.h wikiformat.h winhttp.h xfer.h zip.h
+	rm -f add.h admin.h allrepo.h bag.h blob.h branch.h browse.h captcha.h cgi.h checkin.h checkout.h clearsign.h clone.h comformat.h configure.h construct.h content.h db.h delta.h deltacmd.h descendants.h diff.h diffcmd.h doc.h encode.h file.h http.h http_socket.h http_transport.h info.h login.h main.h manifest.h md5.h merge.h merge3.h my_page.h name.h pivot.h pqueue.h printf.h rebuild.h report.h rss.h rstats.h schema.h setup.h sha1.h shun.h stat.h style.h sync.h tag.h tagview.h th_main.h timeline.h tkt.h tktsetup.h undo.h update.h url.h user.h verify.h vfile.h wiki.h wikiformat.h winhttp.h xfer.h zip.h
 
 page_index.h: $(TRANS_SRC) mkindex
 	./mkindex $(TRANS_SRC) >$@
 headers:	page_index.h makeheaders VERSION.h
-	./makeheaders  add_.c:add.h admin_.c:admin.h allrepo_.c:allrepo.h bag_.c:bag.h blob_.c:blob.h branch_.c:branch.h browse_.c:browse.h cgi_.c:cgi.h checkin_.c:checkin.h checkout_.c:checkout.h clearsign_.c:clearsign.h clone_.c:clone.h comformat_.c:comformat.h configure_.c:configure.h construct_.c:construct.h content_.c:content.h db_.c:db.h delta_.c:delta.h deltacmd_.c:deltacmd.h descendants_.c:descendants.h diff_.c:diff.h diffcmd_.c:diffcmd.h doc_.c:doc.h encode_.c:encode.h file_.c:file.h http_.c:http.h http_socket_.c:http_socket.h http_transport_.c:http_transport.h info_.c:info.h login_.c:login.h main_.c:main.h manifest_.c:manifest.h md5_.c:md5.h merge_.c:merge.h merge3_.c:merge3.h my_page_.c:my_page.h name_.c:name.h pivot_.c:pivot.h pqueue_.c:pqueue.h printf_.c:printf.h rebuild_.c:rebuild.h report_.c:report.h rss_.c:rss.h rstats_.c:rstats.h schema_.c:schema.h setup_.c:setup.h sha1_.c:sha1.h shun_.c:shun.h stat_.c:stat.h style_.c:style.h sync_.c:sync.h tag_.c:tag.h tagview_.c:tagview.h th_main_.c:th_main.h timeline_.c:timeline.h tkt_.c:tkt.h tktsetup_.c:tktsetup.h undo_.c:undo.h update_.c:update.h url_.c:url.h user_.c:user.h verify_.c:verify.h vfile_.c:vfile.h wiki_.c:wiki.h wikiformat_.c:wikiformat.h winhttp_.c:winhttp.h xfer_.c:xfer.h zip_.c:zip.h $(SRCDIR)/sqlite3.h $(SRCDIR)/th.h VERSION.h
+	./makeheaders  add_.c:add.h admin_.c:admin.h allrepo_.c:allrepo.h bag_.c:bag.h blob_.c:blob.h branch_.c:branch.h browse_.c:browse.h captcha_.c:captcha.h cgi_.c:cgi.h checkin_.c:checkin.h checkout_.c:checkout.h clearsign_.c:clearsign.h clone_.c:clone.h comformat_.c:comformat.h configure_.c:configure.h construct_.c:construct.h content_.c:content.h db_.c:db.h delta_.c:delta.h deltacmd_.c:deltacmd.h descendants_.c:descendants.h diff_.c:diff.h diffcmd_.c:diffcmd.h doc_.c:doc.h encode_.c:encode.h file_.c:file.h http_.c:http.h http_socket_.c:http_socket.h http_transport_.c:http_transport.h info_.c:info.h login_.c:login.h main_.c:main.h manifest_.c:manifest.h md5_.c:md5.h merge_.c:merge.h merge3_.c:merge3.h my_page_.c:my_page.h name_.c:name.h pivot_.c:pivot.h pqueue_.c:pqueue.h printf_.c:printf.h rebuild_.c:rebuild.h report_.c:report.h rss_.c:rss.h rstats_.c:rstats.h schema_.c:schema.h setup_.c:setup.h sha1_.c:sha1.h shun_.c:shun.h stat_.c:stat.h style_.c:style.h sync_.c:sync.h tag_.c:tag.h tagview_.c:tagview.h th_main_.c:th_main.h timeline_.c:timeline.h tkt_.c:tkt.h tktsetup_.c:tktsetup.h undo_.c:undo.h update_.c:update.h url_.c:url.h user_.c:user.h verify_.c:verify.h vfile_.c:vfile.h wiki_.c:wiki.h wikiformat_.c:wikiformat.h winhttp_.c:winhttp.h xfer_.c:xfer.h zip_.c:zip.h $(SRCDIR)/sqlite3.h $(SRCDIR)/th.h VERSION.h
 	touch headers
 headers: Makefile
 Makefile:
 add_.c:	$(SRCDIR)/add.c $(SRCDIR)/VERSION translate
 	./translate $(SRCDIR)/add.c | sed -f $(SRCDIR)/VERSION >add_.c
@@ -319,10 +322,17 @@
 
 browse.o:	browse_.c browse.h  $(SRCDIR)/config.h
 	$(XTCC) -o browse.o -c browse_.c
 
 browse.h:	headers
+captcha_.c:	$(SRCDIR)/captcha.c $(SRCDIR)/VERSION translate
+	./translate $(SRCDIR)/captcha.c | sed -f $(SRCDIR)/VERSION >captcha_.c
+
+captcha.o:	captcha_.c captcha.h  $(SRCDIR)/config.h
+	$(XTCC) -o captcha.o -c captcha_.c
+
+captcha.h:	headers
 cgi_.c:	$(SRCDIR)/cgi.c $(SRCDIR)/VERSION translate
 	./translate $(SRCDIR)/cgi.c | sed -f $(SRCDIR)/VERSION >cgi_.c
 
 cgi.o:	cgi_.c cgi.h  $(SRCDIR)/config.h
 	$(XTCC) -o cgi.o -c cgi_.c

Modified src/makemake.tcl from [263645557b] to [725becf5eb].

@@ -12,10 +12,11 @@
   allrepo
   bag
   blob
   branch
   browse
+  captcha
   cgi
   checkin
   checkout
   clearsign
   clone