simple_providers.c revision 299742
1/* 2 * simple_providers.c: providers for SVN_AUTH_CRED_SIMPLE 3 * 4 * ==================================================================== 5 * Licensed to the Apache Software Foundation (ASF) under one 6 * or more contributor license agreements. See the NOTICE file 7 * distributed with this work for additional information 8 * regarding copyright ownership. The ASF licenses this file 9 * to you under the Apache License, Version 2.0 (the 10 * "License"); you may not use this file except in compliance 11 * with the License. You may obtain a copy of the License at 12 * 13 * http://www.apache.org/licenses/LICENSE-2.0 14 * 15 * Unless required by applicable law or agreed to in writing, 16 * software distributed under the License is distributed on an 17 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 18 * KIND, either express or implied. See the License for the 19 * specific language governing permissions and limitations 20 * under the License. 21 * ==================================================================== 22 */ 23 24/* ==================================================================== */ 25 26 27 28/*** Includes. ***/ 29 30#include <apr_pools.h> 31#include "svn_auth.h" 32#include "svn_dirent_uri.h" 33#include "svn_hash.h" 34#include "svn_pools.h" 35#include "svn_error.h" 36#include "svn_utf.h" 37#include "svn_config.h" 38#include "svn_user.h" 39 40#include "private/svn_auth_private.h" 41 42#include "svn_private_config.h" 43 44#include "auth.h" 45 46/*-----------------------------------------------------------------------*/ 47/* File provider */ 48/*-----------------------------------------------------------------------*/ 49 50/* Baton type for the simple provider. */ 51typedef struct simple_provider_baton_t 52{ 53 svn_auth_plaintext_prompt_func_t plaintext_prompt_func; 54 void *prompt_baton; 55 /* We cache the user's answer to the plaintext prompt, keyed 56 * by realm, in case we'll be called multiple times for the 57 * same realm. */ 58 apr_hash_t *plaintext_answers; 59} simple_provider_baton_t; 60 61 62/* Implementation of svn_auth__password_get_t that retrieves 63 the plaintext password from CREDS. */ 64svn_error_t * 65svn_auth__simple_password_get(svn_boolean_t *done, 66 const char **password, 67 apr_hash_t *creds, 68 const char *realmstring, 69 const char *username, 70 apr_hash_t *parameters, 71 svn_boolean_t non_interactive, 72 apr_pool_t *pool) 73{ 74 svn_string_t *str; 75 76 *done = FALSE; 77 78 str = svn_hash_gets(creds, SVN_CONFIG_AUTHN_USERNAME_KEY); 79 if (str && username && strcmp(str->data, username) == 0) 80 { 81 str = svn_hash_gets(creds, SVN_CONFIG_AUTHN_PASSWORD_KEY); 82 if (str && str->data) 83 { 84 *password = str->data; 85 *done = TRUE; 86 } 87 } 88 89 return SVN_NO_ERROR; 90} 91 92/* Implementation of svn_auth__password_set_t that stores 93 the plaintext password in CREDS. */ 94svn_error_t * 95svn_auth__simple_password_set(svn_boolean_t *done, 96 apr_hash_t *creds, 97 const char *realmstring, 98 const char *username, 99 const char *password, 100 apr_hash_t *parameters, 101 svn_boolean_t non_interactive, 102 apr_pool_t *pool) 103{ 104 svn_hash_sets(creds, SVN_CONFIG_AUTHN_PASSWORD_KEY, 105 svn_string_create(password, pool)); 106 *done = TRUE; 107 108 return SVN_NO_ERROR; 109} 110 111/* Set **USERNAME to the username retrieved from CREDS; ignore 112 other parameters. *USERNAME will have the same lifetime as CREDS. */ 113static svn_boolean_t 114simple_username_get(const char **username, 115 apr_hash_t *creds, 116 const char *realmstring, 117 svn_boolean_t non_interactive) 118{ 119 svn_string_t *str; 120 str = svn_hash_gets(creds, SVN_CONFIG_AUTHN_USERNAME_KEY); 121 if (str && str->data) 122 { 123 *username = str->data; 124 return TRUE; 125 } 126 return FALSE; 127} 128 129 130svn_error_t * 131svn_auth__simple_creds_cache_get(void **credentials, 132 void **iter_baton, 133 void *provider_baton, 134 apr_hash_t *parameters, 135 const char *realmstring, 136 svn_auth__password_get_t password_get, 137 const char *passtype, 138 apr_pool_t *pool) 139{ 140 const char *config_dir = svn_hash_gets(parameters, SVN_AUTH_PARAM_CONFIG_DIR); 141 svn_config_t *cfg = svn_hash_gets(parameters, 142 SVN_AUTH_PARAM_CONFIG_CATEGORY_SERVERS); 143 const char *server_group = svn_hash_gets(parameters, 144 SVN_AUTH_PARAM_SERVER_GROUP); 145 const char *username = svn_hash_gets(parameters, 146 SVN_AUTH_PARAM_DEFAULT_USERNAME); 147 const char *password = svn_hash_gets(parameters, 148 SVN_AUTH_PARAM_DEFAULT_PASSWORD); 149 svn_boolean_t non_interactive = svn_hash_gets(parameters, 150 SVN_AUTH_PARAM_NON_INTERACTIVE) 151 != NULL; 152 const char *default_username = NULL; /* Default username from cache. */ 153 const char *default_password = NULL; /* Default password from cache. */ 154 155 /* This checks if we should save the CREDS, iff saving the credentials is 156 allowed by the run-time configuration. */ 157 svn_boolean_t need_to_save = FALSE; 158 apr_hash_t *creds_hash = NULL; 159 svn_error_t *err; 160 svn_string_t *str; 161 162 /* Try to load credentials from a file on disk, based on the 163 realmstring. Don't throw an error, though: if something went 164 wrong reading the file, no big deal. What really matters is that 165 we failed to get the creds, so allow the auth system to try the 166 next provider. */ 167 err = svn_config_read_auth_data(&creds_hash, SVN_AUTH_CRED_SIMPLE, 168 realmstring, config_dir, pool); 169 if (err) 170 { 171 svn_error_clear(err); 172 err = NULL; 173 } 174 else if (creds_hash) 175 { 176 /* We have something in the auth cache for this realm. */ 177 svn_boolean_t have_passtype = FALSE; 178 179 /* The password type in the auth data must match the 180 mangler's type, otherwise the password must be 181 interpreted by another provider. */ 182 str = svn_hash_gets(creds_hash, SVN_CONFIG_AUTHN_PASSTYPE_KEY); 183 if (str && str->data) 184 if (passtype && (0 == strcmp(str->data, passtype))) 185 have_passtype = TRUE; 186 187 /* See if we need to save this username if it is not present in 188 auth cache. */ 189 if (username) 190 { 191 if (!simple_username_get(&default_username, creds_hash, realmstring, 192 non_interactive)) 193 { 194 need_to_save = TRUE; 195 } 196 else 197 { 198 if (strcmp(default_username, username) != 0) 199 need_to_save = TRUE; 200 } 201 } 202 203 /* See if we need to save this password if it is not present in 204 auth cache. */ 205 if (password) 206 { 207 if (have_passtype) 208 { 209 svn_boolean_t done; 210 211 SVN_ERR(password_get(&done, &default_password, creds_hash, 212 realmstring, username, parameters, 213 non_interactive, pool)); 214 if (!done) 215 { 216 need_to_save = TRUE; 217 } 218 else 219 { 220 if (strcmp(default_password, password) != 0) 221 need_to_save = TRUE; 222 } 223 } 224 } 225 226 /* If we don't have a username and a password yet, we try the 227 auth cache */ 228 if (! (username && password)) 229 { 230 if (! username) 231 if (!simple_username_get(&username, creds_hash, realmstring, 232 non_interactive)) 233 username = NULL; 234 235 if (username && ! password) 236 { 237 if (! have_passtype) 238 password = NULL; 239 else 240 { 241 svn_boolean_t done; 242 243 SVN_ERR(password_get(&done, &password, creds_hash, 244 realmstring, username, parameters, 245 non_interactive, pool)); 246 if (!done) 247 password = NULL; 248 249 /* If the auth data didn't contain a password type, 250 force a write to upgrade the format of the auth 251 data file. */ 252 if (password && ! have_passtype) 253 need_to_save = TRUE; 254 } 255 } 256 } 257 } 258 else 259 { 260 /* Nothing was present in the auth cache, so indicate that these 261 credentials should be saved. */ 262 need_to_save = TRUE; 263 } 264 265 /* If we don't have a username yet, check the 'servers' file */ 266 if (! username) 267 { 268 username = svn_config_get_server_setting(cfg, server_group, 269 SVN_CONFIG_OPTION_USERNAME, 270 NULL); 271 } 272 273 /* Ask the OS for the username if we have a password but no 274 username. */ 275 if (password && ! username) 276 username = svn_user_get_name(pool); 277 278 if (username && password) 279 { 280 svn_auth_cred_simple_t *creds = apr_pcalloc(pool, sizeof(*creds)); 281 creds->username = username; 282 creds->password = password; 283 creds->may_save = need_to_save; 284 *credentials = creds; 285 } 286 else 287 *credentials = NULL; 288 289 *iter_baton = NULL; 290 291 return SVN_NO_ERROR; 292} 293 294 295svn_error_t * 296svn_auth__simple_creds_cache_set(svn_boolean_t *saved, 297 void *credentials, 298 void *provider_baton, 299 apr_hash_t *parameters, 300 const char *realmstring, 301 svn_auth__password_set_t password_set, 302 const char *passtype, 303 apr_pool_t *pool) 304{ 305 svn_auth_cred_simple_t *creds = credentials; 306 apr_hash_t *creds_hash = NULL; 307 const char *config_dir; 308 svn_error_t *err; 309 svn_boolean_t dont_store_passwords = 310 svn_hash_gets(parameters, SVN_AUTH_PARAM_DONT_STORE_PASSWORDS) != NULL; 311 svn_boolean_t non_interactive = svn_hash_gets(parameters, 312 SVN_AUTH_PARAM_NON_INTERACTIVE) 313 != NULL; 314 svn_boolean_t no_auth_cache = 315 (! creds->may_save) || (svn_hash_gets(parameters, 316 SVN_AUTH_PARAM_NO_AUTH_CACHE) 317 != NULL); 318 319 /* Make sure we've been passed a passtype. */ 320 SVN_ERR_ASSERT(passtype != NULL); 321 322 *saved = FALSE; 323 324 if (no_auth_cache) 325 return SVN_NO_ERROR; 326 327 config_dir = svn_hash_gets(parameters, SVN_AUTH_PARAM_CONFIG_DIR); 328 329 /* Put the username into the credentials hash. */ 330 creds_hash = apr_hash_make(pool); 331 svn_hash_sets(creds_hash, SVN_CONFIG_AUTHN_USERNAME_KEY, 332 svn_string_create(creds->username, pool)); 333 334 /* Don't store passwords in any form if the user has told 335 * us not to do so. */ 336 if (! dont_store_passwords) 337 { 338 svn_boolean_t may_save_password = FALSE; 339 340 /* If the password is going to be stored encrypted, go right 341 * ahead and store it to disk. Else determine whether saving 342 * in plaintext is OK. */ 343 if (passtype && 344 (strcmp(passtype, SVN_AUTH__WINCRYPT_PASSWORD_TYPE) == 0 345 || strcmp(passtype, SVN_AUTH__KEYCHAIN_PASSWORD_TYPE) == 0 346 || strcmp(passtype, SVN_AUTH__KWALLET_PASSWORD_TYPE) == 0 347 || strcmp(passtype, SVN_AUTH__GNOME_KEYRING_PASSWORD_TYPE) == 0 348 || strcmp(passtype, SVN_AUTH__GPG_AGENT_PASSWORD_TYPE) == 0)) 349 { 350 may_save_password = TRUE; 351 } 352 else 353 { 354#ifdef SVN_DISABLE_PLAINTEXT_PASSWORD_STORAGE 355 may_save_password = FALSE; 356#else 357 const char *store_plaintext_passwords = 358 svn_hash_gets(parameters, SVN_AUTH_PARAM_STORE_PLAINTEXT_PASSWORDS); 359 simple_provider_baton_t *b = 360 (simple_provider_baton_t *)provider_baton; 361 362 if (store_plaintext_passwords 363 && svn_cstring_casecmp(store_plaintext_passwords, 364 SVN_CONFIG_ASK) == 0) 365 { 366 if (non_interactive) 367 /* In non-interactive mode, the default behaviour is 368 * to not store the password, because it is usually 369 * passed on the command line. */ 370 may_save_password = FALSE; 371 else if (b->plaintext_prompt_func) 372 { 373 /* We're interactive, and the client provided a 374 * prompt callback. So we can ask the user. 375 * 376 * Check for a cached answer before prompting. */ 377 svn_boolean_t *cached_answer; 378 cached_answer = svn_hash_gets(b->plaintext_answers, 379 realmstring); 380 if (cached_answer != NULL) 381 may_save_password = *cached_answer; 382 else 383 { 384 apr_pool_t *cached_answer_pool; 385 386 /* Nothing cached for this realm, prompt the user. */ 387 SVN_ERR((*b->plaintext_prompt_func)(&may_save_password, 388 realmstring, 389 b->prompt_baton, 390 pool)); 391 392 /* Cache the user's answer in case we're called again 393 * for the same realm. 394 * 395 * We allocate the answer cache in the hash table's pool 396 * to make sure that is has the same life time as the 397 * hash table itself. This means that the answer will 398 * survive across RA sessions -- which is important, 399 * because otherwise we'd prompt users once per RA session. 400 */ 401 cached_answer_pool = apr_hash_pool_get(b->plaintext_answers); 402 cached_answer = apr_palloc(cached_answer_pool, 403 sizeof(svn_boolean_t)); 404 *cached_answer = may_save_password; 405 svn_hash_sets(b->plaintext_answers, realmstring, 406 cached_answer); 407 } 408 } 409 else 410 { 411 /* TODO: We might want to default to not storing if the 412 * prompt callback is NULL, i.e. have may_save_password 413 * default to FALSE here, in order to force clients to 414 * implement the callback. 415 * 416 * This would change the semantics of old API though. 417 * 418 * So for now, clients that don't implement the callback 419 * and provide no explicit value for 420 * SVN_AUTH_PARAM_STORE_PLAINTEXT_PASSWORDS 421 * cause unencrypted passwords to be stored by default. 422 * Needless to say, our own client is sane, but who knows 423 * what other clients are doing. 424 */ 425 may_save_password = TRUE; 426 } 427 } 428 else if (store_plaintext_passwords 429 && svn_cstring_casecmp(store_plaintext_passwords, 430 SVN_CONFIG_FALSE) == 0) 431 { 432 may_save_password = FALSE; 433 } 434 else if (!store_plaintext_passwords 435 || svn_cstring_casecmp(store_plaintext_passwords, 436 SVN_CONFIG_TRUE) == 0) 437 { 438 may_save_password = TRUE; 439 } 440 else 441 { 442 return svn_error_createf 443 (SVN_ERR_BAD_CONFIG_VALUE, NULL, 444 _("Config error: invalid value '%s' for option '%s'"), 445 store_plaintext_passwords, 446 SVN_AUTH_PARAM_STORE_PLAINTEXT_PASSWORDS); 447 } 448#endif 449 } 450 451 if (may_save_password) 452 { 453 SVN_ERR(password_set(saved, creds_hash, realmstring, 454 creds->username, creds->password, 455 parameters, non_interactive, pool)); 456 if (*saved && passtype) 457 /* Store the password type with the auth data, so that we 458 know which provider owns the password. */ 459 svn_hash_sets(creds_hash, SVN_CONFIG_AUTHN_PASSTYPE_KEY, 460 svn_string_create(passtype, pool)); 461 } 462 } 463 464 /* Save credentials to disk. */ 465 err = svn_config_write_auth_data(creds_hash, SVN_AUTH_CRED_SIMPLE, 466 realmstring, config_dir, pool); 467 if (err) 468 *saved = FALSE; 469 470 /* ### return error? */ 471 svn_error_clear(err); 472 473 return SVN_NO_ERROR; 474} 475 476/* Get cached (unencrypted) credentials from the simple provider's cache. */ 477static svn_error_t * 478simple_first_creds(void **credentials, 479 void **iter_baton, 480 void *provider_baton, 481 apr_hash_t *parameters, 482 const char *realmstring, 483 apr_pool_t *pool) 484{ 485 return svn_auth__simple_creds_cache_get(credentials, iter_baton, 486 provider_baton, parameters, 487 realmstring, 488 svn_auth__simple_password_get, 489 SVN_AUTH__SIMPLE_PASSWORD_TYPE, 490 pool); 491} 492 493/* Save (unencrypted) credentials to the simple provider's cache. */ 494static svn_error_t * 495simple_save_creds(svn_boolean_t *saved, 496 void *credentials, 497 void *provider_baton, 498 apr_hash_t *parameters, 499 const char *realmstring, 500 apr_pool_t *pool) 501{ 502 return svn_auth__simple_creds_cache_set(saved, credentials, provider_baton, 503 parameters, realmstring, 504 svn_auth__simple_password_set, 505 SVN_AUTH__SIMPLE_PASSWORD_TYPE, 506 pool); 507} 508 509static const svn_auth_provider_t simple_provider = { 510 SVN_AUTH_CRED_SIMPLE, 511 simple_first_creds, 512 NULL, 513 simple_save_creds 514}; 515 516 517/* Public API */ 518void 519svn_auth_get_simple_provider2 520 (svn_auth_provider_object_t **provider, 521 svn_auth_plaintext_prompt_func_t plaintext_prompt_func, 522 void* prompt_baton, 523 apr_pool_t *pool) 524{ 525 svn_auth_provider_object_t *po = apr_pcalloc(pool, sizeof(*po)); 526 simple_provider_baton_t *pb = apr_pcalloc(pool, sizeof(*pb)); 527 528 pb->plaintext_prompt_func = plaintext_prompt_func; 529 pb->prompt_baton = prompt_baton; 530 pb->plaintext_answers = apr_hash_make(pool); 531 532 po->vtable = &simple_provider; 533 po->provider_baton = pb; 534 *provider = po; 535} 536 537 538/*-----------------------------------------------------------------------*/ 539/* Prompt provider */ 540/*-----------------------------------------------------------------------*/ 541 542/* Baton type for username/password prompting. */ 543typedef struct simple_prompt_provider_baton_t 544{ 545 svn_auth_simple_prompt_func_t prompt_func; 546 void *prompt_baton; 547 548 /* how many times to re-prompt after the first one fails */ 549 int retry_limit; 550} simple_prompt_provider_baton_t; 551 552 553/* Iteration baton type for username/password prompting. */ 554typedef struct simple_prompt_iter_baton_t 555{ 556 /* how many times we've reprompted */ 557 int retries; 558} simple_prompt_iter_baton_t; 559 560 561 562/*** Helper Functions ***/ 563static svn_error_t * 564prompt_for_simple_creds(svn_auth_cred_simple_t **cred_p, 565 simple_prompt_provider_baton_t *pb, 566 apr_hash_t *parameters, 567 const char *realmstring, 568 svn_boolean_t first_time, 569 svn_boolean_t may_save, 570 apr_pool_t *pool) 571{ 572 const char *default_username = NULL; 573 const char *default_password = NULL; 574 575 *cred_p = NULL; 576 577 /* If we're allowed to check for default usernames and passwords, do 578 so. */ 579 if (first_time) 580 { 581 default_username = svn_hash_gets(parameters, 582 SVN_AUTH_PARAM_DEFAULT_USERNAME); 583 584 /* No default username? Try the auth cache. */ 585 if (! default_username) 586 { 587 const char *config_dir = svn_hash_gets(parameters, 588 SVN_AUTH_PARAM_CONFIG_DIR); 589 apr_hash_t *creds_hash = NULL; 590 svn_string_t *str; 591 svn_error_t *err; 592 593 err = svn_config_read_auth_data(&creds_hash, SVN_AUTH_CRED_SIMPLE, 594 realmstring, config_dir, pool); 595 svn_error_clear(err); 596 if (! err && creds_hash) 597 { 598 str = svn_hash_gets(creds_hash, SVN_CONFIG_AUTHN_USERNAME_KEY); 599 if (str && str->data) 600 default_username = str->data; 601 } 602 } 603 604 /* Still no default username? Try the 'servers' file. */ 605 if (! default_username) 606 { 607 svn_config_t *cfg = svn_hash_gets(parameters, 608 SVN_AUTH_PARAM_CONFIG_CATEGORY_SERVERS); 609 const char *server_group = svn_hash_gets(parameters, 610 SVN_AUTH_PARAM_SERVER_GROUP); 611 default_username = 612 svn_config_get_server_setting(cfg, server_group, 613 SVN_CONFIG_OPTION_USERNAME, 614 NULL); 615 } 616 617 /* Still no default username? Try the UID. */ 618 if (! default_username) 619 default_username = svn_user_get_name(pool); 620 621 default_password = svn_hash_gets(parameters, 622 SVN_AUTH_PARAM_DEFAULT_PASSWORD); 623 } 624 625 /* If we have defaults, just build the cred here and return it. 626 * 627 * ### I do wonder why this is here instead of in a separate 628 * ### 'defaults' provider that would run before the prompt 629 * ### provider... Hmmm. 630 */ 631 if (default_username && default_password) 632 { 633 *cred_p = apr_palloc(pool, sizeof(**cred_p)); 634 (*cred_p)->username = apr_pstrdup(pool, default_username); 635 (*cred_p)->password = apr_pstrdup(pool, default_password); 636 (*cred_p)->may_save = TRUE; 637 } 638 else 639 { 640 SVN_ERR(pb->prompt_func(cred_p, pb->prompt_baton, realmstring, 641 default_username, may_save, pool)); 642 } 643 644 return SVN_NO_ERROR; 645} 646 647 648/* Our first attempt will use any default username/password passed 649 in, and prompt for the remaining stuff. */ 650static svn_error_t * 651simple_prompt_first_creds(void **credentials_p, 652 void **iter_baton, 653 void *provider_baton, 654 apr_hash_t *parameters, 655 const char *realmstring, 656 apr_pool_t *pool) 657{ 658 simple_prompt_provider_baton_t *pb = provider_baton; 659 simple_prompt_iter_baton_t *ibaton = apr_pcalloc(pool, sizeof(*ibaton)); 660 const char *no_auth_cache = svn_hash_gets(parameters, 661 SVN_AUTH_PARAM_NO_AUTH_CACHE); 662 663 SVN_ERR(prompt_for_simple_creds((svn_auth_cred_simple_t **) credentials_p, 664 pb, parameters, realmstring, TRUE, 665 ! no_auth_cache, pool)); 666 667 ibaton->retries = 0; 668 *iter_baton = ibaton; 669 670 return SVN_NO_ERROR; 671} 672 673 674/* Subsequent attempts to fetch will ignore the default values, and 675 simply re-prompt for both, up to a maximum of ib->pb->retry_limit. */ 676static svn_error_t * 677simple_prompt_next_creds(void **credentials_p, 678 void *iter_baton, 679 void *provider_baton, 680 apr_hash_t *parameters, 681 const char *realmstring, 682 apr_pool_t *pool) 683{ 684 simple_prompt_iter_baton_t *ib = iter_baton; 685 simple_prompt_provider_baton_t *pb = provider_baton; 686 const char *no_auth_cache = svn_hash_gets(parameters, 687 SVN_AUTH_PARAM_NO_AUTH_CACHE); 688 689 if ((pb->retry_limit >= 0) && (ib->retries >= pb->retry_limit)) 690 { 691 /* give up, go on to next provider. */ 692 *credentials_p = NULL; 693 return SVN_NO_ERROR; 694 } 695 ib->retries++; 696 697 return prompt_for_simple_creds((svn_auth_cred_simple_t **) credentials_p, 698 pb, parameters, realmstring, FALSE, 699 ! no_auth_cache, pool); 700} 701 702static const svn_auth_provider_t simple_prompt_provider = { 703 SVN_AUTH_CRED_SIMPLE, 704 simple_prompt_first_creds, 705 simple_prompt_next_creds, 706 NULL, 707}; 708 709 710/* Public API */ 711void 712svn_auth_get_simple_prompt_provider 713 (svn_auth_provider_object_t **provider, 714 svn_auth_simple_prompt_func_t prompt_func, 715 void *prompt_baton, 716 int retry_limit, 717 apr_pool_t *pool) 718{ 719 svn_auth_provider_object_t *po = apr_pcalloc(pool, sizeof(*po)); 720 simple_prompt_provider_baton_t *pb = apr_pcalloc(pool, sizeof(*pb)); 721 722 pb->prompt_func = prompt_func; 723 pb->prompt_baton = prompt_baton; 724 pb->retry_limit = retry_limit; 725 726 po->vtable = &simple_prompt_provider; 727 po->provider_baton = pb; 728 *provider = po; 729} 730