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 }