b2f58b80afddb245a9bbe60eacd3fc8e1b9631a0
[citadel.git] / citadel / ctdlmigrate.c
1 /*
2  * Across-the-wire migration utility for Citadel
3  *
4  * Copyright (c) 2009-2021 citadel.org
5  *
6  * This program is open source software; you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License version 3.
8  *
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  * GNU General Public License for more details.
13  */
14
15 #include <stdlib.h>
16 #include <unistd.h>
17 #include <stdio.h>
18 #include <string.h>
19 #include <ctype.h>
20 #include <fcntl.h>
21 #include <sys/types.h>
22 #include <sys/stat.h>
23 #include <sys/utsname.h>
24 #include <sys/wait.h>
25 #include <signal.h>
26 #include <netdb.h>
27 #include <errno.h>
28 #include <limits.h>
29 #include <pwd.h>
30 #include <time.h>
31 #include <readline/readline.h>
32 #include <sys/socket.h>
33 #include <sys/un.h>
34 #include <libcitadel.h>
35 #include "citadel.h"
36 #include "axdefs.h"
37 #include "sysdep.h"
38 #include "config.h"
39 #include "citadel_dirs.h"
40
41
42
43 // yeah, this wouldn't work in a multithreaded program.
44 static int gmaxlen = 0;
45 static char *gdeftext = NULL;
46
47 static int limit_rl(FILE *dummy) {
48         if (rl_end > gmaxlen) {
49                 return '\b';
50         }
51         return rl_getc(dummy);
52 }
53
54
55 static int getz_deftext(void) {
56         if (gdeftext) {
57                 rl_insert_text(gdeftext);
58                 gdeftext = NULL;
59                 rl_startup_hook = NULL;
60         }
61         return 0;
62 }
63
64
65 /*
66  * Replacement for gets() that doesn't throw a compiler warning.
67  * We're only using it for some simple prompts, so we don't need
68  * to worry about attackers exploiting it.
69  */
70 void getz(char *buf, int maxlen, char *default_value, char *prompt) {
71         rl_startup_hook = getz_deftext;
72         rl_getc_function = limit_rl;
73         gmaxlen = maxlen;
74         gdeftext = default_value;
75         strcpy(buf, readline(prompt));
76 }
77
78
79 void ctdlmigrate_exit(int cmdexit) {
80         printf("\n\n\033[3%dmExit code %d\033[0m\n", (cmdexit ? 1 : 2), cmdexit);
81         exit(cmdexit);
82 }
83
84
85 /*
86  * Connect to a Citadel on a remote host using a TCP/IP socket
87  */
88 static int tcp_connectsock(char *host, char *service) {
89         struct in6_addr serveraddr;
90         struct addrinfo hints;
91         struct addrinfo *res = NULL;
92         struct addrinfo *ai = NULL;
93         int rc = (-1);
94         int sock = (-1);
95
96         if ((host == NULL) || IsEmptyStr(host)) {
97                 return(-1);
98         }
99         if ((service == NULL) || IsEmptyStr(service)) {
100                 return(-1);
101         }
102
103         memset(&hints, 0x00, sizeof(hints));
104         hints.ai_flags = AI_NUMERICSERV;
105         hints.ai_family = AF_UNSPEC;
106         hints.ai_socktype = SOCK_STREAM;
107
108         /*
109          * Handle numeric IPv4 and IPv6 addresses
110          */
111         rc = inet_pton(AF_INET, host, &serveraddr);
112         if (rc == 1) {          /* dotted quad */
113                 hints.ai_family = AF_INET;
114                 hints.ai_flags |= AI_NUMERICHOST;
115         } else {
116                 rc = inet_pton(AF_INET6, host, &serveraddr);
117                 if (rc == 1) {  /* IPv6 address */
118                         hints.ai_family = AF_INET6;
119                         hints.ai_flags |= AI_NUMERICHOST;
120                 }
121         }
122
123         /* Begin the connection process */
124
125         rc = getaddrinfo(host, service, &hints, &res);
126         if (rc != 0) {
127                 fprintf(stderr, "ctdlmigrate: %s\n", strerror(errno));
128                 return (-1);
129         }
130
131         /*
132          * Try all available addresses until we connect to one or until we run out.
133          */
134         for (ai = res; ai != NULL; ai = ai->ai_next) {
135                 sock = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
136                 if (sock < 0) {
137                         fprintf(stderr, "ctdlmigrate: %s\n", strerror(errno));
138                         return (-1);
139                 }
140
141                 rc = connect(sock, ai->ai_addr, ai->ai_addrlen);
142                 if (rc >= 0) {
143                         return (sock);  /* Connected! */
144                 }
145                 else {
146                         fprintf(stderr, "ctdlmigrate: %s\n", strerror(errno));
147                         close(sock);    /* Failed.  Close the socket to avoid fd leak! */
148                 }
149         }
150         return (-1);
151 }
152
153
154 int uds_connectsock(char *sockpath) {
155         int s;
156         struct sockaddr_un addr;
157
158         memset(&addr, 0, sizeof(addr));
159         addr.sun_family = AF_UNIX;
160         strcpy(addr.sun_path, sockpath);
161
162         s = socket(AF_UNIX, SOCK_STREAM, 0);
163         if (s < 0) {
164                 fprintf(stderr, "ctdlmigrate: Can't create socket: %s\n", strerror(errno));
165                 return(-1);
166         }
167
168         if (connect(s, (struct sockaddr *) &addr, sizeof(addr)) < 0) {
169                 fprintf(stderr, "ctdlmigrate: can't connect: %s\n", strerror(errno));
170                 close(s);
171                 return(-1);
172         }
173
174         return s;
175 }
176
177
178 /*
179  * input binary data from socket
180  */
181 void serv_read(int serv_sock, char *buf, int bytes) {
182         int len, rlen;
183
184         len = 0;
185         while (len < bytes) {
186                 rlen = read(serv_sock, &buf[len], bytes - len);
187                 if (rlen < 1) {
188                         return;
189                 }
190                 len = len + rlen;
191         }
192 }
193
194
195 /*
196  * send binary to server
197  */
198 void serv_write(int serv_sock, char *buf, int nbytes) {
199         int bytes_written = 0;
200         int retval;
201         while (bytes_written < nbytes) {
202                 retval = write(serv_sock, &buf[bytes_written], nbytes - bytes_written);
203                 if (retval < 1) {
204                         return;
205                 }
206                 bytes_written = bytes_written + retval;
207         }
208 }
209
210
211 /*
212  * input string from socket - implemented in terms of serv_read()
213  */
214 void serv_gets(int serv_sock, char *buf) {
215         int i;
216
217         /* Read one character at a time.
218          */
219         for (i = 0;; i++) {
220                 serv_read(serv_sock, &buf[i], 1);
221                 if (buf[i] == '\n' || i == (SIZ-1))
222                         break;
223         }
224
225         /* If we got a long line, discard characters until the newline.
226          */
227         if (i == (SIZ-1)) {
228                 while (buf[i] != '\n') {
229                         serv_read(serv_sock, &buf[i], 1);
230                 }
231         }
232
233         /* Strip all trailing nonprintables (crlf)
234          */
235         buf[i] = 0;
236 }
237
238
239 /*
240  * send line to server - implemented in terms of serv_write()
241  */
242 void serv_puts(int serv_sock, char *buf) {
243         serv_write(serv_sock, buf, strlen(buf));
244         serv_write(serv_sock, "\n", 1);
245 }
246
247
248 /*
249  * serv_printf()        Send formatted printable data to the server
250  */
251 void serv_printf(int serv_sock, const char *format, ...) {   
252         va_list arg_ptr;   
253         char buf[1024];
254    
255         va_start(arg_ptr, format);   
256         if (vsnprintf(buf, sizeof buf, format, arg_ptr) == -1)
257                 buf[sizeof buf - 2] = '\n';
258         serv_write(serv_sock, buf, strlen(buf)); 
259         va_end(arg_ptr);
260 }
261
262
263 int main(int argc, char *argv[]) {
264         char ctdldir[PATH_MAX]=CTDLDIR;
265         char yesno[2];
266         int cmdexit = 0;                                // when something fails, set cmdexit to nonzero, and skip to the end
267         char buf[PATH_MAX];
268
269         char remote_user[128];
270         char remote_host[128];
271         char remote_pass[128];
272         char *remote_port = "504";
273
274         int linecount = 0;
275         int a;
276         int remote_server_socket = (-1);
277         int local_admin_socket = (-1);
278         int progress = 0;
279
280         /* Parse command line */
281         while ((a = getopt(argc, argv, "h:")) != EOF) {
282                 switch (a) {
283                 case 'h':
284                         strcpy(ctdldir, optarg);
285                         break;
286                 default:
287                         fprintf(stderr, "ctdlmigrate: usage: ctdlmigrate [-h server_dir]\n");
288                         return(1);
289                 }
290         }
291
292         if (chdir(ctdldir) != 0) {
293                 fprintf(stderr, "ctdlmigrate: %s: %s\n", ctdldir, strerror(errno));
294                 exit(errno);
295         }
296
297         signal(SIGINT, ctdlmigrate_exit);
298         signal(SIGQUIT, ctdlmigrate_exit);
299         signal(SIGTERM, ctdlmigrate_exit);
300         signal(SIGSEGV, ctdlmigrate_exit);
301         signal(SIGHUP, ctdlmigrate_exit);
302         signal(SIGPIPE, ctdlmigrate_exit);
303
304         printf( "\033[2J\033[H\n"
305                 "          \033[32m╔═══════════════════════════════════════════════╗\n"
306                 "          ║                                               ║\n"
307                 "          ║    \033[33mCitadel over-the-wire migration utility    \033[32m║\n"
308                 "          ║                                               ║\n"
309                 "          ╚═══════════════════════════════════════════════╝\033[0m\n"
310                 "\n"
311                 "This utility is designed to migrate your Citadel installation\n"
312                 "to a new host system via a network connection, without disturbing\n"
313                 "the source system.  The target may be a different CPU architecture\n"
314                 "and/or operating system.  The source system should be running\n"
315                 "Citadel version \033[33m%d\033[0m or newer, and the target system should be running\n"
316                 "either the same version or a newer version.  You will also need\n"
317                 "the \033[33mrsync\033[0m utility, and OpenSSH v4 or newer.\n"
318                 "\n"
319                 "You must run this utility on the TARGET SYSTEM.  Any existing data\n"
320                 "on this system will be ERASED.  Your target system will be on this\n"
321                 "host in %s.\n"
322                 "\n",
323                 EXPORT_REV_MIN, ctdldir
324         );
325
326         getz(yesno, 1, NULL, "Do you wish to continue? ");
327         puts(yesno);
328         if (tolower(yesno[0]) != 'y') {
329                 cmdexit = 1;
330         }
331
332         if (!cmdexit) {
333                 printf( "\033[2J\033[H\n"
334                         "          \033[32m╔═══════════════════════════════════════════════╗\n"
335                         "          ║                                               ║\n"
336                         "          ║       \033[33mValidate source and target systems\033[32m      ║\n"
337                         "          ║                                               ║\n"
338                         "          ╚═══════════════════════════════════════════════╝\033[0m\n"
339                         "\n\n");
340         
341                 printf("First we must validate that the local target system is running and ready to receive data.\n");
342                 printf("Checking connectivity to Citadel in %s...\n", ctdldir);
343                 local_admin_socket = uds_connectsock("citadel-admin.socket");
344                 if (local_admin_socket < 0) {
345                         cmdexit = 1;
346                 }
347         }
348
349         if (!cmdexit) {
350                 serv_gets(local_admin_socket, buf);
351                 puts(buf);
352                 if (buf[0] != '2') {
353                         cmdexit = 1;
354                 }
355         }
356
357         if (!cmdexit) {
358                 serv_puts(local_admin_socket, "ECHO Connection to Citadel Server succeeded.");
359                 serv_gets(local_admin_socket, buf);
360                 puts(buf);
361                 if (buf[0] != '2') {
362                         cmdexit = 1;
363                 }
364         }
365
366         if (!cmdexit) {
367                 printf("\n");
368                 printf("OK, this side is ready to go.  Now we must connect to the source system.\n\n");
369
370                 getz(remote_host, sizeof remote_host, NULL,     "Enter the name or IP address of the source system: ");
371                 getz(remote_user, sizeof remote_user, "admin",  "  Enter the user name of an administrator account: ");
372                 getz(remote_pass, sizeof remote_pass, NULL,     "              Enter the password for this account: ");
373
374                 remote_server_socket = tcp_connectsock(remote_host, remote_port);
375                 if (remote_server_socket < 0) {
376                         cmdexit = 1;
377                 }
378         }
379
380         if (!cmdexit) {
381                 serv_gets(remote_server_socket, buf);
382                 puts(buf);
383                 if (buf[0] != '2') {
384                         cmdexit = 1;
385                 }
386         }
387
388         if (!cmdexit) {
389                 serv_printf(remote_server_socket, "USER %s\n", remote_user);
390                 serv_gets(remote_server_socket, buf);
391                 puts(buf);
392                 if (buf[0] != '3') {
393                         cmdexit = 1;
394                 }
395         }
396
397         if (!cmdexit) {
398                 serv_printf(remote_server_socket, "PASS %s\n", remote_pass);
399                 serv_gets(remote_server_socket, buf);
400                 puts(buf);
401                 if (buf[0] != '2') {
402                         cmdexit = 1;
403                 }
404         }
405
406         if (!cmdexit) {
407                 printf( "\033[2J\033[H\n"
408                         "          \033[32m╔═══════════════════════════════════════════════════════════════════╗\n"
409                         "          ║                                                                   ║\n"
410                         "          ║ \033[33mMigrating from: %-50s\033[32m║\n"
411                         "          ║                                                                   ║\n"
412                         "          ╠═══════════════════════════════════════════════════════════════════╣\n"
413                         "          ║                                                                   ║\n"
414                         "          ║ Lines received: 0                           Percent complete: 0   ║\n"
415                         "          ║                                                                   ║\n"
416                         "          ╚═══════════════════════════════════════════════════════════════════╝\033[0m\n"
417                         "\n", remote_host
418                 );
419         }
420
421         if (!cmdexit) {
422                 serv_puts(remote_server_socket, "MIGR export");
423                 serv_gets(remote_server_socket, buf);
424                 if (buf[0] != '1') {
425                         printf("\n\033[31m%s\033[0m\n", buf);
426                         cmdexit = 3;
427                 }
428         }
429
430         if (!cmdexit) {
431                 serv_puts(local_admin_socket, "MIGR import");
432                 serv_gets(local_admin_socket, buf);
433                 if (buf[0] != '4') {
434                         printf("\n\033[31m%s\033[0m\n", buf);
435                         cmdexit = 3;
436                 }
437         }
438
439         if (!cmdexit) {
440                 char *ptr;
441                 time_t last_update = time(NULL);
442
443                 while (serv_gets(remote_server_socket, buf), strcmp(buf, "000")) {
444                         ptr = strchr(buf, '\n');
445                         if (ptr) *ptr = 0;      // remove the newline character
446                         ++linecount;
447                         if (!strncasecmp(buf, "<progress>", 10)) {
448                                 progress = atoi(&buf[10]);
449                                 printf("\033[8;75H\033[33m%d\033[0m\033[20;0H\n", progress);
450                         }
451                         if (time(NULL) != last_update) {
452                                 last_update = time(NULL);
453                                 printf("\033[8;29H\033[33m%d\033[0m\033[20;0H\n", linecount);
454                         }
455                         serv_puts(local_admin_socket, buf);
456                 }
457         
458                 serv_puts(local_admin_socket, "000");
459         }
460
461         if ( (cmdexit == 0) && (progress < 100) ) {
462                 printf("\033[31mERROR: source stream ended before 100 percent of data was received.\033[0m\n");
463                 ctdlmigrate_exit(86);
464         }
465
466         // FIXME restart the local server now
467
468         ctdlmigrate_exit(cmdexit);
469 }