tsynk.c - synk - synchronize files between hosts
(DIR) Log
(DIR) Files
(DIR) Refs
(DIR) README
(DIR) LICENSE
---
tsynk.c (12664B)
---
1 #include <sys/socket.h>
2 #include <sys/stat.h>
3 #include <sys/types.h>
4 #include <sys/wait.h>
5
6 #include <errno.h>
7 #include <stdarg.h>
8 #include <stdint.h>
9 #include <stdio.h>
10 #include <stdlib.h>
11 #include <string.h>
12 #include <netdb.h>
13 #include <unistd.h>
14
15 #include "arg.h"
16 #include "sha512.h"
17 #include "synk.h"
18
19 #define IS_LOOPBACK(p) ((p)->peer.sin_addr.s_addr == htonl(INADDR_LOOPBACK))
20 #define log(l,...) if(verbose>=l){printf(__VA_ARGS__);}
21
22 /* different operationnal mode for TCP connection */
23 enum {
24 SYNK_CLIENT,
25 SYNK_SERVER
26 };
27
28 enum {
29 LOG_NONE = 0,
30 LOG_VERBOSE = 1,
31 LOG_DEBUG = 2,
32 };
33
34 static void usage(char *name);
35 static char *echo(char * []);
36 static char **concat(int, ...);
37
38 static long gettimestamp(const char *path);
39 static struct in_addr *getinetaddr(char *);
40 static struct metadata_t *getmetadata(const char *);
41 static struct peer_t *freshestpeer(struct peers_t *);
42 static int getpeermeta(struct peer_t *, struct metadata_t *);
43 static int flushpeers(struct peers_t *);
44 static int spawnremote(struct peers_t *);
45 static int uptodate(struct peers_t *);
46 static int dosync(struct peer_t *master, struct peer_t *slave);
47 static int syncwithmaster(struct peer_t *master, struct peers_t *plist);
48 static int syncfile(struct peers_t *, const char *);
49 static int sendmetadata(struct client_t *);
50 static int waitclient(in_addr_t, in_port_t);
51
52 const char *rsynccmd[] = { "rsync", "-azEq", "--delete", NULL };
53 const char *sshcmd[] = { "ssh", NULL };
54
55 int verbose = LOG_NONE;
56
57 void
58 usage(char *name)
59 {
60 fprintf(stderr, "usage: %s [-vs] [-f FILE] [-p PORT] [-h HOST] [FILE..]\n", name);
61 exit(1);
62 }
63
64 /*
65 * Same as the echo(1) command as defined by POSIX. Takes a like of arguments
66 * and return a string containing all args separated by white spaces.
67 */
68 char *
69 echo(char *args[])
70 {
71 size_t len = 0;
72 char *str = NULL;
73 char **p;
74
75 str = malloc(_POSIX_ARG_MAX);
76 memset(str, 0, _POSIX_ARG_MAX);
77
78 for (p = args; *p || len > _POSIX_ARG_MAX; p++) {
79 snprintf(str + len, _POSIX_ARG_MAX, "%s ", *p);
80 len += strnlen(*p, _POSIX_ARG_MAX) + 1;
81 }
82
83 str[len-1] = 0;
84
85 return str;
86 }
87
88 /*
89 * Take a variable number of arrays, and concatenate them in a single array.
90 * The first argument is the number of arrays passed
91 * All arrays should be NULL terminated, or undefined behavior may occur.
92 */
93 char **
94 concat(int n, ...)
95 {
96 size_t i, len = 0;
97 va_list args;
98 char **p, **tmp, **cat = { NULL };
99
100 va_start(args, n);
101 while (n --> 0) {
102 p = va_arg(args, char **);
103
104 /* count args in the given array */
105 for (i=0; p[i]; ++i);
106
107 /* Leave room for a NULL arg at the end */
108 i += n ? 0 : 1;
109
110 tmp = realloc(cat, (len + i) * sizeof(char *));
111 if (!tmp) {
112 perror("realloc");
113 free(cat);
114 va_end(args);
115 return NULL;
116 }
117 cat = tmp;
118 memcpy(cat + len, p, i*sizeof(char *));
119 len += i;
120 }
121
122 va_end(args);
123 return cat;
124 }
125
126 /*
127 * Retrieve metadata about a filename and store it in the given pointer.
128 * The pointer must be already allocated
129 */
130 struct metadata_t *
131 getmetadata(const char *fn)
132 {
133 FILE *f = NULL;
134 struct metadata_t *meta = NULL;
135
136 if ((meta = malloc(sizeof(struct metadata_t))) == NULL) {
137 perror("malloc");
138 return NULL;
139 }
140
141 memset(meta, 0, sizeof(struct metadata_t));
142 snprintf(meta->path, _POSIX_PATH_MAX, "%s", fn);
143 if ((f = fopen(fn, "r")) == NULL)
144 return meta;
145
146 sha512(f, meta->hash);
147 meta->mtime = gettimestamp(meta->path);
148
149 fclose(f);
150
151 return meta;
152 }
153
154 /*
155 * Returns the UNIX timestamp for the given file, or -1 in case stat(2)
156 * is in error.
157 */
158 long
159 gettimestamp(const char *path)
160 {
161 struct stat sb;
162 if (stat(path, &sb) < 0) {
163 fprintf(stderr, "stat: %s: %s\n", path, strerror(errno));;
164 return -1;
165 }
166
167 return sb.st_mtim.tv_sec;
168 }
169
170 /*
171 * Put an hostname, get an in_addr!
172 * This is intended to be consumed directly, as gethostbyname() might
173 * return a pointer to a static buffer
174 */
175 struct in_addr *
176 getinetaddr(char *hostname)
177 {
178 struct hostent *he;
179
180 if (!(he = gethostbyname(hostname))) {
181 herror(hostname);
182 return NULL;
183 }
184
185 return ((struct in_addr **)he->h_addr_list)[0];
186 }
187
188 /*
189 * Add a peer to the singly-linked list referencing peers.
190 * metadata structure will be zeroed for future use.
191 */
192 struct peer_t *
193 addpeer(struct peers_t *plist, char *hostname, in_port_t port)
194 {
195 struct peer_t *entry = NULL;
196 struct in_addr *host;
197
198 entry = malloc(sizeof(struct peer_t));
199 memset(&entry->meta, 0, sizeof(struct metadata_t));
200 memset(&entry->peer, 0, sizeof(struct sockaddr_in));
201
202 strncpy(entry->host, hostname, HOST_NAME_MAX);
203 host = getinetaddr(hostname);
204
205 entry->peer.sin_family = AF_INET;
206 entry->peer.sin_addr.s_addr = host->s_addr;
207 entry->peer.sin_port = htons(port);
208
209 SLIST_INSERT_HEAD(plist, entry, entries);
210
211 log(LOG_DEBUG, "+ peer %s:%d\n", entry->host, ntohs(entry->peer.sin_port));
212
213 return entry;
214 }
215
216 /*
217 * return a pointer to the peer having the highest timestamp.
218 * NULL is returned in case the local file is the most recent
219 */
220 struct peer_t *
221 freshestpeer(struct peers_t *plist)
222 {
223 long ts = -1;
224 struct peer_t *tmp = NULL;
225 struct peer_t *freshest = NULL;
226
227 SLIST_FOREACH(tmp, plist, entries) {
228 if (tmp->meta.mtime > ts) {
229 freshest = tmp;
230 ts = tmp->meta.mtime;
231 }
232 }
233
234 log(LOG_VERBOSE, "master: %s\n", freshest->host);
235 return freshest;
236 }
237
238 /*
239 * Client part: connect to the given address/port and send the given path to
240 * the server. The server should return the timestamp for this file on the
241 * socket. Connection is terminated after receiving the timestamp
242 */
243 int
244 getpeermeta(struct peer_t *clt, struct metadata_t *local)
245 {
246 int i, cfd;
247 ssize_t r, len = 0;
248
249 if ((cfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) {
250 perror("socket");
251 return -1;
252 }
253
254 for (i=0; i<MAXRETRY; i++) {
255 if (!connect(cfd, (struct sockaddr *) &(clt->peer), sizeof(clt->peer)))
256 break;
257
258 if (errno != ECONNREFUSED || i+1 >= MAXRETRY) {
259 fprintf(stderr, "%s: %s\n", inet_ntoa(clt->peer.sin_addr), strerror(errno));;
260 return -1;
261 }
262 usleep(250000);
263 }
264
265 if (write(cfd, local, sizeof(struct metadata_t)) < 0) {
266 perror("write");
267 return -1;
268 }
269
270 /* ... which should return the metadata for this file */
271 len = 0;
272 while (len < (ssize_t)sizeof(struct metadata_t)) {
273 if ((r = read(cfd, (unsigned char *)&(clt->meta) + len, RCVBUFSZ)) < 0) {
274 perror("read");
275 return -1;
276 }
277 len += r;
278 }
279
280 return close(cfd);
281 }
282
283 /*
284 * Empty the linked-list containing all peers
285 */
286 int
287 flushpeers(struct peers_t *plist)
288 {
289 struct peer_t *tmp = NULL;
290 while (!SLIST_EMPTY(plist)) {
291 tmp = SLIST_FIRST(plist);
292 SLIST_REMOVE_HEAD(plist, entries);
293 free(tmp);
294 }
295
296 return 0;
297 }
298
299 /*
300 * Read a path from a connected client, get the timestamp for this path and
301 * send it back to the client. Close connection afterward.
302 */
303 int
304 sendmetadata(struct client_t *c)
305 {
306 char buf;
307 ssize_t len = 0;
308 struct metadata_t *local, remote;
309
310 memset(&remote, 0, sizeof(remote));
311
312 if ((len = read(c->fd, &remote, sizeof(remote))) < 0) {
313 fprintf(stderr, "%s: %s\n", inet_ntoa(c->inet), strerror(errno));;
314 return -1;
315 }
316
317 local = getmetadata(remote.path);
318
319 /* .. and send it to the client */
320 write(c->fd, local, sizeof(struct metadata_t));
321
322 while (recv(c->fd, &buf, 1, MSG_PEEK) > 0);
323 close(c->fd);
324 free(c);
325
326 return -1;
327 }
328
329 /*
330 * Server part: bind on given address/port and wait for a client connection.
331 * Only one client is handled per server instance, and the server gets close
332 * at the end.
333 */
334 int
335 waitclient(in_addr_t host, in_port_t port)
336 {
337 int sfd;
338 int cfd;
339 socklen_t len;
340 struct sockaddr_in clt;
341 struct sockaddr_in srv;
342 struct client_t *c = NULL;
343
344 if ((sfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) {
345 perror("socket");
346 return -1;
347 }
348
349 memset(&srv, 0, sizeof(srv));
350 srv.sin_family = AF_INET;
351 srv.sin_addr.s_addr = host;
352 srv.sin_port = htons(port);
353
354 if (bind(sfd, (struct sockaddr *)&srv, sizeof(srv)) < 0) {
355 perror("bind");
356 return -1;
357 }
358
359 if (listen(sfd, MAXCONNECT) < 0) {
360 perror("listen");
361 return -1;
362 }
363
364 len = sizeof(clt);
365 if ((cfd = accept(sfd, (struct sockaddr *)&clt, &len)) < 0) {
366 perror("accept");
367 return -1;
368 }
369
370 alarm(0); /* cancel previously set SIGALRM */
371
372 c = malloc(sizeof(struct client_t));
373 c->fd = cfd;
374 c->inet = clt.sin_addr;
375
376 sendmetadata(c);
377
378 return close(sfd);
379 }
380
381 /*
382 * Connect via ssh to a remote and spawn an instance running in server-mode
383 */
384 int
385 spawnremote(struct peers_t *plist)
386 {
387 char **cmd = NULL;
388 char synk_cmd[_POSIX_ARG_MAX];
389
390 struct peer_t *tmp;
391
392 SLIST_FOREACH(tmp, plist, entries) {
393 if (IS_LOOPBACK(tmp))
394 continue;
395 snprintf(synk_cmd, _POSIX_ARG_MAX, "synk -s -h %s -p %d",
396 inet_ntoa(tmp->peer.sin_addr),
397 ntohs(tmp->peer.sin_port));
398 cmd = concat(2, sshcmd, (char *[]){ tmp->host, synk_cmd, NULL });
399 if (!fork()) {
400 execvp(cmd[0], cmd);
401 fprintf(stderr, "execvp: %s: %s\n", cmd[0], strerror(errno));;
402 return -1;
403 }
404 }
405 return 0;
406 }
407
408 /*
409 * Check the synchronisation status between all peers. If at least 2 hashes
410 * differ, it returns with a non-zero status.
411 */
412 int
413 uptodate(struct peers_t *plist)
414 {
415 struct peer_t *tmp, *ref;
416
417 ref = SLIST_FIRST(plist);
418 SLIST_FOREACH(tmp, plist, entries) {
419 if (sha512_compare(ref->meta.hash, tmp->meta.hash)) {
420 log(LOG_DEBUG, "+ sha512 mismatch: %s / %s)\n", ref->host, tmp->host);
421 return 0;
422 }
423 }
424
425 log(LOG_DEBUG, "+ no difference found: %s\n", ref->meta.path);
426 return 1;
427 }
428
429 /*
430 * Given a master and a slave, create the appropriate rsync(1) command to
431 * get the slave in sync with the master, from localhost (this might involve
432 * using ssh to spawn the rsync process on a remote master)
433 */
434 int
435 dosync(struct peer_t *master, struct peer_t *slave)
436 {
437 char **cmd = NULL;
438 char *args[] = { NULL, NULL, NULL };
439 char source[_POSIX_ARG_MAX] = "";
440 char destination[_POSIX_ARG_MAX] = "";
441
442 if (IS_LOOPBACK(slave)) {
443 snprintf(destination, _POSIX_ARG_MAX, "%s", master->meta.path);
444 snprintf(source, _POSIX_ARG_MAX, "%s:%s", master->host, slave->meta.path);
445 } else {
446 snprintf(source, _POSIX_ARG_MAX, "%s", master->meta.path);
447 snprintf(destination, _POSIX_ARG_MAX, "%s:%s", slave->host, slave->meta.path);
448 }
449
450 args[0] = source;
451 args[1] = destination;
452
453 cmd = concat(2, rsynccmd, args);
454
455 if (!IS_LOOPBACK(master) && !IS_LOOPBACK(slave)) {
456 cmd = concat(2, sshcmd, (char *[]){
457 master->host, echo(cmd), NULL });
458 }
459
460 if (!fork()) {
461 log(LOG_VERBOSE, "synk: %s\n", echo(cmd));
462 execvp(cmd[0], cmd);
463 fprintf(stderr, "execvp: %s: %s\n", cmd[0], strerror(errno));;
464 return -1;
465 }
466 free(cmd);
467
468 return 0;
469 }
470
471 /*
472 * Logic to synchronize a remote peer with all the slaves if they differ
473 */
474 int
475 syncwithmaster(struct peer_t *master, struct peers_t *plist)
476 {
477 int ret = 0;
478 struct peer_t *slave = NULL;
479 SLIST_FOREACH(slave, plist, entries) {
480 if (slave == master)
481 continue;
482 if (!sha512_compare(master->meta.hash, slave->meta.hash))
483 continue;
484
485 ret += dosync(master, slave);
486 }
487 return ret;
488 }
489
490 /*
491 * Check the synchronisation state of a file between mutliple peers, and
492 * synchronise them if they differ
493 */
494 int
495 syncfile(struct peers_t *plist, const char *fn)
496 {
497 int ret = 0;
498 struct metadata_t *local;
499 struct peer_t *tmp = NULL;
500 struct peer_t *master = NULL;
501
502 local = getmetadata(fn);
503
504 if (!local)
505 return -1;
506
507 SLIST_FOREACH(tmp, plist, entries) {
508 if (IS_LOOPBACK(tmp)) {
509 memcpy(&tmp->meta, local, sizeof(struct metadata_t));
510 } else {
511 if (getpeermeta(tmp, local) != 0)
512 return -1;
513 }
514
515 log(LOG_VERBOSE, "peer: %s\t%s\t%.7s\t%lu\n",
516 tmp->host,
517 tmp->meta.path,
518 sha512_format(tmp->meta.hash),
519 tmp->meta.mtime);
520 }
521
522 if (!uptodate(plist)) {
523 master = freshestpeer(plist);
524 ret = syncwithmaster(master, plist);
525 }
526
527 free(local);
528
529 while (waitpid(-1, NULL, WNOHANG) > 0);
530
531 return ret;
532 }
533
534 int
535 main(int argc, char *argv[])
536 {
537 char *argv0, *fn;
538 char config[_POSIX_PATH_MAX] = PATHCONFIG;
539 char *hostname = NULL;
540 in_port_t port = DEFPORT;
541 uint8_t mode = SYNK_CLIENT;
542 struct peers_t plist;
543
544 SLIST_INIT(&plist);
545
546 ARGBEGIN{
547 case 'f':
548 strncpy(config, EARGF(usage(argv0)), _POSIX_PATH_MAX);
549 break;
550 case 'h':
551 hostname = EARGF(usage(argv0));
552 if (mode == SYNK_CLIENT)
553 addpeer(&plist, hostname, port);
554 break;
555 case 'p': port = atoi(EARGF(usage(argv0))); break;
556 case 's': mode = SYNK_SERVER; break;
557 case 'v': verbose++; break;
558 }ARGEND;
559
560 switch(mode) {
561 case SYNK_CLIENT:
562 if (SLIST_EMPTY(&plist)) {
563 log(LOG_DEBUG, "+ using config %s\n", config);
564 parseconf(&plist, config);
565 }
566
567 addpeer(&plist, "localhost", DEFPORT);
568 while ((fn = *(argv++)) != NULL) {
569 spawnremote(&plist);
570 syncfile(&plist, fn);
571 }
572 flushpeers(&plist);
573 break;
574 case SYNK_SERVER:
575 alarm(SERVERTIMEO);
576 waitclient(getinetaddr(hostname)->s_addr, port);
577 break;
578 }
579 return 0;
580 }