Commit | Line | Data |
---|---|---|
2f1de3dd CW |
1 | /* update-game-score.c --- Update a score file |
2 | Copyright (C) 2002 Free Software Foundation, Inc. | |
3 | ||
4 | This file is part of GNU Emacs. | |
5 | ||
6 | GNU Emacs is free software; you can redistribute it and/or modify | |
7 | it under the terms of the GNU General Public License as published by | |
8 | the Free Software Foundation; either version 2, or (at your option) | |
9 | any later version. | |
10 | ||
11 | GNU Emacs is distributed in the hope that it will be useful, | |
12 | but WITHOUT ANY WARRANTY; without even the implied warranty of | |
13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
14 | GNU General Public License for more details. | |
15 | ||
16 | You should have received a copy of the GNU General Public License | |
17 | along with GNU Emacs; see the file COPYING. If not, write to | |
18 | the Free Software Foundation, Inc., 59 Temple Place - Suite 330, | |
19 | Boston, MA 02111-1307, USA. */ | |
20 | ||
21 | /* This program is allows a game to securely and atomically update a | |
8eec804f CW |
22 | score file. It should be installed setuid, owned by an appropriate |
23 | user like `games'. | |
24 | ||
25 | Alternatively, it can be compiled without HAVE_SHARED_GAME_DIR | |
26 | defined, and in that case it will store scores in the user's home | |
27 | directory (it should NOT be setuid). | |
2f1de3dd CW |
28 | |
29 | Created 2002/03/22, by Colin Walters <walters@debian.org> | |
30 | */ | |
31 | ||
32 | #define _GNU_SOURCE | |
33 | ||
c42d6dbd EZ |
34 | #include <config.h> |
35 | ||
2f1de3dd CW |
36 | #include <unistd.h> |
37 | #include <errno.h> | |
38 | #include <string.h> | |
39 | #include <stdlib.h> | |
40 | #include <stdio.h> | |
41 | #include <time.h> | |
cd553ffb | 42 | #include <pwd.h> |
2f1de3dd CW |
43 | #include <ctype.h> |
44 | #include <fcntl.h> | |
8eec804f | 45 | #include <stdarg.h> |
2f1de3dd | 46 | #include <sys/stat.h> |
2f1de3dd CW |
47 | |
48 | #define MAX_ATTEMPTS 5 | |
8eec804f CW |
49 | #define MAX_SCORES 200 |
50 | #define MAX_DATA_LEN 1024 | |
5795b420 | 51 | |
8eec804f CW |
52 | #if !defined (__GNUC__) || __GNUC__ < 2 |
53 | #define __attribute__(x) | |
5795b420 | 54 | #endif |
2f1de3dd | 55 | |
cf398788 CW |
56 | /* Declare the prototype for a general external function. */ |
57 | #if defined (PROTOTYPES) || defined (WINDOWSNT) | |
58 | #define P_(proto) proto | |
59 | #else | |
60 | #define P_(proto) () | |
61 | #endif | |
62 | ||
2f1de3dd | 63 | int |
cf398788 CW |
64 | usage(err) |
65 | int err; | |
2f1de3dd CW |
66 | { |
67 | fprintf(stdout, "Usage: update-game-score [-m MAX ] [ -r ] game/scorefile SCORE DATA\n"); | |
68 | fprintf(stdout, " update-game-score -h\n"); | |
69 | fprintf(stdout, " -h\t\tDisplay this help.\n"); | |
70 | fprintf(stdout, " -m MAX\t\tLimit the maximum number of scores to MAX.\n"); | |
71 | fprintf(stdout, " -r\t\tSort the scores in increasing order.\n"); | |
d99fabf0 | 72 | fprintf(stdout, " -d DIR\t\tStore scores in DIR (only if not setuid).\n"); |
2f1de3dd CW |
73 | exit(err); |
74 | } | |
75 | ||
76 | int | |
cf398788 | 77 | lock_file P_((const char *filename, void **state)); |
2f1de3dd | 78 | int |
cf398788 | 79 | unlock_file P_((const char *filename, void *state)); |
2f1de3dd CW |
80 | |
81 | struct score_entry | |
82 | { | |
83 | long score; | |
cd553ffb | 84 | char *username; |
2f1de3dd CW |
85 | char *data; |
86 | }; | |
87 | ||
88 | int | |
cf398788 CW |
89 | read_scores P_((const char *filename, struct score_entry **scores, |
90 | int *count)); | |
2f1de3dd | 91 | int |
cf398788 CW |
92 | push_score P_((struct score_entry **scores, int *count, |
93 | int newscore, char *username, char *newdata)); | |
2f1de3dd | 94 | void |
cf398788 | 95 | sort_scores P_((struct score_entry *scores, int count, int reverse)); |
2f1de3dd | 96 | int |
cf398788 CW |
97 | write_scores P_((const char *filename, const struct score_entry *scores, |
98 | int count)); | |
99 | ||
100 | void lose P_((const char *msg)) | |
101 | __attribute__ ((noreturn)); | |
102 | ||
103 | void lose(msg) | |
104 | const char *msg; | |
105 | { | |
106 | fprintf(stderr, "%s\n", msg); | |
107 | exit(1); | |
108 | } | |
2f1de3dd | 109 | |
cf398788 CW |
110 | void lose_syserr P_((const char *msg)) |
111 | __attribute__ ((noreturn)); | |
d99fabf0 | 112 | |
cf398788 CW |
113 | void lose_syserr(msg) |
114 | const char *msg; | |
d99fabf0 | 115 | { |
cf398788 CW |
116 | fprintf(stderr, "%s: %s\n", msg, strerror(errno)); |
117 | exit(1); | |
d99fabf0 CW |
118 | } |
119 | ||
cd553ffb | 120 | char * |
d99fabf0 | 121 | get_user_id(void) |
cd553ffb CW |
122 | { |
123 | char *name; | |
d99fabf0 | 124 | struct passwd *buf = getpwuid(getuid()); |
cd553ffb CW |
125 | if (!buf) |
126 | { | |
127 | int count = 1; | |
128 | int uid = (int) getuid(); | |
8eec804f CW |
129 | int tuid = uid; |
130 | while (tuid /= 10) | |
cd553ffb CW |
131 | count++; |
132 | name = malloc(count+1); | |
d99fabf0 CW |
133 | if (!name) |
134 | return NULL; | |
cd553ffb CW |
135 | sprintf(name, "%d", uid); |
136 | return name; | |
137 | } | |
138 | return buf->pw_name; | |
139 | } | |
140 | ||
5795b420 | 141 | char * |
cf398788 CW |
142 | get_prefix(running_suid, user_prefix) |
143 | int running_suid; | |
144 | char *user_prefix; | |
8eec804f | 145 | { |
d99fabf0 | 146 | if (!running_suid && user_prefix == NULL) |
cf398788 | 147 | lose("Not using a shared game directory, and no prefix given."); |
d99fabf0 CW |
148 | if (running_suid) |
149 | { | |
150 | #ifdef HAVE_SHARED_GAME_DIR | |
151 | return HAVE_SHARED_GAME_DIR; | |
152 | #else | |
cf398788 | 153 | lose("This program was compiled without HAVE_SHARED_GAME_DIR,\n and should not be suid."); |
d99fabf0 CW |
154 | #endif |
155 | } | |
156 | return user_prefix; | |
8eec804f CW |
157 | } |
158 | ||
2f1de3dd | 159 | int |
cf398788 CW |
160 | main(argc, argv) |
161 | int argc; | |
162 | char **argv; | |
2f1de3dd | 163 | { |
d99fabf0 | 164 | int c, running_suid; |
2f1de3dd | 165 | void *lockstate; |
d99fabf0 | 166 | char *user_id, *scorefile, *prefix, *user_prefix = NULL; |
2f1de3dd CW |
167 | struct stat buf; |
168 | struct score_entry *scores; | |
8eec804f | 169 | int newscore, scorecount, reverse = 0, max = MAX_SCORES; |
2f1de3dd CW |
170 | char *newdata; |
171 | ||
172 | srand(time(0)); | |
173 | ||
d99fabf0 | 174 | while ((c = getopt(argc, argv, "hrm:d:")) != -1) |
2f1de3dd CW |
175 | switch (c) |
176 | { | |
177 | case 'h': | |
178 | usage(0); | |
179 | break; | |
d99fabf0 CW |
180 | case 'd': |
181 | user_prefix = optarg; | |
182 | break; | |
2f1de3dd CW |
183 | case 'r': |
184 | reverse = 1; | |
185 | break; | |
186 | case 'm': | |
187 | max = atoi(optarg); | |
8eec804f CW |
188 | if (max > MAX_SCORES) |
189 | max = MAX_SCORES; | |
2f1de3dd CW |
190 | break; |
191 | default: | |
192 | usage(1); | |
193 | } | |
194 | ||
195 | if (optind+3 != argc) | |
196 | usage(1); | |
5795b420 | 197 | |
d99fabf0 | 198 | running_suid = (getuid() != geteuid()); |
5795b420 | 199 | |
d99fabf0 | 200 | prefix = get_prefix(running_suid, user_prefix); |
8eec804f CW |
201 | |
202 | scorefile = malloc(strlen(prefix) + strlen(argv[optind]) + 2); | |
2f1de3dd | 203 | if (!scorefile) |
cf398788 | 204 | lose_syserr("Couldn't allocate score file"); |
8eec804f | 205 | |
5795b420 | 206 | strcpy(scorefile, prefix); |
8eec804f | 207 | strcat(scorefile, "/"); |
2f1de3dd CW |
208 | strcat(scorefile, argv[optind]); |
209 | newscore = atoi(argv[optind+1]); | |
210 | newdata = argv[optind+2]; | |
8eec804f CW |
211 | if (strlen(newdata) > MAX_DATA_LEN) |
212 | newdata[MAX_DATA_LEN] = '\0'; | |
d99fabf0 CW |
213 | |
214 | if ((user_id = get_user_id()) == NULL) | |
cf398788 | 215 | lose_syserr("Couldn't determine user id"); |
2f1de3dd CW |
216 | |
217 | if (stat(scorefile, &buf) < 0) | |
cf398788 CW |
218 | lose_syserr("Failed to access scores file"); |
219 | ||
2f1de3dd | 220 | if (lock_file(scorefile, &lockstate) < 0) |
cf398788 CW |
221 | lose_syserr("Failed to lock scores file"); |
222 | ||
2f1de3dd CW |
223 | if (read_scores(scorefile, &scores, &scorecount) < 0) |
224 | { | |
8eec804f | 225 | unlock_file(scorefile, lockstate); |
cf398788 | 226 | lose_syserr("Failed to read scores file"); |
2f1de3dd | 227 | } |
d99fabf0 | 228 | push_score(&scores, &scorecount, newscore, user_id, newdata); |
8eec804f CW |
229 | /* Limit the number of scores. If we're using reverse sorting, then |
230 | we should increment the beginning of the array, to skip over the | |
231 | *smallest* scores. Otherwise, we just decrement the number of | |
232 | scores, since the smallest will be at the end. */ | |
233 | if (scorecount > MAX_SCORES) | |
234 | scorecount -= (scorecount - MAX_SCORES); | |
235 | if (reverse) | |
236 | scores += (scorecount - MAX_SCORES); | |
2f1de3dd CW |
237 | sort_scores(scores, scorecount, reverse); |
238 | if (write_scores(scorefile, scores, scorecount) < 0) | |
239 | { | |
8eec804f | 240 | unlock_file(scorefile, lockstate); |
cf398788 | 241 | lose_syserr("Failed to write scores file"); |
2f1de3dd CW |
242 | } |
243 | unlock_file(scorefile, lockstate); | |
244 | exit(0); | |
2f1de3dd CW |
245 | } |
246 | ||
247 | int | |
cf398788 CW |
248 | read_score(f, score) |
249 | FILE *f; | |
250 | struct score_entry *score; | |
2f1de3dd CW |
251 | { |
252 | int c; | |
253 | if (feof(f)) | |
254 | return 1; | |
255 | while ((c = getc(f)) != EOF | |
cd553ffb CW |
256 | && isdigit(c)) |
257 | { | |
258 | score->score *= 10; | |
259 | score->score += (c-48); | |
260 | } | |
261 | while ((c = getc(f)) != EOF | |
262 | && isspace(c)) | |
263 | ; | |
264 | if (c == EOF) | |
265 | return -1; | |
f5bceaf8 | 266 | ungetc(c, f); |
cd553ffb CW |
267 | #ifdef HAVE_GETDELIM |
268 | { | |
7605f1bd | 269 | size_t count = 0; |
cd553ffb CW |
270 | if (getdelim(&score->username, &count, ' ', f) < 1 |
271 | || score->username == NULL) | |
272 | return -1; | |
1d4328ff CW |
273 | /* Trim the space */ |
274 | score->username[strlen(score->username)-1] = '\0'; | |
2f1de3dd | 275 | } |
cd553ffb CW |
276 | #else |
277 | { | |
278 | int unameread = 0; | |
279 | int unamelen = 30; | |
f5bceaf8 CW |
280 | char *username = malloc(unamelen); |
281 | if (!username) | |
282 | return -1; | |
cd553ffb CW |
283 | |
284 | while ((c = getc(f)) != EOF | |
285 | && !isspace(c)) | |
286 | { | |
8eec804f CW |
287 | if (unameread >= unamelen-1) |
288 | if (!(username = realloc(username, unamelen *= 2))) | |
289 | return -1; | |
cd553ffb CW |
290 | username[unameread] = c; |
291 | unameread++; | |
292 | } | |
f5bceaf8 CW |
293 | if (c == EOF) |
294 | return -1; | |
295 | username[unameread] = '\0'; | |
cd553ffb CW |
296 | score->username = username; |
297 | } | |
298 | #endif | |
2f1de3dd CW |
299 | #ifdef HAVE_GETLINE |
300 | score->data = NULL; | |
7605f1bd | 301 | errno = 0; |
2f1de3dd | 302 | { |
7605f1bd | 303 | size_t len; |
2f1de3dd CW |
304 | if (getline(&score->data, &len, f) < 0) |
305 | return -1; | |
5795b420 | 306 | score->data[strlen(score->data)-1] = '\0'; |
2f1de3dd CW |
307 | } |
308 | #else | |
cd553ffb CW |
309 | { |
310 | int cur = 0; | |
311 | int len = 16; | |
312 | char *buf = malloc(len); | |
313 | if (!buf) | |
314 | return -1; | |
f5bceaf8 CW |
315 | while ((c = getc(f)) != EOF |
316 | && c != '\n') | |
cd553ffb CW |
317 | { |
318 | if (cur >= len-1) | |
319 | { | |
320 | if (!(buf = realloc(buf, len *= 2))) | |
321 | return -1; | |
322 | } | |
323 | buf[cur] = c; | |
324 | cur++; | |
325 | } | |
326 | score->data = buf; | |
5795b420 | 327 | score->data[cur] = '\0'; |
cd553ffb | 328 | } |
2f1de3dd | 329 | #endif |
2f1de3dd CW |
330 | return 0; |
331 | } | |
332 | ||
333 | int | |
cf398788 CW |
334 | read_scores(filename, scores, count) |
335 | const char *filename; | |
336 | struct score_entry **scores; | |
337 | int *count; | |
2f1de3dd CW |
338 | { |
339 | int readval, scorecount, cursize; | |
340 | struct score_entry *ret; | |
341 | FILE *f = fopen(filename, "r"); | |
342 | if (!f) | |
343 | return -1; | |
344 | scorecount = 0; | |
345 | cursize = 16; | |
346 | ret = malloc(sizeof(struct score_entry) * cursize); | |
347 | if (!ret) | |
348 | return -1; | |
349 | while ((readval = read_score(f, &ret[scorecount])) == 0) | |
350 | { | |
351 | /* We encoutered an error */ | |
352 | if (readval < 0) | |
353 | return -1; | |
354 | scorecount++; | |
355 | if (scorecount >= cursize) | |
356 | { | |
357 | ret = realloc(ret, cursize *= 2); | |
358 | if (!ret) | |
359 | return -1; | |
360 | } | |
361 | } | |
362 | *count = scorecount; | |
363 | *scores = ret; | |
364 | return 0; | |
365 | } | |
366 | ||
367 | int | |
cf398788 CW |
368 | score_compare(a, b) |
369 | const void *a; | |
370 | const void *b; | |
2f1de3dd CW |
371 | { |
372 | const struct score_entry *sa = (const struct score_entry *) a; | |
373 | const struct score_entry *sb = (const struct score_entry *) b; | |
374 | return (sb->score > sa->score) - (sb->score < sa->score); | |
375 | } | |
376 | ||
377 | int | |
cf398788 CW |
378 | score_compare_reverse(a, b) |
379 | const void *a; | |
380 | const void *b; | |
2f1de3dd CW |
381 | { |
382 | const struct score_entry *sa = (const struct score_entry *) a; | |
383 | const struct score_entry *sb = (const struct score_entry *) b; | |
384 | return (sa->score > sb->score) - (sa->score < sb->score); | |
385 | } | |
386 | ||
387 | int | |
cf398788 CW |
388 | push_score(scores, count, newscore, username, newdata) |
389 | struct score_entry **scores; | |
390 | int *count; int newscore; | |
391 | char *username; | |
392 | char *newdata; | |
2f1de3dd CW |
393 | { |
394 | struct score_entry *newscores = realloc(*scores, | |
395 | sizeof(struct score_entry) * ((*count) + 1)); | |
396 | if (!newscores) | |
397 | return -1; | |
398 | newscores[*count].score = newscore; | |
cd553ffb | 399 | newscores[*count].username = username; |
2f1de3dd CW |
400 | newscores[*count].data = newdata; |
401 | (*count) += 1; | |
402 | *scores = newscores; | |
403 | return 0; | |
404 | } | |
405 | ||
406 | void | |
cf398788 CW |
407 | sort_scores(scores, count, reverse) |
408 | struct score_entry *scores; | |
409 | int count; | |
410 | int reverse; | |
2f1de3dd CW |
411 | { |
412 | qsort(scores, count, sizeof(struct score_entry), | |
413 | reverse ? score_compare_reverse : score_compare); | |
414 | } | |
415 | ||
416 | int | |
cf398788 CW |
417 | write_scores(filename, scores, count) |
418 | const char *filename; | |
419 | const struct score_entry * scores; | |
420 | int count; | |
2f1de3dd CW |
421 | { |
422 | FILE *f; | |
423 | int i; | |
424 | char *tempfile = malloc(strlen(filename) + strlen(".tempXXXXXX") + 1); | |
425 | if (!tempfile) | |
426 | return -1; | |
427 | strcpy(tempfile, filename); | |
428 | strcat(tempfile, ".tempXXXXXX"); | |
8eec804f | 429 | #ifdef HAVE_MKSTEMP |
2f1de3dd | 430 | if (mkstemp(tempfile) < 0 |
8eec804f CW |
431 | #else |
432 | if (mktemp(tempfile) != tempfile | |
433 | #endif | |
2f1de3dd CW |
434 | || !(f = fopen(tempfile, "w"))) |
435 | return -1; | |
436 | for (i = 0; i < count; i++) | |
cd553ffb CW |
437 | if (fprintf(f, "%ld %s %s\n", scores[i].score, scores[i].username, |
438 | scores[i].data) < 0) | |
2f1de3dd CW |
439 | return -1; |
440 | fclose(f); | |
8eec804f | 441 | if (rename(tempfile, filename) < 0) |
2f1de3dd | 442 | return -1; |
8eec804f | 443 | if (chmod(filename, 0644) < 0) |
2f1de3dd | 444 | return -1; |
8eec804f | 445 | return 0; |
2f1de3dd | 446 | } |
cf398788 | 447 | |
2f1de3dd | 448 | int |
cf398788 CW |
449 | lock_file(filename, state) |
450 | const char *filename; | |
451 | void **state; | |
2f1de3dd CW |
452 | { |
453 | int fd; | |
7c4f6873 | 454 | struct stat buf; |
2f1de3dd CW |
455 | int attempts = 0; |
456 | char *lockext = ".lockfile"; | |
457 | char *lockpath = malloc(strlen(filename) + strlen(lockext) + 60); | |
458 | if (!lockpath) | |
459 | return -1; | |
460 | strcpy(lockpath, filename); | |
461 | strcat(lockpath, lockext); | |
462 | *state = lockpath; | |
463 | trylock: | |
464 | attempts++; | |
7c4f6873 CW |
465 | /* If the lock is over an hour old, delete it. */ |
466 | if (stat(lockpath, &buf) == 0 | |
467 | && (difftime(buf.st_ctime, time(NULL) > 60*60))) | |
468 | unlink(lockpath); | |
2f1de3dd CW |
469 | if ((fd = open(lockpath, O_CREAT | O_EXCL, 0600)) < 0) |
470 | { | |
471 | if (errno == EEXIST) | |
472 | { | |
473 | /* Break the lock; we won't corrupt the file, but we might | |
474 | lose some scores. */ | |
475 | if (attempts > MAX_ATTEMPTS) | |
7c4f6873 CW |
476 | { |
477 | unlink(lockpath); | |
478 | attempts = 0; | |
479 | } | |
8eec804f | 480 | sleep((rand() % 2)+1); |
2f1de3dd CW |
481 | goto trylock; |
482 | } | |
483 | else | |
484 | return -1; | |
485 | } | |
486 | close(fd); | |
487 | return 0; | |
488 | } | |
cf398788 | 489 | |
2f1de3dd | 490 | int |
cf398788 CW |
491 | unlock_file(filename, state) |
492 | const char *filename; | |
493 | void *state; | |
2f1de3dd CW |
494 | { |
495 | char *lockpath = (char *) state; | |
496 | int ret = unlink(lockpath); | |
497 | int saved_errno = errno; | |
498 | free(lockpath); | |
499 | errno = saved_errno; | |
500 | return ret; | |
501 | } |