Complete.Org: Mailing Lists: Archives: freeciv-dev: July 2005:
[Freeciv-Dev] (PR#13262) include pubserver jobs v5
Home

[Freeciv-Dev] (PR#13262) include pubserver jobs v5

[Top] [All Lists]

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index] [Thread Index]
Subject: [Freeciv-Dev] (PR#13262) include pubserver jobs v5
From: "Per I. Mathisen" <per@xxxxxxxxxxx>
Date: Tue, 12 Jul 2005 15:16:19 -0700
Reply-to: bugs@xxxxxxxxxxx

<URL: http://bugs.freeciv.org/Ticket/Display.html?id=13262 >

This is the pubserver-in-a-diff patch. Changes from previous version:
 * requires PR#13461 and PR#13463
 * uses is_safe_name(), used for security-sensitive filenames
 * you cannot /take when you have loaded a game on pubserver
 * refactored code as requested by Jason
 * the 'valid username users count as unready' rule only applies
   for pubserver
 * leave it to pubserver to create directories
 * enabled 'load' command with ctrl cmdlevel
 * fixed a bunch of bugs through extensive testing

Since most the changes in this patch are only active when -P is used, and
can only be tested on pubserver (or a similar server), I suggest this
patch is soon committed.

I have a server running on pubserver with this patch, running on a
smaller, experimental pubserver I wrote to test this patch.

  - Per

Index: common/packets.def
===================================================================
RCS file: /home/freeciv/CVS/freeciv/common/packets.def,v
retrieving revision 1.140
diff -u -r1.140 packets.def
--- common/packets.def  4 Jul 2005 18:42:27 -0000       1.140
+++ common/packets.def  12 Jul 2005 21:52:43 -0000
@@ -343,6 +343,7 @@
   UINT32 tech;
   UINT32 skill_level;
 
+  STRING pubserver[MAX_LEN_NAME]; add-cap(pubserver)
   BOOL is_new_game;   # TRUE only in pregame for "new" (not loaded) games
   FLOAT seconds_to_phasedone;
   UINT32 timeout;
Index: server/civserver.c
===================================================================
RCS file: /home/freeciv/CVS/freeciv/server/civserver.c,v
retrieving revision 1.230
diff -u -r1.230 civserver.c
--- server/civserver.c  3 Feb 2005 08:04:55 -0000       1.230
+++ server/civserver.c  12 Jul 2005 21:52:44 -0000
@@ -105,6 +105,7 @@
 #endif
 
   /* initialize server */
+  game.info.pubserver[0] = '\0';   /* identifies it as not pubserver */
   srv_init();
 
   /* parse command-line arguments... */
@@ -145,6 +146,9 @@
       free(option);
     } else if ((option = get_option_malloc("--bind", argv, &inx, argc))) {
       srvarg.bind_addr = option; /* Never freed. */
+    } else if ((option = get_option_malloc("--Pubserver", argv, &inx, argc))) {
+      sz_strlcpy(game.info.pubserver, option);
+      free(option);
     } else if ((option = get_option_malloc("--read", argv, &inx, argc)))
       srvarg.script_filename = option; /* Never freed. */
     else if ((option = get_option_malloc("--quitidle", argv, &inx, argc))) {
@@ -173,7 +177,8 @@
       sz_strlcpy(srvarg.serverid, option);
       free(option);
     } else if ((option = get_option_malloc("--saves", argv, &inx, argc))) {
-      srvarg.saves_pathname = option; /* Never freed. */
+      srvarg.saves_pathname = option; /* Base path. Never freed. */
+      sz_strlcpy(srvarg.final_savepath, option); /* Possibly changing path. */
     } else if (is_option("--version", argv[inx]))
       showvers = TRUE;
     else {
@@ -184,6 +189,11 @@
     inx++;
   }
 
+  if (is_pubserver() && !srvarg.exit_on_end) {
+    fc_fprintf(stderr, _("Pubserver mode requires --exit-on-end\n"));
+    exit(EXIT_FAILURE);
+  }
+
   if (showvers && !showhelp) {
     fc_fprintf(stderr, "%s \n", freeciv_name_version());
     exit(EXIT_SUCCESS);
@@ -221,6 +231,8 @@
 
     fc_fprintf(stderr, _("  -p, --port PORT\tListen for clients on "
                         "port PORT\n"));
+    fc_fprintf(stderr, _("  -P, --Pubserver VAL\tSee http://www.freeciv";
+                         "/index.php/Pubserver (requires -e)\n"));
     fc_fprintf(stderr, _("  -q, --quitidle TIME\tQuit if no players "
                         "for TIME seconds\n"));
     fc_fprintf(stderr, _("  -e, --exit-on-end\t"
Index: server/commands.c
===================================================================
RCS file: /home/freeciv/CVS/freeciv/server/commands.c,v
retrieving revision 1.12
diff -u -r1.12 commands.c
--- server/commands.c   21 May 2005 19:40:25 -0000      1.12
+++ server/commands.c   12 Jul 2005 21:52:44 -0000
@@ -337,7 +337,7 @@
       "    --file <filename>\n"
       "and use the 'start' command once players have reconnected.")
   },
-  {"load",      ALLOW_HACK,
+  {"load",      ALLOW_CTRL,
    /* TRANS: translate text between <> only */
    N_("load\n"
       "load <file-name>"),
@@ -345,7 +345,7 @@
    N_("Load a game from <file-name>. Any current data including players, "
       "rulesets and server options are lost.\n")
   },
-  {"read",     ALLOW_HACK,
+  {"read",     ALLOW_CTRL,
    /* TRANS: translate text between <> only */
    N_("read <file-name>"),
    N_("Process server commands from file."), NULL
Index: server/connecthand.c
===================================================================
RCS file: /home/freeciv/CVS/freeciv/server/connecthand.c,v
retrieving revision 1.52
diff -u -r1.52 connecthand.c
--- server/connecthand.c        14 Jun 2005 18:49:09 -0000      1.52
+++ server/connecthand.c        12 Jul 2005 21:52:45 -0000
@@ -99,6 +99,13 @@
     notify_conn(dest, _("Welcome to the %s Server at port %d."),
                 freeciv_name_version(), srvarg.port);
   }
+  if (is_pubserver()) {
+    notify_conn(dest, _("Server is running in public mode. Some "
+                "commands will be restricted."));
+    notify_conn(dest, _("This is game \"%s\". Use this number "
+                "to load a saved game or report problems."), 
+               game.info.pubserver);
+  }
 
   /* FIXME: this (getting messages about others logging on) should be a 
    * message option for the client with event */
Index: server/console.c
===================================================================
RCS file: /home/freeciv/CVS/freeciv/server/console.c,v
retrieving revision 1.25
diff -u -r1.25 console.c
--- server/console.c    21 Mar 2005 23:01:12 -0000      1.25
+++ server/console.c    12 Jul 2005 21:52:45 -0000
@@ -47,9 +47,8 @@
 ************************************************************************/
 static void con_handle_log(int level, const char *message, bool file_too)
 {
-  /* Write to console only when not written to file.
-     LOG_FATAL messages always to console too. */
-  if (! file_too || level <= LOG_FATAL) {
+  /* Write debug/verbose message to console only when not written to file. */
+  if (!file_too || level <= LOG_NORMAL) {
     if (console_rfcstyle) {
       con_write(C_LOG_BASE + level, "%s", message);
     } else {
Index: server/srv_main.c
===================================================================
RCS file: /home/freeciv/CVS/freeciv/server/srv_main.c,v
retrieving revision 1.277
diff -u -r1.277 srv_main.c
--- server/srv_main.c   11 Jul 2005 19:31:13 -0000      1.277
+++ server/srv_main.c   12 Jul 2005 21:52:47 -0000
@@ -17,10 +17,14 @@
 
 #include <assert.h>
 #include <ctype.h>
+#include <dirent.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
 #include <time.h>
+#include <errno.h>
+#include <sys/stat.h>
+#include <sys/types.h>
 
 #ifdef HAVE_NETDB_H
 #include <netdb.h>
@@ -141,6 +145,14 @@
 static bool has_been_srv_init = FALSE;
 
 /**************************************************************************
+  Are we running in pubserver mode?
+**************************************************************************/
+bool is_pubserver(void)
+{
+  return (game.info.pubserver[0] != '\0');
+}
+
+/**************************************************************************
   Initialize the game seed.  This may safely be called multiple times.
 **************************************************************************/
 void init_game_seed(void)
@@ -180,6 +192,7 @@
   srvarg.load_filename[0] = '\0';
   srvarg.script_filename = NULL;
   srvarg.saves_pathname = "";
+  srvarg.final_savepath[0] = '\0';
 
   srvarg.quitidle = 0;
 
@@ -711,7 +724,7 @@
 **************************************************************************/
 void save_game(char *orig_filename, const char *save_reason)
 {
-  char filename[600];
+  char filename[600] = "civgame";
   char *dot;
   struct section_file file;
   struct timer *timer_cpu, *timer_user;
@@ -727,10 +740,16 @@
     *dot = '\0';
   }
 
-  /* If orig_filename is NULL or empty, use "civgame.info.year>m". */
-  if (filename[0] == '\0'){
-    my_snprintf(filename, sizeof(filename),
-       "%s%+05dm", game.save_name, game.info.year);
+  if (is_pubserver()) {
+    /* game.savename could have been renamed, and we need to be sure
+     * the game is not, for scripts that may want to find it */
+    sz_strlcpy(filename, "civgame"); 
+  } else {
+    /* If orig_filename is NULL or empty, use "civgame.info.year>m". */
+    if (filename[0] == '\0'){
+      my_snprintf(filename, sizeof(filename),
+       "  %s%+05dm", game.save_name, game.info.year);
+    }
   }
   
   timer_cpu = new_timer_start(TIMER_CPU, TIMER_ACTIVE);
@@ -751,9 +770,9 @@
     char tmpname[600];
 
     /* Ensure the saves directory exists. */
-    make_dir(srvarg.saves_pathname);
+    make_dir(srvarg.final_savepath);
 
-    sz_strlcpy(tmpname, srvarg.saves_pathname);
+    sz_strlcpy(tmpname, srvarg.final_savepath);
     if (tmpname[0] != '\0') {
       sz_strlcat(tmpname, "/");
     }
@@ -761,10 +780,11 @@
     sz_strlcpy(filename, tmpname);
   }
 
-  if(!section_file_save(&file, filename, game.info.save_compress_level))
+  if (!section_file_save(&file, filename, game.info.save_compress_level)) {
     con_write(C_FAIL, _("Failed saving game as %s"), filename);
-  else
+  } else {
     con_write(C_OK, _("Game saved as %s"), filename);
+  }
 
   section_file_free(&file);
 
@@ -1340,12 +1360,17 @@
     int num_ready = 0, num_unready = 0;
 
     players_iterate(pplayer) {
-      if (pplayer->is_connected) {
-       if (pplayer->is_ready) {
-         num_ready++;
-       } else {
-         num_unready++;
-       }
+      /* For pubserver:  If we load a game, some players may be assigned 
+       * to user accounts, in which case we should not start until they 
+       * have joined too.  Otherwise it would be easy to cheat.  This 
+       * will be problematic for scenarios where creators forget to 
+       * reset usernames. */
+      if (pplayer->is_ready && pplayer->is_connected) {
+       num_ready++;
+      } else if (pplayer->is_connected 
+                 || (is_valid_username(pplayer->username)
+                     && is_pubserver() > 0)) {
+       num_unready++;
       }
     } players_iterate_end;
     if (num_unready > 0) {
@@ -1721,6 +1746,36 @@
 }
 
 /**************************************************************************
+  Special public server code.  Enabled with the -P command-line option.
+**************************************************************************/
+static void pubserver_init(void)
+{
+  if (is_pubserver()) {
+    DIR *dir;
+    char path[90], gamelogfile[100], conlogfile[100];
+
+    /* Create directory */
+    my_snprintf(path, sizeof(path), "%s%s%s",
+                strlen(srvarg.saves_pathname) > 0 ? srvarg.saves_pathname : "",
+                strlen(srvarg.saves_pathname) > 0 ? "/" : "",
+                game.info.pubserver);
+    dir = opendir(path);
+    if (dir == NULL) {
+      freelog(LOG_FATAL, "Could not open directory \"%s\".", path);
+      exit(EXIT_FAILURE);
+    }
+    /* Redirect output to saves directory */
+    sz_strlcpy(srvarg.final_savepath, path);
+    my_snprintf(gamelogfile, sizeof(gamelogfile), "%s/gamelog.txt", 
+                srvarg.final_savepath);
+    my_snprintf(conlogfile, sizeof(conlogfile), "%s/cmdline.txt", 
+                srvarg.final_savepath);
+    con_log_init(conlogfile, srvarg.loglevel);
+    gamelog_init(gamelogfile);
+  }
+}
+
+/**************************************************************************
   Server initialization.
 **************************************************************************/
 void srv_main(void)
@@ -1771,12 +1826,13 @@
 
   /* load a script file */
   if (srvarg.script_filename
-      && !read_init_script(NULL, srvarg.script_filename)) {
+      && !read_init_script(NULL, srvarg.script_filename, TRUE)) {
     exit(EXIT_FAILURE);
   }
 
   /* Run server loop */
   while (TRUE) {
+    pubserver_init();
     srv_loop();
 
     send_game_state(game.game_connections, CLIENT_GAME_OVER_STATE);
Index: server/srv_main.h
===================================================================
RCS file: /home/freeciv/CVS/freeciv/server/srv_main.h,v
retrieving revision 1.35
diff -u -r1.35 srv_main.h
--- server/srv_main.h   21 Jun 2005 10:17:03 -0000      1.35
+++ server/srv_main.h   12 Jul 2005 21:52:47 -0000
@@ -33,11 +33,12 @@
   /* the log level */
   int loglevel;
   /* filenames */
-  char *log_filename;
-  char *gamelog_filename;
+  char *log_filename;      /* ignored by pubserver */
+  char *gamelog_filename;  /* ignored by pubserver */
   char load_filename[512]; /* FIXME: may not be long enough? use MAX_PATH? */
   char *script_filename;
-  char *saves_pathname;
+  char *saves_pathname;     /* base path; used by pubserver for all output */
+  char final_savepath[512]; /* may be changed by pubserver code */
   char serverid[256];
   /* quit if there no players after a given time interval */
   int quitidle;
@@ -78,5 +79,7 @@
 
 extern bool force_end_of_sniff;
 
+bool is_pubserver(void);
+
 void init_available_nations(void);
 #endif /* FC__SRV_MAIN_H */
Index: server/stdinhand.c
===================================================================
RCS file: /home/freeciv/CVS/freeciv/server/stdinhand.c,v
retrieving revision 1.426
diff -u -r1.426 stdinhand.c
--- server/stdinhand.c  7 Jul 2005 08:34:40 -0000       1.426
+++ server/stdinhand.c  12 Jul 2005 21:52:50 -0000
@@ -115,6 +115,16 @@
 Returns whether the specified server setting (option) should be
 sent to the client.
 *********************************************************************/
+static bool restricted_filepaths(struct connection *caller)
+{
+  return (is_pubserver()
+          || (caller && caller->access_level != ALLOW_HACK));
+}
+
+/********************************************************************
+Returns whether the specified server setting (option) should be
+sent to the client.
+*********************************************************************/
 static bool sset_is_to_client(int idx)
 {
   return (settings[idx].to_client == SSET_TO_CLIENT);
@@ -744,6 +754,11 @@
 **************************************************************************/
 static bool save_command(struct connection *caller, char *arg, bool check)
 {
+  if (restricted_filepaths(caller)) {
+    cmd_reply(CMD_SAVE, caller, C_FAIL,
+              _("You cannot save games manually on this server."));
+    return FALSE;
+  }
   if (!check) {
     save_game(arg, "User request");
   }
@@ -959,22 +974,51 @@
 
 /**************************************************************************
   Returns FALSE iff there was an error.
+
+  Security: We will look for a file with mandatory extension '.serv',
+  and on public servers we will not look outside the data directories.
+  As long as the user cannot create files with arbitrary names in the
+  root of the data directories, this should ensure that we will not be 
+  tricked into loading non-approved content. The script is read with the 
+  permissions of the caller, so it will in any case not lead to elevated
+  permissions unless there are other bugs.
 **************************************************************************/
-bool read_init_script(struct connection *caller, char *script_filename)
+bool read_init_script(struct connection *caller, char *script_filename,
+                      bool from_cmdline)
 {
   FILE *script_file;
+  const char extension[] = ".serv";
+  char serv_filename[strlen(extension) + strlen(script_filename) + 2];
   char tilde_filename[4096];
   char *real_filename;
 
-  interpret_tilde(tilde_filename, sizeof(tilde_filename), script_filename);
+  my_snprintf(serv_filename, sizeof(serv_filename), "%s%s", 
+              script_filename, extension);
+
+  if (restricted_filepaths(caller) && !from_cmdline) {
+    if (!is_safe_name(script_filename)) {
+      cmd_reply(CMD_READ_SCRIPT, caller, C_FAIL,
+                _("Name \"%s\" disallowed for security reasons."), 
+                serv_filename);
+      return FALSE;
+    }
+    sz_strlcpy(tilde_filename, serv_filename);
+  } else {
+    interpret_tilde(tilde_filename, sizeof(tilde_filename), serv_filename);
+  }
 
   real_filename = datafilename(tilde_filename);
   if (!real_filename) {
+    if (restricted_filepaths(caller) && !from_cmdline) {
+      cmd_reply(CMD_READ_SCRIPT, caller, C_FAIL,
+                _("No command script found by the name \"%s\"."), 
+                serv_filename);
+      return FALSE;
+    }
+    /* File is outside data directories */
     real_filename = tilde_filename;
   }
 
-  /* This used to print out the script_filename, but it seems more useful
-   * to show the real_filename. */
   freelog(LOG_NORMAL, _("Loading script file: %s"), real_filename);
 
   if (is_reg_file_for_access(real_filename, FALSE)
@@ -982,7 +1026,7 @@
     char buffer[MAX_LEN_CONSOLE_LINE];
     /* the size is set as to not overflow buffer in handle_stdin_input */
     while(fgets(buffer,MAX_LEN_CONSOLE_LINE-1,script_file))
-      handle_stdin_input((struct connection *)NULL, buffer, FALSE);
+      handle_stdin_input(caller, buffer, FALSE);
     fclose(script_file);
     return TRUE;
   } else {
@@ -1003,7 +1047,7 @@
     return TRUE; /* FIXME: no actual checks done */
   }
   /* warning: there is no recursion check! */
-  return read_init_script(caller, arg);
+  return read_init_script(caller, arg, FALSE);
 }
 
 /**************************************************************************
@@ -1094,7 +1138,11 @@
 **************************************************************************/
 static bool write_command(struct connection *caller, char *arg, bool check)
 {
-  if (!check) {
+  if (restricted_filepaths(caller)) {
+    cmd_reply(CMD_WRITE_SCRIPT, caller, C_OK, _("You cannot use the write "
+              "command on this server for security reasons."));
+    return FALSE;
+  } else if (!check) {
     write_init_script(arg);
   }
   return TRUE;
@@ -2880,6 +2928,14 @@
     goto end;
   } 
 
+  if (!game.info.is_new_game
+      && is_valid_username(pplayer->username)
+      && is_pubserver()) {
+    cmd_reply(CMD_TAKE, caller, C_FAIL,
+              _("You cannot take over other players in a loaded game."));
+    goto end;
+  }
+
   res = TRUE;
   if (check) {
     goto end;
@@ -3143,20 +3199,52 @@
 {
   struct timer *loadtimer, *uloadtimer;  
   struct section_file file;
-  char arg[strlen(filename) + 1];
-
-  /* We make a local copy because the parameter might be a pointer to 
-   * srvarg.load_filename, which we edit down below. */
-  sz_strlcpy(arg, filename);
+  char arg[512 + strlen(filename)];
+  int i;
 
-  if (!arg || arg[0] == '\0') {
-    cmd_reply(CMD_LOAD, caller, C_FAIL, _("Usage: load <filename>"));
-    send_load_game_info(FALSE);
+  if (!filename || filename[0] == '\0') {
+    cmd_reply(CMD_LOAD, caller, C_FAIL, _("Usage: load <game name>"));
+    return FALSE;
+  }
+  if (restricted_filepaths(caller) && !is_safe_name(filename)) {
+    cmd_reply(CMD_LOAD, caller, C_FAIL, _("Name \"%s\" disallowed for "
+              "security reasons."), filename);
     return FALSE;
   }
+  /* if filename is a valid number, it may be a pubserver savegame */
+  if (is_pubserver() && sscanf(filename, "%d", &i) == 1) {
+    my_snprintf(arg, sizeof(arg), "%s%s%s/civgame.sav%s", 
+                srvarg.saves_pathname, 
+                srvarg.saves_pathname[0] == '\0' ? "" : "/",
+                filename, game.info.save_compress_level > 0 ? ".gz" : "");
+  } else {
+    /* otherwise, it is a normal savegame or maybe a scenario */
+    char tmp[256 + strlen(filename)];
+
+    my_snprintf(tmp, sizeof(tmp), "%s.sav", filename);
+    if (!datafilename(tmp)) {
+      my_snprintf(tmp, sizeof(tmp), "%s.sav.gz", filename);
+      if (!datafilename(tmp)) {
+        my_snprintf(tmp, sizeof(tmp), "scenario/%s.sav", filename);
+        if (!datafilename(tmp)) {
+          my_snprintf(tmp, sizeof(tmp), "scenario/%s.sav.gz", filename);
+          if (restricted_filepaths(caller) && !datafilename(tmp)) {
+            cmd_reply(CMD_LOAD, caller, C_FAIL, _("Cannot find savegame or "
+                      "scenario with the name \"%s\"."), filename);
+            return FALSE;
+          }
+        }
+      }
+    }
+    if (datafilename(tmp)) {
+      sz_strlcpy(arg, datafilename(tmp));
+    } else {
+      sz_strlcpy(arg, filename);
+    }
+  }
 
   if (server_state != PRE_GAME_STATE) {
-    cmd_reply(CMD_LOAD, caller, C_FAIL, _("Can't load a game while another "
+    cmd_reply(CMD_LOAD, caller, C_FAIL, _("Cannot load a game while another "
                                           "is running."));
     send_load_game_info(FALSE);
     return FALSE;
@@ -3165,7 +3253,7 @@
   /* attempt to parse the file */
 
   if (!section_file_load_nodup(&file, arg)) {
-    cmd_reply(CMD_LOAD, caller, C_FAIL, _("Couldn't load savefile: %s"), arg);
+    cmd_reply(CMD_LOAD, caller, C_FAIL, _("Could not load savefile: %s"), arg);
     send_load_game_info(FALSE);
     return FALSE;
   }
@@ -3216,16 +3304,30 @@
 }
 
 /**************************************************************************
-  ...
+  Load rulesets from a given ruleset directory.
+
+  Security: There are some rudimentary checks in load_rulesets() to see
+  if this directory realls is a viable ruleset directory. For public
+  servers, we check against directory redirection (is_safe_name) and
+  other bad stuff in the directory name, and will only use directories
+  inside the data directories.
 **************************************************************************/
 static bool set_rulesetdir(struct connection *caller, char *str, bool check)
 {
   char filename[512], *pfilename;
+
   if ((str == NULL) || (strlen(str)==0)) {
     cmd_reply(CMD_RULESETDIR, caller, C_SYNTAX,
              _("Current ruleset directory is \"%s\""), game.rulesetdir);
     return FALSE;
   }
+  if (restricted_filepaths(caller)
+      && (!is_safe_name(str) || strchr(str, '.'))) {
+    cmd_reply(CMD_RULESETDIR, caller, C_SYNTAX,
+             _("Ruleset directory name \"%s\" disallowed for security "
+               "reasons."), str);
+    return FALSE;
+  }  
   my_snprintf(filename, sizeof(filename), "%s", str);
   pfilename = datafilename(filename);
   if (!pfilename) {
Index: server/stdinhand.h
===================================================================
RCS file: /home/freeciv/CVS/freeciv/server/stdinhand.h,v
retrieving revision 1.30
diff -u -r1.30 stdinhand.h
--- server/stdinhand.h  9 Dec 2004 16:38:35 -0000       1.30
+++ server/stdinhand.h  12 Jul 2005 21:52:50 -0000
@@ -28,7 +28,8 @@
 void report_settable_server_options(struct connection *dest, int which);
 void set_ai_level_direct(struct player *pplayer, int level);
 void set_ai_level_directer(struct player *pplayer, int level);
-bool read_init_script(struct connection *caller, char *script_filename);
+bool read_init_script(struct connection *caller, char *script_filename,
+                      bool from_cmdline);
 void show_players(struct connection *caller);
 
 bool load_command(struct connection *caller, char *arg, bool check);

[Prev in Thread] Current Thread [Next in Thread]