[Freeciv-Dev] Re: (PR#13262) pubserver-in-a-diff
[Top] [All Lists]
[Date Prev][Date Next][Thread Prev][Thread Next][Date Index] [Thread Index]
Subject: |
[Freeciv-Dev] Re: (PR#13262) pubserver-in-a-diff |
From: |
"Per I. Mathisen" <per@xxxxxxxxxxx> |
Date: |
Wed, 22 Jun 2005 07:28:27 -0700 |
Reply-to: |
bugs@xxxxxxxxxxx |
<URL: http://bugs.freeciv.org/Ticket/Display.html?id=13262 >
On Mon, 13 Jun 2005, Per I. Mathisen wrote:
> This patch implements the ./configure switch --enable-pubserver.
New version. What is changed?
* scripts are read with permission level of requesting player (is
read with hack cmdlevel in cvs)
* display normal or more important freelogs even if we log to file
* pubserver only: report game # on client login
* pubserver only: allow /read, /rulesetdir and /load of scenarios;
the security measures for these should now be very tight
* when loading a game, if players have valid users associated with them,
these players will not 'ready' when they have no connection; this is to
stop cheating by loading savegames from previous games without
alerting or waiting for the other players
* '/', '\\', '$', '`' are made illegal in names (including file names
on pubserver only)
I think perhaps authentication does not need to be part of this switch.
That would make it harder to run a public server than is necessary - since
it is plausible some people would want to run public servers without
authentication.
I still need to figure out how to reap old games. Since we create a 'game
session' directory whenever a server starts, they will quickly add up. We
need to know which are 'in use' and which are not, and then find some
criterias for which games to reap and which to leave be. Games with no
savegames are obvious candidates for quick reaping, but how to know they
are not in use (by another civserver)?
- Per
Index: configure.ac
===================================================================
RCS file: /home/freeciv/CVS/freeciv/configure.ac,v
retrieving revision 1.104
diff -u -r1.104 configure.ac
--- configure.ac 8 May 2005 13:31:24 -0000 1.104
+++ configure.ac 22 Jun 2005 14:22:23 -0000
@@ -41,6 +41,17 @@
FC_CHECK_AUTH
+AC_ARG_ENABLE(pubserver,
+[ --enable-pubserver compile for public server usage],
+[case "${enableval}" in
+ yes) pubserver=true ;;
+ no) pubserver=false ;;
+ *) AC_MSG_ERROR(bad value ${enableval} for --enable-pubserver) ;;
+esac], [pubserver=false])
+if test "$pubserver" = true ; then
+ AC_DEFINE(PUBSERVER, 1, [Compile for public server usage])
+fi
+
dnl no=do not compile server, yes=compile server, *=error
AC_ARG_ENABLE(server,
[ --disable-server do not compile the server],
Index: common/game.c
===================================================================
RCS file: /home/freeciv/CVS/freeciv/common/game.c,v
retrieving revision 1.220
diff -u -r1.220 game.c
--- common/game.c 21 Jun 2005 16:21:01 -0000 1.220
+++ common/game.c 22 Jun 2005 14:22:24 -0000
@@ -233,7 +233,7 @@
game.info.cooling = 0;
game.info.watchtower_extra_vision = GAME_DEFAULT_WATCHTOWER_EXTRA_VISION;
game.info.allowed_city_names = GAME_DEFAULT_ALLOWED_CITY_NAMES;
- game.info.save_nturns = 10;
+ game.info.save_nturns = GAME_DEFAULT_SAVETURNS;
#ifdef HAVE_LIBZ
game.info.save_compress_level = GAME_DEFAULT_COMPRESS_LEVEL;
#else
Index: common/game.h
===================================================================
RCS file: /home/freeciv/CVS/freeciv/common/game.h,v
retrieving revision 1.194
diff -u -r1.194 game.h
--- common/game.h 14 Jun 2005 18:49:08 -0000 1.194
+++ common/game.h 22 Jun 2005 14:22:24 -0000
@@ -333,6 +333,11 @@
#define GAME_DEFAULT_RULESETDIR "default"
#define GAME_DEFAULT_SAVE_NAME "civgame"
+#ifdef PUBSERVER
+#define GAME_DEFAULT_SAVETURNS 1
+#else
+#define GAME_DEFAULT_SAVETURNS 10
+#endif
#define GAME_DEFAULT_SKILL_LEVEL 3 /* easy */
#define GAME_OLD_DEFAULT_SKILL_LEVEL 5 /* normal; for old save games */
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 22 Jun 2005 14:22:26 -0000
@@ -173,7 +173,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 {
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 22 Jun 2005 14:22:26 -0000
@@ -337,6 +337,16 @@
" --file <filename>\n"
"and use the 'start' command once players have reconnected.")
},
+#ifdef PUBSERVER
+ {"load", ALLOW_CTRL,
+ /* TRANS: translate text between <> only */
+ N_("load\n"
+ "load <game number | scenario name>"),
+ N_("Load a scenario or the latest savegame from a public server game."),
+ N_("Load a game or scenario from the public server. Any current data "
+ "including players, rulesets and server options are lost.\n")
+ },
+#else
{"load", ALLOW_HACK,
/* TRANS: translate text between <> only */
N_("load\n"
@@ -345,7 +355,8 @@
N_("Load a game from <file-name>. Any current data including players, "
"rulesets and server options are lost.\n")
},
- {"read", ALLOW_HACK,
+#endif
+ {"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 22 Jun 2005 14:22:26 -0000
@@ -99,6 +99,13 @@
notify_conn(dest, _("Welcome to the %s Server at port %d."),
freeciv_name_version(), srvarg.port);
}
+#ifdef PUBSERVER
+ notify_conn(dest, _("Server is running in public mode. Some "
+ "commands will be restricted."));
+ notify_conn(dest, _("This is game %d. Use this number "
+ "to load a saved game or report problems."),
+ srvarg.pubserver_serial);
+#endif
/* 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 22 Jun 2005 14:22:26 -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/settings.c
===================================================================
RCS file: /home/freeciv/CVS/freeciv/server/settings.c,v
retrieving revision 1.27
diff -u -r1.27 settings.c
--- server/settings.c 14 Jun 2005 18:49:09 -0000 1.27
+++ server/settings.c 22 Jun 2005 14:22:26 -0000
@@ -951,7 +951,7 @@
N_("Turns per auto-save"),
N_("The game will be automatically saved per this number of "
"turns. Zero means never auto-save."), NULL,
- 0, 200, 10)
+ 0, 200, GAME_DEFAULT_SAVETURNS)
/* Could undef entire option if !HAVE_LIBZ, but this way users get to see
* what they're missing out on if they didn't compile with zlib? --dwp
@@ -979,7 +979,7 @@
GAME_NO_COMPRESS_LEVEL, GAME_NO_COMPRESS_LEVEL,
GAME_NO_COMPRESS_LEVEL)
#endif
-
+#ifndef PUBSERVER
GEN_STRING("savename", game.save_name,
SSET_META, SSET_INTERNAL, SSET_VITAL, SSET_SERVER_ONLY,
N_("Auto-save name prefix"),
@@ -987,7 +987,7 @@
"\"<prefix><year>.sav\". This setting sets "
"the <prefix> part."), NULL,
GAME_DEFAULT_SAVE_NAME)
-
+#endif
GEN_BOOL("scorelog", game.scorelog,
SSET_META, SSET_INTERNAL, SSET_SITUATIONAL, SSET_SERVER_ONLY,
N_("Whether to log player statistics"),
Index: server/srv_main.c
===================================================================
RCS file: /home/freeciv/CVS/freeciv/server/srv_main.c,v
retrieving revision 1.273
diff -u -r1.273 srv_main.c
--- server/srv_main.c 21 Jun 2005 16:21:02 -0000 1.273
+++ server/srv_main.c 22 Jun 2005 14:22:27 -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>
@@ -166,6 +170,7 @@
/* init server arguments... */
+ srvarg.pubserver_serial = 0;
srvarg.metaserver_no_send = DEFAULT_META_SERVER_NO_SEND;
sz_strlcpy(srvarg.metaserver_addr, DEFAULT_META_SERVER_ADDR);
@@ -179,8 +184,13 @@
srvarg.load_filename[0] = '\0';
srvarg.script_filename = NULL;
srvarg.saves_pathname = "";
+ srvarg.final_savepath[0] = '\0';
+#ifdef PUBSERVER
+ srvarg.quitidle = 30;
+#else
srvarg.quitidle = 0;
+#endif
srvarg.auth_enabled = FALSE;
srvarg.auth_allow_guests = FALSE;
@@ -726,11 +736,15 @@
*dot = '\0';
}
+#ifndef PUBSERVER
/* 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);
}
+#else
+ sz_strlcpy(filename, game.save_name);
+#endif
timer_cpu = new_timer_start(TIMER_CPU, TIMER_ACTIVE);
timer_user = new_timer_start(TIMER_USER, TIMER_ACTIVE);
@@ -750,9 +764,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, "/");
}
@@ -760,10 +774,14 @@
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
+ }
+#ifndef PUBSERVER
+ else {
con_write(C_OK, _("Game saved as %s"), filename);
+ }
+#endif
section_file_free(&file);
@@ -1334,12 +1352,15 @@
int num_ready = 0, num_unready = 0;
players_iterate(pplayer) {
- if (pplayer->is_connected) {
- if (pplayer->is_ready) {
- num_ready++;
- } else {
- num_unready++;
- }
+ /* 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)) {
+ num_unready++;
}
} players_iterate_end;
if (num_unready > 0) {
@@ -1770,6 +1791,37 @@
/* Run server loop */
while (TRUE) {
+#ifdef PUBSERVER
+ /* Find a new empty directory number. We operate from
+ * whereever we are started. */
+ DIR *dir;
+ int i = 0;
+ char path[20], gamelogfile[40], conlogfile[40];
+
+ /* Find first unused directory */
+ dir = NULL;
+ do {
+ my_snprintf(path, sizeof(path), "%d", i);
+ dir = opendir(path);
+ if (i >= 10000) {
+ die("We either have in excess of 10k games, or a path problem. "
+ "Clean up or fix it! (%s)", path);
+ }
+ i++;
+ } while (dir != NULL);
+ make_dir(path);
+ srvarg.pubserver_serial = i;
+ sz_strlcpy(srvarg.final_savepath, path);
+ /* Redirect output to saves directory */
+ 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);
+ notify_player(NULL, _("You are playing public game %d."), i);
+#endif
+
srv_loop();
send_game_state(game.game_connections, CLIENT_GAME_OVER_STATE);
@@ -1830,7 +1882,6 @@
{
init_available_nations();
- freelog(LOG_NORMAL, _("Now accepting new client connections."));
while(server_state == PRE_GAME_STATE) {
sniff_packets(); /* Accepting commands. */
}
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 22 Jun 2005 14:22:27 -0000
@@ -22,6 +22,7 @@
BV_DEFINE(bv_draw, MAX_NUM_PLAYERS);
struct server_arguments {
+ int pubserver_serial;
/* metaserver information */
bool metaserver_no_send;
char metaserver_addr[256];
@@ -33,11 +34,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;
Index: server/stdinhand.c
===================================================================
RCS file: /home/freeciv/CVS/freeciv/server/stdinhand.c,v
retrieving revision 1.422
diff -u -r1.422 stdinhand.c
--- server/stdinhand.c 21 Jun 2005 10:17:03 -0000 1.422
+++ server/stdinhand.c 22 Jun 2005 14:22:28 -0000
@@ -755,10 +755,16 @@
**************************************************************************/
static bool save_command(struct connection *caller, char *arg, bool check)
{
+#ifdef PUBSERVER
+ cmd_reply(CMD_SAVE, caller, C_FAIL,
+ _("You cannot save games manually on this server."));
+ return FALSE;
+#else
if (!check) {
save_game(arg, "User request");
}
return TRUE;
+#endif
}
/**************************************************************************
@@ -970,22 +976,49 @@
/**************************************************************************
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)
{
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);
+#ifdef PUBSERVER
+ if (!is_ascii_name(serv_filename)) {
+ cmd_reply(CMD_READ_SCRIPT, caller, C_FAIL,
+ _("Illegal name '%s'."), serv_filename);
+ return FALSE;
+ }
+ sz_strlcpy(tilde_filename, serv_filename);
+#else
+ interpret_tilde(tilde_filename, sizeof(tilde_filename), serv_filename);
+#endif
real_filename = datafilename(tilde_filename);
if (!real_filename) {
+#ifdef PUBSERVER
+ cmd_reply(CMD_READ_SCRIPT, caller, C_FAIL,
+ _("No command script found by the name \"%s\"."),
+ serv_filename);
+ return FALSE;
+#else
+ /* File is outside data directories */
real_filename = tilde_filename;
+#endif
}
- /* 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)
@@ -993,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 {
@@ -1017,6 +1050,7 @@
return read_init_script(caller, arg);
}
+#ifndef PUBSERVER
/**************************************************************************
...
(Should this take a 'caller' argument for output? --dwp)
@@ -1098,6 +1132,7 @@
_("Could not write script file '%s'."), real_filename);
}
}
+#endif
/**************************************************************************
...
@@ -1105,10 +1140,15 @@
**************************************************************************/
static bool write_command(struct connection *caller, char *arg, bool check)
{
+#ifdef PUBSERVER
+ cmd_reply(CMD_WRITE_SCRIPT, caller, C_OK, _("You cannot write on this
server."));
+ return FALSE;
+#else
if (!check) {
write_init_script(arg);
}
return TRUE;
+#endif
}
/**************************************************************************
@@ -3135,7 +3175,37 @@
{
struct timer *loadtimer, *uloadtimer;
struct section_file file;
- char arg[strlen(filename) + 1];
+#ifdef PUBSERVER
+ char arg[512 + strlen(filename)];
+ int i;
+
+ if (!filename || filename[0] == '\0' || !is_ascii_name(filename)) {
+ cmd_reply(CMD_LOAD, caller, C_FAIL, _("Usage: load <game number>"));
+ return FALSE;
+ }
+ /* if filename is a valid number, it is a savegame */
+ if (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 scenario */
+ char tmp[256 + strlen(filename)];
+
+ my_snprintf(tmp, sizeof(tmp), "scenario/%s.sav", filename);
+ if (!datafilename(tmp)) {
+ my_snprintf(tmp, sizeof(tmp), "scenario/%s.sav.gz", filename);
+ if (!datafilename(tmp)) {
+ cmd_reply(CMD_LOAD, caller, C_FAIL, _("Cannot find scenario \"%s\"."),
+ filename);
+ return FALSE;
+ }
+ }
+ sz_strlcpy(arg, datafilename(tmp));
+ }
+#else
+ 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. */
@@ -3146,9 +3216,10 @@
send_load_game_info(FALSE);
return FALSE;
}
+#endif
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;
@@ -3157,7 +3228,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;
}
@@ -3208,7 +3279,13 @@
}
/**************************************************************************
- ...
+ 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_ascii_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)
{
@@ -3218,6 +3295,13 @@
_("Current ruleset directory is \"%s\""), game.rulesetdir);
return FALSE;
}
+#ifdef PUBSERVER
+ if (!is_ascii_name(str) || strchr(str, '.')) {
+ cmd_reply(CMD_RULESETDIR, caller, C_SYNTAX,
+ _("Illegal ruleset directory name \"%s\"."), str);
+ return FALSE;
+ }
+#endif
my_snprintf(filename, sizeof(filename), "%s", str);
pfilename = datafilename(filename);
if (!pfilename) {
Index: utility/shared.c
===================================================================
RCS file: /home/freeciv/CVS/freeciv/utility/shared.c,v
retrieving revision 1.133
diff -u -r1.133 shared.c
--- utility/shared.c 15 Jun 2005 20:23:00 -0000 1.133
+++ utility/shared.c 22 Jun 2005 14:22:28 -0000
@@ -385,7 +385,8 @@
***************************************************************/
bool is_ascii_name(const char *name)
{
- const char illegal_chars[] = {'|', '%', '"', ',', '*', '<', '>', '\0'};
+ const char illegal_chars[] = { '|', '%', '"', ',', '*', '<', '>',
+ '/', '\\', '$', '`', '\0' };
int i, j;
/* must not be NULL or empty */
- [Freeciv-Dev] Re: (PR#13262) RFC: pubserver-in-a-diff, Mike Kaufman, 2005/06/16
- [Freeciv-Dev] Re: (PR#13262) RFC: pubserver-in-a-diff, Per I. Mathisen, 2005/06/22
- [Freeciv-Dev] Re: (PR#13262) RFC: pubserver-in-a-diff, Mike Kaufman, 2005/06/22
- [Freeciv-Dev] Re: (PR#13262) pubserver-in-a-diff,
Per I. Mathisen <=
- [Freeciv-Dev] Re: (PR#13262) pubserver-in-a-diff, Mike Kaufman, 2005/06/22
- [Freeciv-Dev] Re: (PR#13262) pubserver-in-a-diff, Jason Short, 2005/06/22
- [Freeciv-Dev] Re: (PR#13262) pubserver-in-a-diff, Per I. Mathisen, 2005/06/22
- [Freeciv-Dev] Re: (PR#13262) pubserver-in-a-diff, Per I. Mathisen, 2005/06/22
- [Freeciv-Dev] Re: (PR#13262) pubserver-in-a-diff, Mike Kaufman, 2005/06/22
- [Freeciv-Dev] Re: (PR#13262) pubserver-in-a-diff, Jason Short, 2005/06/22
- [Freeciv-Dev] Re: (PR#13262) pubserver-in-a-diff, Per I. Mathisen, 2005/06/22
- [Freeciv-Dev] Re: (PR#13262) pubserver-in-a-diff, Jason Short, 2005/06/22
- [Freeciv-Dev] Re: (PR#13262) pubserver-in-a-diff, Per I. Mathisen, 2005/06/22
- [Freeciv-Dev] Re: (PR#13262) pubserver-in-a-diff, Jason Short, 2005/06/22
|
|