1 // This file is part of BOINC.
2 // http://boinc.berkeley.edu
3 // Copyright (C) 2017 University of California
4 //
5 // BOINC is free software; you can redistribute it and/or modify it
6 // under the terms of the GNU Lesser General Public License
7 // as published by the Free Software Foundation,
8 // either version 3 of the License, or (at your option) any later version.
9 //
10 // BOINC is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
13 // See the GNU Lesser General Public License for more details.
14 //
15 // You should have received a copy of the GNU Lesser General Public License
16 // along with BOINC.  If not, see <http://www.gnu.org/licenses/>.
17 
18 // code for communicating with account managers (AMs)
19 
20 #include "cpp.h"
21 
22 #ifdef _WIN32
23 #include "boinc_win.h"
24 #else
25 #include "config.h"
26 #include <cstring>
27 #endif
28 
29 #ifdef _MSC_VER
30 #define snprintf _snprintf
31 #endif
32 
33 #include "crypt.h"
34 #include "error_numbers.h"
35 #include "parse.h"
36 #include "str_util.h"
37 #include "str_replace.h"
38 #include "url.h"
39 
40 #include "client_msgs.h"
41 #include "client_state.h"
42 #include "cs_notice.h"
43 #include "file_names.h"
44 #include "filesys.h"
45 #include "gui_http.h"
46 #include "log_flags.h"
47 #include "project.h"
48 
49 #include "acct_mgr.h"
50 
51 static const char *run_mode_name[] = {"", "always", "auto", "never"};
52 
53 // do an account manager RPC;
54 // if URL is null, detach from current account manager
55 //
do_rpc(string _url,string name,string password_hash,bool _via_gui)56 int ACCT_MGR_OP::do_rpc(
57     string _url, string name, string password_hash,
58     bool _via_gui
59 ) {
60     int retval;
61     unsigned int i;
62     char url[256], password[256], buf[256];
63     FILE *pwdf;
64 
65     strlcpy(url, _url.c_str(), sizeof(url));
66 
67     error_num = ERR_IN_PROGRESS;
68     error_str = "";
69     via_gui = _via_gui;
70     if (global_prefs_xml) {
71         free(global_prefs_xml);
72         global_prefs_xml = 0;
73     }
74 
75     // if null URL, detach from current AMS
76     //
77     if (!strlen(url) && strlen(gstate.acct_mgr_info.master_url)) {
78         msg_printf(NULL, MSG_INFO, "Removing account manager info");
79         gstate.acct_mgr_info.clear();
80         boinc_delete_file(ACCT_MGR_URL_FILENAME);
81         boinc_delete_file(ACCT_MGR_LOGIN_FILENAME);
82         error_num = 0;
83         for (i=0; i<gstate.projects.size(); i++) {
84             gstate.projects[i]->detach_ams();
85         }
86         ::rss_feeds.update_feed_list();
87         gstate.set_client_state_dirty("detach from AMS");
88         return 0;
89     }
90 
91     canonicalize_master_url(url, sizeof(url));
92     if (!valid_master_url(url)) {
93         error_num = ERR_INVALID_URL;
94         return 0;
95     }
96 
97     strlcpy(ami.master_url, url, sizeof(ami.master_url));
98     strlcpy(ami.project_name, "", sizeof(ami.project_name));
99     strlcpy(ami.login_name, name.c_str(), sizeof(ami.login_name));
100     strlcpy(ami.password_hash, password_hash.c_str(), sizeof(ami.password_hash));
101 
102     FILE* f = boinc_fopen(ACCT_MGR_REQUEST_FILENAME, "w");
103     if (!f) return ERR_FOPEN;
104     fprintf(f,
105         "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n"
106         "<acct_mgr_request>\n"
107         "   <name>%s</name>\n"
108         "   <password_hash>%s</password_hash>\n"
109         "   <host_cpid>%s</host_cpid>\n"
110         "   <domain_name>%s</domain_name>\n"
111         "   <client_version>%d.%d.%d</client_version>\n"
112         "   <run_mode>%s</run_mode>\n",
113         name.c_str(), password_hash.c_str(),
114         gstate.host_info.host_cpid,
115         gstate.host_info.domain_name,
116         gstate.core_client_version.major,
117         gstate.core_client_version.minor,
118         gstate.core_client_version.release,
119         run_mode_name[gstate.cpu_run_mode.get_perm()]
120     );
121     if (strlen(gstate.acct_mgr_info.previous_host_cpid)) {
122         fprintf(f,
123             "   <previous_host_cpid>%s</previous_host_cpid>\n",
124             gstate.acct_mgr_info.previous_host_cpid
125         );
126     }
127 
128     // If the AMS requested it, send GUI RPC port and password hash.
129     // This is for the "farm" account manager so it
130     // can know where to send GUI RPC requests to
131     // without having to configure each host
132     //
133     if (gstate.acct_mgr_info.send_gui_rpc_info) {
134         if (gstate.cmdline_gui_rpc_port) {
135             fprintf(f,"   <gui_rpc_port>%d</gui_rpc_port>\n", gstate.cmdline_gui_rpc_port);
136         } else {
137             fprintf(f,"   <gui_rpc_port>%d</gui_rpc_port>\n", GUI_RPC_PORT);
138         }
139         if (boinc_file_exists(GUI_RPC_PASSWD_FILE)) {
140             safe_strcpy(password, "");
141             pwdf = fopen(GUI_RPC_PASSWD_FILE, "r");
142             if (pwdf) {
143                 if (fgets(password, 256, pwdf)) {
144                     strip_whitespace(password);
145                 }
146                 fclose(pwdf);
147             }
148             fprintf(f,"   <gui_rpc_password>%s</gui_rpc_password>\n", password);
149         }
150     }
151     for (i=0; i<gstate.projects.size(); i++) {
152         PROJECT* p = gstate.projects[i];
153         double not_started_dur, in_progress_dur;
154         p->get_task_durs(not_started_dur, in_progress_dur);
155         fprintf(f,
156             "   <project>\n"
157             "      <url>%s</url>\n"
158             "      <project_name>%s</project_name>\n"
159             "      <suspended_via_gui>%d</suspended_via_gui>\n"
160             "      <hostid>%d</hostid>\n"
161             "      <not_started_dur>%f</not_started_dur>\n"
162             "      <in_progress_dur>%f</in_progress_dur>\n"
163             "      <attached_via_acct_mgr>%d</attached_via_acct_mgr>\n"
164             "      <dont_request_more_work>%d</dont_request_more_work>\n"
165             "      <detach_when_done>%d</detach_when_done>\n"
166             "      <ended>%d</ended>\n"
167             "      <resource_share>%f</resource_share>\n"
168             "      <cpu_ec>%f</cpu_ec>\n"
169             "      <cpu_time>%f</cpu_time>\n"
170             "      <gpu_ec>%f</gpu_ec>\n"
171             "      <gpu_time>%f</gpu_time>\n"
172             "      <njobs_success>%d</njobs_success>\n"
173             "      <njobs_error>%d</njobs_error>\n"
174             "      <disk_usage>%f</disk_usage>\n"
175             "      <disk_share>%f</disk_share>\n",
176             p->master_url,
177             p->project_name,
178             p->suspended_via_gui?1:0,
179             p->hostid,
180             not_started_dur,
181             in_progress_dur,
182             p->attached_via_acct_mgr?1:0,
183             p->dont_request_more_work?1:0,
184             p->detach_when_done?1:0,
185             p->ended?1:0,
186             p->resource_share,
187             p->cpu_ec,
188             p->cpu_time,
189             p->gpu_ec,
190             p->gpu_time,
191             p->njobs_success,
192             p->njobs_error,
193             p->disk_usage,
194             p->disk_share
195         );
196         if (p->attached_via_acct_mgr) {
197             fprintf(f,
198                 "      <account_key>%s</account_key>\n",
199                 p->authenticator
200             );
201         }
202         fprintf(f,
203             "   </project>\n"
204         );
205     }
206     MIOFILE mf;
207     mf.init_file(f);
208 
209     // send working prefs
210     //
211     fprintf(f, "<working_global_preferences>\n");
212     gstate.global_prefs.write(mf);
213     fprintf(f, "</working_global_preferences>\n");
214 
215     if (boinc_file_exists(GLOBAL_PREFS_FILE_NAME)) {
216         FILE* fprefs = fopen(GLOBAL_PREFS_FILE_NAME, "r");
217         if (fprefs) {
218             copy_stream(fprefs, f);
219             fclose(fprefs);
220         }
221     }
222     gstate.host_info.write(mf, !cc_config.suppress_net_info, true);
223     if (strlen(gstate.acct_mgr_info.opaque)) {
224         fprintf(f,
225             "   <opaque>\n%s\n"
226             "   </opaque>\n",
227             gstate.acct_mgr_info.opaque
228         );
229     }
230     gstate.time_stats.write(mf, true);
231     gstate.net_stats.write(mf);
232     fprintf(f, "</acct_mgr_request>\n");
233     fclose(f);
234     snprintf(buf, sizeof(buf), "%srpc.php", url);
235     retval = gui_http->do_rpc_post(
236         this, buf, ACCT_MGR_REQUEST_FILENAME, ACCT_MGR_REPLY_FILENAME, true
237     );
238     if (retval) {
239         error_num = retval;
240         return retval;
241     }
242     msg_printf(NULL, MSG_INFO, "Contacting account manager at %s", url);
243 
244     return 0;
245 }
246 
handle_no_rsc(const char * name,bool value)247 void AM_ACCOUNT::handle_no_rsc(const char* name, bool value) {
248     int i = rsc_index(name);
249     if (i < 0) return;
250     no_rsc[i] = value;
251 }
252 
parse(XML_PARSER & xp)253 int AM_ACCOUNT::parse(XML_PARSER& xp) {
254     char buf[256];
255     bool btemp;
256     int retval;
257     double dtemp;
258 
259     detach = false;
260     update = false;
261     memset(no_rsc, 0, sizeof(no_rsc));
262     dont_request_more_work.init();
263     detach_when_done.init();
264     suspend.init();
265     abort_not_started.init();
266     url = "";
267     safe_strcpy(url_signature, "");
268     authenticator = "";
269     resource_share.init();
270 
271     while (!xp.get_tag()) {
272         if (!xp.is_tag) {
273             if (log_flags.unparsed_xml) {
274                 msg_printf(0, MSG_INFO,
275                     "[unparsed_xml] AM_ACCOUNT::parse: unexpected text %s",
276                     xp.parsed_tag
277                 );
278             }
279             continue;
280         }
281         if (xp.match_tag("/account")) {
282             if (url.length()) return 0;
283             return ERR_XML_PARSE;
284         }
285         if (xp.parse_string("url", url)) continue;
286         if (xp.match_tag("url_signature")) {
287             retval = xp.element_contents("</url_signature>", url_signature, sizeof(url_signature));
288             if (retval) return retval;
289             safe_strcat(url_signature, "\n");
290             continue;
291         }
292         if (xp.parse_string("authenticator", authenticator)) continue;
293         if (xp.parse_bool("detach", detach)) continue;
294         if (xp.parse_bool("update", update)) continue;
295         if (xp.parse_bool("no_cpu", btemp)) {
296             handle_no_rsc("CPU", btemp);
297             continue;
298         }
299 
300         // deprecated
301         if (xp.parse_bool("no_cuda", btemp)) {
302             handle_no_rsc(GPU_TYPE_NVIDIA, btemp);
303             continue;
304         }
305         if (xp.parse_bool("no_ati", btemp)) {
306             handle_no_rsc(GPU_TYPE_NVIDIA, btemp);
307             continue;
308         }
309 
310         if (xp.parse_str("no_rsc", buf, sizeof(buf))) {
311             handle_no_rsc(buf, true);
312             continue;
313         }
314         if (xp.parse_bool("dont_request_more_work", btemp)) {
315             dont_request_more_work.set(btemp);
316             continue;
317         }
318         if (xp.parse_bool("detach_when_done", btemp)) {
319             detach_when_done.set(btemp);
320             continue;
321         }
322         if (xp.parse_double("resource_share", dtemp)) {
323             if (dtemp >= 0) {
324                 resource_share.set(dtemp);
325             } else {
326                 msg_printf(NULL, MSG_INFO,
327                     "Resource share out of range: %f", dtemp
328                 );
329             }
330             continue;
331         }
332         if (xp.parse_bool("suspend", btemp)) {
333             suspend.set(btemp);
334             continue;
335         }
336         if (xp.parse_bool("abort_not_started", btemp)) {
337             abort_not_started.set(btemp);
338             continue;
339         }
340         if (xp.parse_string("sci_keywords", sci_keywords)) {
341             continue;
342         }
343         if (xp.parse_string("loc_keywords", loc_keywords)) {
344             continue;
345         }
346         if (log_flags.unparsed_xml) {
347             msg_printf(NULL, MSG_INFO,
348                 "[unparsed_xml] AM_ACCOUNT: unrecognized %s", xp.parsed_tag
349             );
350         }
351         xp.skip_unexpected(log_flags.unparsed_xml, "AM_ACCOUNT::parse");
352     }
353     return ERR_XML_PARSE;
354 }
355 
356 // parse RPC reply from account manager
357 //
parse(FILE * f)358 int ACCT_MGR_OP::parse(FILE* f) {
359     string message;
360     int retval;
361     MIOFILE mf;
362     mf.init_file(f);
363     XML_PARSER xp(&mf);
364 
365     accounts.clear();
366     error_str = "";
367     error_num = 0;
368     repeat_sec = 0;
369     safe_strcpy(host_venue, "");
370     safe_strcpy(ami.opaque, "");
371     ami.no_project_notices = false;
372     rss_feeds.clear();
373     if (!xp.parse_start("acct_mgr_reply")) return ERR_XML_PARSE;
374     while (!xp.get_tag()) {
375         if (!xp.is_tag) {
376             if (log_flags.unparsed_xml) {
377                 msg_printf(0, MSG_INFO,
378                     "[unparsed_xml] ACCT_MGR_OP::parse: unexpected text %s",
379                     xp.parsed_tag
380                 );
381             }
382             continue;
383         }
384         if (xp.match_tag("/acct_mgr_reply")) return 0;
385         if (xp.parse_str("name", ami.project_name, 256)) continue;
386         if (xp.parse_int("error_num", error_num)) continue;
387         if (xp.parse_string("error", error_str)) continue;
388         if (xp.parse_string("error_msg", error_str)) continue;
389         if (xp.parse_double("repeat_sec", repeat_sec)) continue;
390         if (xp.parse_string("message", message)) {
391             msg_printf(NULL, MSG_INFO, "Account manager: %s", message.c_str());
392             continue;
393         }
394         if (xp.match_tag("opaque")) {
395             retval = xp.element_contents("</opaque>", ami.opaque, sizeof(ami.opaque));
396             if (retval) return retval;
397             continue;
398         }
399         if (xp.match_tag("signing_key")) {
400             retval = xp.element_contents("</signing_key>", ami.signing_key, sizeof(ami.signing_key));
401             if (retval) return retval;
402             continue;
403         }
404         if (xp.match_tag("account")) {
405             AM_ACCOUNT account;
406             retval = account.parse(xp);
407             if (retval) {
408                 msg_printf(NULL, MSG_INTERNAL_ERROR,
409                     "Can't parse account in account manager reply: %s",
410                     boincerror(retval)
411                 );
412             } else {
413                 accounts.push_back(account);
414             }
415             continue;
416         }
417         if (xp.match_tag("global_preferences")) {
418             retval = dup_element_contents(
419                 f,
420                 "</global_preferences>",
421                 &global_prefs_xml
422             );
423             if (retval) {
424                 msg_printf(NULL, MSG_INTERNAL_ERROR,
425                     "Can't parse global prefs in account manager reply: %s",
426                     boincerror(retval)
427                 );
428                 return retval;
429             }
430             continue;
431         }
432         if (xp.parse_str("host_venue", host_venue, sizeof(host_venue))) {
433             continue;
434         }
435         if (xp.match_tag("rss_feeds")) {
436             got_rss_feeds = true;
437             parse_rss_feed_descs(xp, rss_feeds);
438             continue;
439         }
440         if (xp.parse_bool("no_project_notices", ami.no_project_notices)) {
441             continue;
442         }
443         if (log_flags.unparsed_xml) {
444             msg_printf(NULL, MSG_INFO,
445                 "[unparsed_xml] ACCT_MGR_OP::parse: unrecognized tag <%s>",
446                 xp.parsed_tag
447             );
448         }
449         xp.skip_unexpected(log_flags.unparsed_xml, "ACCT_MGR_OP::parse");
450     }
451     return ERR_XML_PARSE;
452 }
453 
is_weak_auth(const char * auth)454 static inline bool is_weak_auth(const char* auth) {
455     return (strstr(auth, "_") != NULL);
456 }
457 
handle_reply(int http_op_retval)458 void ACCT_MGR_OP::handle_reply(int http_op_retval) {
459 #ifndef SIM
460     unsigned int i;
461     int retval;
462     bool verified;
463     PROJECT* pp;
464     bool sig_ok;
465     bool got_error = false;
466 
467     // check for failures of HTTP OP, reply parse
468     //
469     if (http_op_retval) {
470         msg_printf(&ami, MSG_INFO, "AM RPC HTTP failure: %s",
471             boincerror(http_op_retval)
472         );
473         got_error = true;
474     } else {
475         FILE* f = fopen(ACCT_MGR_REPLY_FILENAME, "r");
476         if (f) {
477             retval = parse(f);
478             if (retval) {
479                 got_error = true;
480                 msg_printf(&ami, MSG_INFO, "AM reply parse error");
481             }
482             fclose(f);
483         } else {
484             msg_printf(&ami, MSG_INFO, "AM reply file missing");
485             got_error = true;
486         }
487     }
488 
489     // if no errors so far, check for errors from AM
490     //
491     if (!got_error) {
492         gstate.acct_mgr_info.password_error = false;
493         if (error_num == ERR_BAD_PASSWD && !via_gui) {
494             gstate.acct_mgr_info.password_error = true;
495         }
496 
497         // Show error message from AM if available.
498         // check both error_str and error_num since an account manager may only
499         // return a BOINC based error code for password failures or invalid
500         // email addresses
501         //
502         if (error_str.size()) {
503             msg_printf(&ami, MSG_USER_ALERT,
504                 "%s: %s",
505                 _("Message from account manager"),
506                 error_str.c_str()
507             );
508             got_error = true;
509         } else if (error_num) {
510             msg_printf(&ami, MSG_USER_ALERT,
511                 "%s: %s",
512                 _("Message from account manager"),
513                 boincerror(error_num)
514             );
515             got_error = true;
516         }
517     }
518 
519     if (got_error) {
520         gstate.acct_mgr_info.next_rpc_time =
521             gstate.now
522             + calculate_exponential_backoff(
523                 gstate.acct_mgr_info.nfailures,
524                 ACCT_MGR_MIN_BACKOFF, ACCT_MGR_MAX_BACKOFF
525             )
526         ;
527         gstate.acct_mgr_info.nfailures++;
528         return;
529     }
530 
531     // The RPC was successful
532     //
533     // Detach projects that are
534     // - detach_when_done
535     // - done
536     // - attached via AM
537     //
538     while (1) {
539         bool found = false;
540         for (i=0; i<gstate.projects.size(); i++) {
541             PROJECT* p = gstate.projects[i];
542             if (p->detach_when_done && !gstate.nresults_for_project(p) && p->attached_via_acct_mgr) {
543                 gstate.detach_project(p);
544                 found = true;
545             }
546         }
547         if (!found) break;
548     }
549 
550     gstate.acct_mgr_info.nfailures = 0;
551 
552     msg_printf(NULL, MSG_INFO, "Account manager contact succeeded");
553 
554     // demand a signing key
555     //
556     sig_ok = true;
557     if (!strlen(ami.signing_key)) {
558         msg_printf(NULL, MSG_INTERNAL_ERROR,
559             "No signing key from account manager"
560         );
561         sig_ok = false;
562     }
563 
564     // don't accept new signing key if we already have one
565     //
566     if (strlen(gstate.acct_mgr_info.signing_key)
567         && strcmp(gstate.acct_mgr_info.signing_key, ami.signing_key)
568     ) {
569         msg_printf(NULL, MSG_INTERNAL_ERROR,
570             "Inconsistent signing key from account manager"
571         );
572         sig_ok = false;
573     }
574 
575     if (sig_ok) {
576         // if the AM RPC had an error, some items may be missing; don't copy
577         //
578         if (strlen(ami.project_name)) {
579             safe_strcpy(gstate.acct_mgr_info.project_name, ami.project_name);
580         }
581         if (strlen(ami.signing_key)) {
582             safe_strcpy(gstate.acct_mgr_info.signing_key, ami.signing_key);
583         }
584         if (strlen(ami.opaque)) {
585             safe_strcpy(gstate.acct_mgr_info.opaque, ami.opaque);
586         }
587         safe_strcpy(gstate.acct_mgr_info.master_url, ami.master_url);
588         safe_strcpy(gstate.acct_mgr_info.login_name, ami.login_name);
589         safe_strcpy(gstate.acct_mgr_info.password_hash, ami.password_hash);
590         gstate.acct_mgr_info.no_project_notices = ami.no_project_notices;
591 
592         // process projects
593         //
594         for (i=0; i<accounts.size(); i++) {
595             AM_ACCOUNT& acct = accounts[i];
596             pp = gstate.lookup_project(acct.url.c_str());
597             if (pp) {
598                 if (acct.detach) {
599                     if (pp->attached_via_acct_mgr) {
600                         gstate.detach_project(pp);
601                     }
602                 } else {
603                     // The AM can leave authenticator blank if request message
604                     // had the current account info
605                     //
606                     if (acct.authenticator.size()) {
607                         if (strcmp(pp->authenticator, acct.authenticator.c_str())) {
608                             // if old and new auths are both weak,
609                             // use the new one
610                             //
611                             if (is_weak_auth(pp->authenticator)
612                                 && is_weak_auth(acct.authenticator.c_str())
613                             ) {
614                                 safe_strcpy(pp->authenticator, acct.authenticator.c_str());
615                                 msg_printf(pp, MSG_INFO,
616                                     "Received new authenticator from account manager"
617                                 );
618                             } else {
619                                 // otherwise skip this update
620                                 //
621                                 msg_printf(pp, MSG_INFO,
622                                     "Already attached to a different account"
623                                 );
624                                 continue;
625                             }
626                         }
627                     }
628                     pp->attached_via_acct_mgr = true;
629                     if (acct.dont_request_more_work.present) {
630                         pp->dont_request_more_work = acct.dont_request_more_work.value;
631                     } else {
632                         pp->dont_request_more_work = false;
633                     }
634                     if (acct.detach_when_done.present) {
635                         pp->detach_when_done = acct.detach_when_done.value;
636                         if (pp->detach_when_done) {
637                             pp->dont_request_more_work = true;
638                         }
639                     } else {
640                         pp->detach_when_done = false;
641                     }
642 
643                     // initiate a scheduler RPC if requested by AMS
644                     //
645                     if (acct.update) {
646                         pp->sched_rpc_pending = RPC_REASON_ACCT_MGR_REQ;
647                         pp->min_rpc_time = 0;
648                     }
649                     if (acct.resource_share.present) {
650                         pp->ams_resource_share = acct.resource_share.value;
651                         pp->resource_share = pp->ams_resource_share;
652                     } else {
653                         // no host-specific resource share;
654                         // if currently have one, restore to value from web
655                         //
656                         if (pp->ams_resource_share >= 0) {
657                             pp->ams_resource_share = -1;
658                             PROJECT p2;
659                             safe_strcpy(p2.master_url, pp->master_url);
660                             retval = p2.parse_account_file();
661                             if (!retval) {
662                                 pp->resource_share = p2.resource_share;
663                             } else {
664                                 pp->resource_share = 100;
665                             }
666                         }
667                     }
668 
669                     if (acct.suspend.present) {
670                         if (acct.suspend.value) {
671                             pp->suspend();
672                         } else {
673                             pp->resume();
674                         }
675                     }
676                     if (acct.abort_not_started.present) {
677                         if (acct.abort_not_started.value) {
678                             pp->abort_not_started();
679                         }
680                     }
681                     for (int j=0; j<MAX_RSC; j++) {
682                         pp->no_rsc_ams[j] = acct.no_rsc[j];
683                     }
684                     pp->sci_keywords = acct.sci_keywords;
685                     pp->loc_keywords = acct.loc_keywords;
686                 }
687             } else {
688                 // here we don't already have the project.
689                 //
690                 retval = check_string_signature2(
691                     acct.url.c_str(), acct.url_signature, ami.signing_key, verified
692                 );
693                 if (retval || !verified) {
694                     msg_printf(NULL, MSG_INTERNAL_ERROR,
695                         "Bad signature for URL %s", acct.url.c_str()
696                     );
697                     continue;
698                 }
699                 if (acct.authenticator.empty()) {
700                     msg_printf(NULL, MSG_INFO,
701                         "Account manager reply missing authenticator for %s",
702                         acct.url.c_str()
703                     );
704                     continue;
705                 }
706 
707                 // Attach to it, unless the acct mgr is telling us to detach
708                 //
709                 if (!acct.detach && !(acct.detach_when_done.present && acct.detach_when_done.value)) {
710                     msg_printf(NULL, MSG_INFO,
711                         "Attaching to %s", acct.url.c_str()
712                     );
713                     gstate.add_project(
714                         acct.url.c_str(), acct.authenticator.c_str(), "", true
715                     );
716                     pp = gstate.lookup_project(acct.url.c_str());
717                     if (pp) {
718                         for (int j=0; j<MAX_RSC; j++) {
719                             pp->no_rsc_ams[j] = acct.no_rsc[j];
720                         }
721                         if (acct.dont_request_more_work.present) {
722                             pp->dont_request_more_work = acct.dont_request_more_work.value;
723                         }
724                         if (acct.suspend.present && acct.suspend.value) {
725                             pp->suspend();
726                         }
727                     } else {
728                         msg_printf(NULL, MSG_INTERNAL_ERROR,
729                             "Failed to add project: %s",
730                             acct.url.c_str()
731                         );
732                     }
733                 }
734             }
735         }
736 
737 #ifdef USE_NET_PREFS
738         bool read_prefs = false;
739         if (strlen(host_venue) && strcmp(host_venue, gstate.main_host_venue)) {
740             safe_strcpy(gstate.main_host_venue, host_venue);
741             read_prefs = true;
742         }
743 
744         // process prefs if any
745         //
746         if (global_prefs_xml) {
747             retval = gstate.save_global_prefs(
748                 global_prefs_xml, ami.master_url, ami.master_url
749             );
750             if (retval) {
751                 msg_printf(NULL, MSG_INTERNAL_ERROR, "Can't save global prefs");
752             }
753             read_prefs = true;
754         }
755 
756         // process prefs if prefs or venue changed
757         //
758         if (read_prefs) {
759             gstate.read_global_prefs();
760         }
761 #endif
762 
763         handle_sr_feeds(rss_feeds, &gstate.acct_mgr_info);
764 
765         // in case no_project_notices changed
766         //
767         ::rss_feeds.update_feed_list();
768     }
769 
770     safe_strcpy(
771         gstate.acct_mgr_info.previous_host_cpid, gstate.host_info.host_cpid
772     );
773     if (repeat_sec) {
774         gstate.acct_mgr_info.next_rpc_time = gstate.now + repeat_sec;
775     } else {
776         gstate.acct_mgr_info.next_rpc_time = gstate.now + 86400;
777     }
778     gstate.acct_mgr_info.write_info();
779     gstate.set_client_state_dirty("account manager RPC");
780 #endif
781 }
782 
783 // write AM info to files.
784 // This is done after each AM RPC,
785 // perhaps overkill since the info doesn't generally change.
786 // But doesn't matter since infrequent.
787 //
write_info()788 int ACCT_MGR_INFO::write_info() {
789     FILE* f;
790     if (strlen(master_url)) {
791         f = fopen(ACCT_MGR_URL_FILENAME, "w");
792         if (!f) {
793             msg_printf(NULL, MSG_USER_ALERT,
794                 "Can't write to %s; check file and directory permissions",
795                 ACCT_MGR_URL_FILENAME
796             );
797             return ERR_FOPEN;
798         }
799         fprintf(f,
800             "<acct_mgr>\n"
801             "    <name>%s</name>\n"
802             "    <url>%s</url>\n",
803             project_name,
804             master_url
805         );
806         if (send_gui_rpc_info) {
807             fprintf(f, "    <send_gui_rpc_info/>\n");
808         }
809         if (strlen(signing_key)) {
810             fprintf(f,
811                 "    <signing_key>\n%s\n</signing_key>\n",
812                 signing_key
813             );
814         }
815         fprintf(f,
816             "</acct_mgr>\n"
817         );
818         fclose(f);
819     }
820 
821     if (strlen(login_name)) {
822         f = fopen(ACCT_MGR_LOGIN_FILENAME, "w");
823         if (!f) {
824             msg_printf(NULL, MSG_USER_ALERT,
825                 "Can't write to %s; check file and directory permissions",
826                 ACCT_MGR_LOGIN_FILENAME
827             );
828             return ERR_FOPEN;
829         }
830         fprintf(f,
831             "<acct_mgr_login>\n"
832             "    <login>%s</login>\n"
833             "    <password_hash>%s</password_hash>\n"
834             "    <previous_host_cpid>%s</previous_host_cpid>\n"
835             "    <next_rpc_time>%f</next_rpc_time>\n"
836             "    <opaque>\n%s\n"
837             "    </opaque>\n"
838             "    <no_project_notices>%d</no_project_notices>\n",
839             login_name,
840             password_hash,
841             previous_host_cpid,
842             next_rpc_time,
843             opaque,
844             no_project_notices?1:0
845         );
846         if (!sched_req_opaque.empty()) {
847             fprintf(f,
848                 "<sched_req_opaque>\n<![CDATA[\n%s\n]]>\n</sched_req_opaque>\n",
849                 sched_req_opaque.c_str()
850             );
851         }
852         fprintf(f,
853             "</acct_mgr_login>\n"
854         );
855         fclose(f);
856     }
857     return 0;
858 }
859 
clear()860 void ACCT_MGR_INFO::clear() {
861     safe_strcpy(project_name, "");
862     safe_strcpy(master_url, "");
863     safe_strcpy(login_name, "");
864     safe_strcpy(user_name, "");
865     safe_strcpy(password_hash, "");
866     safe_strcpy(signing_key, "");
867     safe_strcpy(previous_host_cpid, "");
868     safe_strcpy(opaque, "");
869     sched_req_opaque.clear();
870     safe_strcpy(cookie_failure_url, "");
871     next_rpc_time = 0;
872     nfailures = 0;
873     send_gui_rpc_info = false;
874     password_error = false;
875     no_project_notices = false;
876     cookie_required = false;
877 }
878 
ACCT_MGR_INFO()879 ACCT_MGR_INFO::ACCT_MGR_INFO() {
880     clear();
881 }
882 
parse_login_file(FILE * p)883 int ACCT_MGR_INFO::parse_login_file(FILE* p) {
884     MIOFILE mf;
885     int retval;
886 
887     mf.init_file(p);
888     XML_PARSER xp(&mf);
889     if (!xp.parse_start("acct_mgr_login")) {
890         msg_printf(NULL, MSG_INTERNAL_ERROR,
891             "missing start tag in account manager login file"
892         );
893     }
894     while (!xp.get_tag()) {
895         if (!xp.is_tag) {
896             printf("unexpected text: %s\n", xp.parsed_tag);
897             continue;
898         }
899         if (xp.match_tag("/acct_mgr_login")) break;
900         else if (xp.parse_str("login", login_name, 256)) continue;
901         else if (xp.parse_str("password_hash", password_hash, 256)) continue;
902         else if (xp.parse_str("previous_host_cpid", previous_host_cpid, sizeof(previous_host_cpid))) continue;
903         else if (xp.parse_double("next_rpc_time", next_rpc_time)) continue;
904         else if (xp.match_tag("opaque")) {
905             retval = xp.element_contents("</opaque>", opaque, sizeof(opaque));
906             if (retval) {
907                 msg_printf(NULL, MSG_INFO,
908                     "error parsing <opaque> in acct_mgr_login.xml"
909                 );
910             }
911             continue;
912         }
913         else if (xp.match_tag("sched_req_opaque")) {
914             char buf[65536];
915             retval = xp.element_contents(
916                 "</sched_req_opaque>", buf, sizeof(buf)
917             );
918             if (retval) {
919                 msg_printf(NULL, MSG_INFO,
920                     "error parsing <sched_req_opaque> in acct_mgr_login.xml"
921                 );
922             }
923             sched_req_opaque = string(buf);
924             continue;
925         }
926         else if (xp.parse_bool("no_project_notices", no_project_notices)) continue;
927         if (log_flags.unparsed_xml) {
928             msg_printf(NULL, MSG_INFO,
929                 "[unparsed_xml] unrecognized %s in acct_mgr_login.xml",
930                 xp.parsed_tag
931             );
932         }
933         xp.skip_unexpected(
934             log_flags.unparsed_xml, "ACCT_MGR_INFO::parse_login_file"
935         );
936     }
937     return 0;
938 }
939 
940 // called at client startup.
941 // If currently using an AM, read its URL and login files
942 //
init()943 int ACCT_MGR_INFO::init() {
944     MIOFILE mf;
945     FILE*   p;
946     int retval;
947 
948     clear();
949     p = fopen(ACCT_MGR_URL_FILENAME, "r");
950     if (!p) {
951         // if not using acct mgr, make sure projects not flagged,
952         // otherwise won't be able to detach them.
953         //
954         for (unsigned int i=0; i<gstate.projects.size(); i++) {
955             gstate.projects[i]->attached_via_acct_mgr = false;
956         }
957         return 0;
958     }
959     mf.init_file(p);
960     XML_PARSER xp(&mf);
961     if (!xp.parse_start("acct_mgr")) {
962         //
963     }
964     while (!xp.get_tag()) {
965         if (!xp.is_tag) {
966             printf("unexpected text: %s\n", xp.parsed_tag);
967             continue;
968         }
969         if (xp.match_tag("/acct_mgr")) break;
970         else if (xp.parse_str("name", project_name, 256)) continue;
971         else if (xp.parse_str("url", master_url, 256)) continue;
972         else if (xp.parse_bool("send_gui_rpc_info", send_gui_rpc_info)) continue;
973         else if (xp.match_tag("signing_key")) {
974             retval = xp.element_contents("</signing_key>", signing_key, sizeof(signing_key));
975             if (retval) {
976                 msg_printf(NULL, MSG_INFO,
977                     "error parsing <signing_key> in acct_mgr_url.xml"
978                 );
979             }
980             continue;
981         }
982         else if (xp.parse_bool("cookie_required", cookie_required)) continue;
983         else if (xp.parse_str("cookie_failure_url", cookie_failure_url, 256)) continue;
984         if (log_flags.unparsed_xml) {
985             msg_printf(NULL, MSG_INFO,
986                 "[unparsed_xml] ACCT_MGR_INFO::init: unrecognized %s",
987                 xp.parsed_tag
988             );
989         }
990         xp.skip_unexpected(log_flags.unparsed_xml, "ACCT_MGR_INFO::init");
991     }
992     fclose(p);
993 
994     p = fopen(ACCT_MGR_LOGIN_FILENAME, "r");
995     if (p) {
996         parse_login_file(p);
997         fclose(p);
998     }
999     if (using_am()) {
1000         msg_printf(NULL, MSG_INFO, "Using account manager %s", project_name);
1001         if (strlen(user_name)) {
1002             msg_printf(NULL, MSG_INFO, "Account manager login: %s", user_name);
1003         }
1004     }
1005     return 0;
1006 }
1007 
poll()1008 bool ACCT_MGR_INFO::poll() {
1009     if (!using_am()) return false;
1010     if (gstate.acct_mgr_op.gui_http->is_busy()) return false;
1011 
1012     if (gstate.now > next_rpc_time) {
1013 
1014         // default synch period is 1 day
1015         //
1016         next_rpc_time = gstate.now + 86400;
1017         gstate.acct_mgr_op.do_rpc(
1018             master_url, login_name, password_hash, false
1019         );
1020         return true;
1021     }
1022     return false;
1023 }
1024