Changeset 988 for vendor/current/nsswitch/pam_winbind.c
- Timestamp:
- Nov 24, 2016, 1:14:11 PM (9 years ago)
- File:
-
- 1 edited
Legend:
- Unmodified
- Added
- Removed
-
vendor/current/nsswitch/pam_winbind.c
r860 r988 12 12 13 13 #include "pam_winbind.h" 14 #define CONST_DISCARD(type,ptr) ((type)(void *)ptr) 15 14 15 enum pam_winbind_request_type 16 { 17 PAM_WINBIND_AUTHENTICATE, 18 PAM_WINBIND_SETCRED, 19 PAM_WINBIND_ACCT_MGMT, 20 PAM_WINBIND_OPEN_SESSION, 21 PAM_WINBIND_CLOSE_SESSION, 22 PAM_WINBIND_CHAUTHTOK, 23 PAM_WINBIND_CLEANUP 24 }; 16 25 17 26 static int wbc_error_to_pam_error(wbcErr status) … … 141 150 do { \ 142 151 _pam_log_debug(ctx, LOG_DEBUG, "[pamh: %p] LEAVE: " \ 143 function " returning %d (%s)", ctx ->pamh, retval, \152 function " returning %d (%s)", ctx ? ctx->pamh : NULL, retval, \ 144 153 _pam_error_code_str(retval)); \ 145 154 _pam_log_state(ctx); \ … … 164 173 #endif 165 174 166 167 /*168 * Work around the pam API that has functions with void ** as parameters169 * These lead to strict aliasing warnings with gcc.170 */171 static int _pam_get_item(const pam_handle_t *pamh,172 int item_type,173 const void *_item)174 {175 const void **item = (const void **)_item;176 return pam_get_item(pamh, item_type, item);177 }178 static int _pam_get_data(const pam_handle_t *pamh,179 const char *module_data_name,180 const void *_data)181 {182 const void **data = (const void **)_data;183 return pam_get_data(pamh, module_data_name, data);184 }185 175 186 176 /* some syslogging */ … … 203 193 const char *service; 204 194 205 _pam_get_item(pamh, PAM_SERVICE,&service);195 pam_get_item(pamh, PAM_SERVICE, (const void **) &service); 206 196 207 197 format2 = (char *)malloc(strlen(MODULE_NAME)+strlen(format)+strlen(service)+5); … … 281 271 va_list args; 282 272 283 if (! _pam_log_is_debug_enabled(r->ctrl)) {273 if (!r || !_pam_log_is_debug_enabled(r->ctrl)) { 284 274 return; 285 275 } … … 350 340 _pam_log_state_datum(ctx, item_type, #item_type, \ 351 341 _LOG_PASSWORD_AS_STRING) 342 /* 343 * wrapper to preserve old behaviour of iniparser which ignored 344 * key values that had no value assigned like 345 * key = 346 * for a key like above newer iniparser will return a zero-length 347 * string, previously iniparser would return NULL 348 * 349 * JRA: For compatibility, tiniparser behaves like iniparser. 350 */ 351 static const char *tiniparser_getstring_nonempty(struct tiniparser_dictionary *d, 352 const char *key, 353 const char *def) 354 { 355 const char *ret = tiniparser_getstring(d, key, def); 356 if (ret && strlen(ret) == 0) { 357 ret = NULL; 358 } 359 return ret; 360 } 352 361 353 362 static void _pam_log_state(struct pwb_context *ctx) 354 363 { 355 if (! _pam_log_is_debug_state_enabled(ctx->ctrl)) {364 if (!ctx || !_pam_log_is_debug_state_enabled(ctx->ctrl)) { 356 365 return; 357 366 } … … 389 398 int argc, 390 399 const char **argv, 391 dictionary **result_d) 400 enum pam_winbind_request_type type, 401 struct tiniparser_dictionary **result_d) 392 402 { 393 403 int ctrl = 0; … … 395 405 int i; 396 406 const char **v; 397 dictionary *d = NULL;407 struct tiniparser_dictionary *d = NULL; 398 408 399 409 if (flags & PAM_SILENT) { … … 413 423 } 414 424 415 d = iniparser_load(CONST_DISCARD(char *, config_file));425 d = tiniparser_load(config_file); 416 426 if (d == NULL) { 417 427 goto config_from_pam; 418 428 } 419 429 420 if ( iniparser_getboolean(d, CONST_DISCARD(char *, "global:debug"), false)) {430 if (tiniparser_getboolean(d, "global:debug", false)) { 421 431 ctrl |= WINBIND_DEBUG_ARG; 422 432 } 423 433 424 if ( iniparser_getboolean(d, CONST_DISCARD(char *, "global:debug_state"), false)) {434 if (tiniparser_getboolean(d, "global:debug_state", false)) { 425 435 ctrl |= WINBIND_DEBUG_STATE; 426 436 } 427 437 428 if ( iniparser_getboolean(d, CONST_DISCARD(char *, "global:cached_login"), false)) {438 if (tiniparser_getboolean(d, "global:cached_login", false)) { 429 439 ctrl |= WINBIND_CACHED_LOGIN; 430 440 } 431 441 432 if ( iniparser_getboolean(d, CONST_DISCARD(char *, "global:krb5_auth"), false)) {442 if (tiniparser_getboolean(d, "global:krb5_auth", false)) { 433 443 ctrl |= WINBIND_KRB5_AUTH; 434 444 } 435 445 436 if ( iniparser_getboolean(d, CONST_DISCARD(char *, "global:silent"), false)) {446 if (tiniparser_getboolean(d, "global:silent", false)) { 437 447 ctrl |= WINBIND_SILENT; 438 448 } 439 449 440 if ( iniparser_getstring(d, CONST_DISCARD(char *, "global:krb5_ccache_type"), NULL) != NULL) {450 if (tiniparser_getstring_nonempty(d, "global:krb5_ccache_type", NULL) != NULL) { 441 451 ctrl |= WINBIND_KRB5_CCACHE_TYPE; 442 452 } 443 453 444 if (( iniparser_getstring(d, CONST_DISCARD(char *, "global:require-membership-of"), NULL)454 if ((tiniparser_getstring_nonempty(d, "global:require-membership-of", NULL) 445 455 != NULL) || 446 ( iniparser_getstring(d, CONST_DISCARD(char *, "global:require_membership_of"), NULL)456 (tiniparser_getstring_nonempty(d, "global:require_membership_of", NULL) 447 457 != NULL)) { 448 458 ctrl |= WINBIND_REQUIRED_MEMBERSHIP; 449 459 } 450 460 451 if ( iniparser_getboolean(d, CONST_DISCARD(char *, "global:try_first_pass"), false)) {461 if (tiniparser_getboolean(d, "global:try_first_pass", false)) { 452 462 ctrl |= WINBIND_TRY_FIRST_PASS_ARG; 453 463 } 454 464 455 if ( iniparser_getint(d, CONST_DISCARD(char *, "global:warn_pwd_expire"), 0)) {465 if (tiniparser_getint(d, "global:warn_pwd_expire", 0)) { 456 466 ctrl |= WINBIND_WARN_PWD_EXPIRE; 457 467 } 458 468 459 if ( iniparser_getboolean(d, CONST_DISCARD(char *, "global:mkhomedir"), false)) {469 if (tiniparser_getboolean(d, "global:mkhomedir", false)) { 460 470 ctrl |= WINBIND_MKHOMEDIR; 461 471 } … … 480 490 else if (!strcasecmp(*v, "unknown_ok")) 481 491 ctrl |= WINBIND_UNKNOWN_OK_ARG; 482 else if (!strncasecmp(*v, "require_membership_of", 483 strlen("require_membership_of"))) 492 else if ((type == PAM_WINBIND_AUTHENTICATE 493 || type == PAM_WINBIND_SETCRED) 494 && !strncasecmp(*v, "require_membership_of", 495 strlen("require_membership_of"))) 484 496 ctrl |= WINBIND_REQUIRED_MEMBERSHIP; 485 else if (!strncasecmp(*v, "require-membership-of", 486 strlen("require-membership-of"))) 497 else if ((type == PAM_WINBIND_AUTHENTICATE 498 || type == PAM_WINBIND_SETCRED) 499 && !strncasecmp(*v, "require-membership-of", 500 strlen("require-membership-of"))) 487 501 ctrl |= WINBIND_REQUIRED_MEMBERSHIP; 488 502 else if (!strcasecmp(*v, "krb5_auth")) … … 495 509 else if (!strcasecmp(*v, "mkhomedir")) 496 510 ctrl |= WINBIND_MKHOMEDIR; 497 else { 511 else if (!strncasecmp(*v, "warn_pwd_expire", 512 strlen("warn_pwd_expire"))) 513 ctrl |= WINBIND_WARN_PWD_EXPIRE; 514 else if (type != PAM_WINBIND_CLEANUP) { 498 515 __pam_log(pamh, ctrl, LOG_ERR, 499 516 "pam_parse: unknown option: %s", *v); … … 507 524 } else { 508 525 if (d) { 509 iniparser_freedict(d);526 tiniparser_freedict(d); 510 527 } 511 528 } … … 521 538 522 539 if (ctx->dict) { 523 iniparser_freedict(ctx->dict);540 tiniparser_freedict(ctx->dict); 524 541 } 525 542 … … 531 548 int argc, 532 549 const char **argv, 550 enum pam_winbind_request_type type, 533 551 struct pwb_context **ctx_p) 534 552 { … … 539 557 #endif 540 558 541 r = TALLOC_ZERO_P(NULL, struct pwb_context);559 r = talloc_zero(NULL, struct pwb_context); 542 560 if (!r) { 543 561 return PAM_BUF_ERR; … … 550 568 r->argc = argc; 551 569 r->argv = argv; 552 r->ctrl = _pam_parse(pamh, flags, argc, argv, &r->dict);570 r->ctrl = _pam_parse(pamh, flags, argc, argv, type, &r->dict); 553 571 if (r->ctrl == -1) { 554 572 TALLOC_FREE(r); … … 565 583 int error_status) 566 584 { 567 int ctrl = _pam_parse(pamh, 0, 0, NULL, NULL);585 int ctrl = _pam_parse(pamh, 0, 0, NULL, PAM_WINBIND_CLEANUP, NULL); 568 586 if (_pam_log_is_debug_state_enabled(ctrl)) { 569 587 __pam_log_debug(pamh, ctrl, LOG_DEBUG, … … 646 664 struct pam_conv *conv; 647 665 648 retval = _pam_get_item(pamh, PAM_CONV,&conv);666 retval = pam_get_item(pamh, PAM_CONV, (const void **) &conv); 649 667 if (retval == PAM_SUCCESS) { 650 668 retval = conv->conv(nargs, 651 (const struct pam_message **)message,669 discard_const_p(const struct pam_message *, message), 652 670 response, conv->appdata_ptr); 653 671 } … … 813 831 struct pam_message msg, *pmsg; 814 832 struct pam_response *resp = NULL; 815 const char *prompt;816 833 int ret; 817 834 bool retval = false; 818 prompt = _("Do you want to change your password now?");819 835 pmsg = &msg; 820 836 msg.msg_style = PAM_RADIO_TYPE; 821 msg.msg = prompt;837 msg.msg = _("Do you want to change your password now?"); 822 838 ret = converse(ctx->pamh, 1, &pmsg, &resp); 823 839 if (resp == NULL) { … … 1050 1066 int dest_buffer_size) 1051 1067 { 1052 int dest_length = strlen(dest); 1053 int src_length = strlen(src); 1054 1055 if (dest_length + src_length + 1 > dest_buffer_size) { 1056 return false; 1057 } 1058 1059 memcpy(dest + dest_length, src, src_length + 1); 1060 return true; 1068 size_t len; 1069 len = strlcat(dest, src, dest_buffer_size); 1070 return (len < dest_buffer_size); 1061 1071 } 1062 1072 … … 1221 1231 struct wbcLogonUserInfo *info) 1222 1232 { 1223 char var[PATH_MAX];1233 char *var = NULL; 1224 1234 int ret; 1225 1235 uint32_t i; … … 1248 1258 "request returned KRB5CCNAME: %s", krb5ccname); 1249 1259 1250 if ( snprintf(var, sizeof(var), "KRB5CCNAME=%s", krb5ccname) == -1) {1260 if (asprintf(&var, "KRB5CCNAME=%s", krb5ccname) == -1) { 1251 1261 return; 1252 1262 } 1253 1263 1254 1264 ret = pam_putenv(ctx->pamh, var); 1255 if (ret ) {1265 if (ret != PAM_SUCCESS) { 1256 1266 _pam_log(ctx, LOG_ERR, 1257 1267 "failed to set KRB5CCNAME to %s: %s", 1258 1268 var, pam_strerror(ctx->pamh, ret)); 1259 1269 } 1270 free(var); 1260 1271 } 1261 1272 … … 1318 1329 ret = pam_set_data(ctx->pamh, data_name, talloc_strdup(NULL, value), 1319 1330 _pam_winbind_cleanup_func); 1320 if (ret ) {1331 if (ret != PAM_SUCCESS) { 1321 1332 _pam_log_debug(ctx, LOG_DEBUG, 1322 1333 "Could not set data %s: %s\n", … … 1646 1657 1647 1658 ret = _pam_create_homedir(ctx, create_dir, mode); 1648 if (ret ) {1659 if (ret != PAM_SUCCESS) { 1649 1660 return ret; 1650 1661 } … … 1771 1782 "krb5_cc_type", 1772 1783 0, 1773 (uint8_t *)cctype,1784 discard_const_p(uint8_t, cctype), 1774 1785 strlen(cctype)+1); 1775 1786 if (!WBC_ERROR_IS_OK(wbc_status)) { … … 1952 1963 1953 1964 params.account_name = user; 1954 params.level = WBC_ AUTH_USER_LEVEL_PLAIN;1965 params.level = WBC_CHANGE_PASSWORD_LEVEL_PLAIN; 1955 1966 params.old_password.plaintext = oldpass; 1956 1967 params.new_password.plaintext = newpass; … … 1991 2002 1992 2003 /* FIXME: avoid to send multiple PAM messages after another */ 1993 switch ( reject_reason) {2004 switch ((int)reject_reason) { 1994 2005 case -1: 1995 2006 break; … … 2129 2140 if (on(WINBIND_TRY_FIRST_PASS_ARG, ctrl) || 2130 2141 on(WINBIND_USE_FIRST_PASS_ARG, ctrl)) { 2131 retval = _pam_get_item(ctx->pamh, authtok_flag, &item); 2142 retval = pam_get_item(ctx->pamh, 2143 authtok_flag, 2144 (const void **) &item); 2132 2145 if (retval != PAM_SUCCESS) { 2133 2146 /* very strange. */ … … 2237 2250 _pam_delete(token); /* clean it up */ 2238 2251 if (retval != PAM_SUCCESS || 2239 (retval = _pam_get_item(ctx->pamh, authtok_flag,&item)) != PAM_SUCCESS) {2252 (retval = pam_get_item(ctx->pamh, authtok_flag, (const void **) &item)) != PAM_SUCCESS) { 2240 2253 2241 2254 _pam_log(ctx, LOG_CRIT, "error manipulating password"); … … 2287 2300 } 2288 2301 2289 parm_opt = iniparser_getstring(ctx->dict, key, NULL);2302 parm_opt = tiniparser_getstring_nonempty(ctx->dict, key, NULL); 2290 2303 TALLOC_FREE(key); 2291 2304 … … 2335 2348 } 2336 2349 2337 parm_opt = iniparser_getint(ctx->dict, key, -1);2350 parm_opt = tiniparser_getint(ctx->dict, key, -1); 2338 2351 TALLOC_FREE(key); 2339 2352 … … 2357 2370 ret = get_conf_item_string(ctx, "require_membership_of", 2358 2371 WINBIND_REQUIRED_MEMBERSHIP); 2359 if (ret ) {2372 if (ret != NULL) { 2360 2373 return ret; 2361 2374 } … … 2370 2383 WINBIND_WARN_PWD_EXPIRE); 2371 2384 /* no or broken setting */ 2372 if (ret < =0) {2385 if (ret < 0) { 2373 2386 return DEFAULT_DAYS_TO_WARN_BEFORE_PWD_EXPIRES; 2374 2387 } … … 2409 2422 * 2410 2423 * @param ctx PAM winbind context. 2411 * @param upn U Ser UPN to be trabslated.2424 * @param upn User UPN to be translated. 2412 2425 * 2413 2426 * @return converted name. NULL pointer on failure. Caller needs to free. … … 2424 2437 char *name; 2425 2438 char *p; 2439 char *result; 2426 2440 2427 2441 /* This cannot work when the winbind separator = @ */ … … 2455 2469 } 2456 2470 2457 return talloc_asprintf(ctx, "%s%c%s", domain, sep, name); 2471 result = talloc_asprintf(ctx, "%s%c%s", domain, sep, name); 2472 wbcFreeMemory(domain); 2473 wbcFreeMemory(name); 2474 return result; 2458 2475 } 2459 2476 2460 2477 static int _pam_delete_cred(pam_handle_t *pamh, int flags, 2461 int argc, const char **argv) 2478 int argc, enum pam_winbind_request_type type, 2479 const char **argv) 2462 2480 { 2463 2481 int retval = PAM_SUCCESS; … … 2470 2488 ZERO_STRUCT(logoff); 2471 2489 2472 retval = _pam_winbind_init_context(pamh, flags, argc, argv, &ctx);2473 if (retval ) {2474 goto out;2490 retval = _pam_winbind_init_context(pamh, flags, argc, argv, type, &ctx); 2491 if (retval != PAM_SUCCESS) { 2492 return retval; 2475 2493 } 2476 2494 … … 2486 2504 2487 2505 retval = pam_get_user(pamh, &user, _("Username: ")); 2488 if (retval ) {2506 if (retval != PAM_SUCCESS) { 2489 2507 _pam_log(ctx, LOG_ERR, 2490 2508 "could not identify user"); … … 2524 2542 "ccfilename", 2525 2543 0, 2526 (uint8_t *)ccname,2544 discard_const_p(uint8_t, ccname), 2527 2545 strlen(ccname)+1); 2528 2546 if (!WBC_ERROR_IS_OK(wbc_status)) { … … 2605 2623 struct pwb_context *ctx = NULL; 2606 2624 2607 retval = _pam_winbind_init_context(pamh, flags, argc, argv, &ctx); 2608 if (retval) { 2609 goto out; 2625 retval = _pam_winbind_init_context(pamh, flags, argc, argv, 2626 PAM_WINBIND_AUTHENTICATE, &ctx); 2627 if (retval != PAM_SUCCESS) { 2628 return retval; 2610 2629 } 2611 2630 … … 2742 2761 } 2743 2762 2744 if (ctx != NULL) { 2745 _PAM_LOG_FUNCTION_LEAVE("pam_sm_authenticate", ctx, retval); 2746 TALLOC_FREE(ctx); 2747 } 2763 _PAM_LOG_FUNCTION_LEAVE("pam_sm_authenticate", ctx, retval); 2764 2765 TALLOC_FREE(ctx); 2748 2766 2749 2767 return retval; … … 2757 2775 struct pwb_context *ctx = NULL; 2758 2776 2759 ret = _pam_winbind_init_context(pamh, flags, argc, argv, &ctx); 2760 if (ret) { 2761 goto out; 2777 ret = _pam_winbind_init_context(pamh, flags, argc, argv, 2778 PAM_WINBIND_SETCRED, &ctx); 2779 if (ret != PAM_SUCCESS) { 2780 return ret; 2762 2781 } 2763 2782 … … 2767 2786 2768 2787 case PAM_DELETE_CRED: 2769 ret = _pam_delete_cred(pamh, flags, argc, argv); 2788 ret = _pam_delete_cred(pamh, flags, argc, 2789 PAM_WINBIND_SETCRED, argv); 2770 2790 break; 2771 2791 case PAM_REFRESH_CRED: … … 2789 2809 } 2790 2810 2791 out:2792 2793 2811 _PAM_LOG_FUNCTION_LEAVE("pam_sm_setcred", ctx, ret); 2794 2812 … … 2808 2826 const char *username; 2809 2827 int ret = PAM_USER_UNKNOWN; 2810 void*tmp = NULL;2828 const char *tmp = NULL; 2811 2829 struct pwb_context *ctx = NULL; 2812 2830 2813 ret = _pam_winbind_init_context(pamh, flags, argc, argv, &ctx); 2814 if (ret) { 2815 goto out; 2831 ret = _pam_winbind_init_context(pamh, flags, argc, argv, 2832 PAM_WINBIND_ACCT_MGMT, &ctx); 2833 if (ret != PAM_SUCCESS) { 2834 return ret; 2816 2835 } 2817 2836 … … 2849 2868 (const void **)&tmp); 2850 2869 if (tmp != NULL) { 2851 ret = atoi( (const char *)tmp);2870 ret = atoi(tmp); 2852 2871 switch (ret) { 2853 2872 case PAM_AUTHTOK_EXPIRED: … … 2906 2925 struct pwb_context *ctx = NULL; 2907 2926 2908 ret = _pam_winbind_init_context(pamh, flags, argc, argv, &ctx); 2909 if (ret) { 2910 goto out; 2927 ret = _pam_winbind_init_context(pamh, flags, argc, argv, 2928 PAM_WINBIND_OPEN_SESSION, &ctx); 2929 if (ret != PAM_SUCCESS) { 2930 return ret; 2911 2931 } 2912 2932 … … 2917 2937 ret = _pam_mkhomedir(ctx); 2918 2938 } 2919 out: 2939 2920 2940 _PAM_LOG_FUNCTION_LEAVE("pam_sm_open_session", ctx, ret); 2921 2941 … … 2932 2952 struct pwb_context *ctx = NULL; 2933 2953 2934 ret = _pam_winbind_init_context(pamh, flags, argc, argv, &ctx); 2935 if (ret) { 2936 goto out; 2954 ret = _pam_winbind_init_context(pamh, flags, argc, argv, 2955 PAM_WINBIND_CLOSE_SESSION, &ctx); 2956 if (ret != PAM_SUCCESS) { 2957 return ret; 2937 2958 } 2938 2959 2939 2960 _PAM_LOG_FUNCTION_ENTER("pam_sm_close_session", ctx); 2940 2961 2941 out:2942 2962 _PAM_LOG_FUNCTION_LEAVE("pam_sm_close_session", ctx, ret); 2943 2963 … … 2975 2995 struct passwd *pwd = NULL; 2976 2996 2977 _pam_get_data(ctx->pamh, PAM_WINBIND_NEW_AUTHTOK_REQD_DURING_AUTH,2978 &new_authtok_reqd_during_auth);2997 pam_get_data(ctx->pamh, PAM_WINBIND_NEW_AUTHTOK_REQD_DURING_AUTH, 2998 (const void **) &new_authtok_reqd_during_auth); 2979 2999 pam_set_data(ctx->pamh, PAM_WINBIND_NEW_AUTHTOK_REQD_DURING_AUTH, 2980 3000 NULL, NULL); … … 3007 3027 /* <DO NOT free() THESE> */ 3008 3028 const char *user; 3009 char *pass_old, *pass_new; 3029 const char *pass_old; 3030 const char *pass_new; 3010 3031 /* </DO NOT free() THESE> */ 3011 3032 … … 3017 3038 struct pwb_context *ctx = NULL; 3018 3039 3019 ret = _pam_winbind_init_context(pamh, flags, argc, argv, &ctx); 3020 if (ret) { 3021 goto out; 3040 ret = _pam_winbind_init_context(pamh, flags, argc, argv, 3041 PAM_WINBIND_CHAUTHTOK, &ctx); 3042 if (ret != PAM_SUCCESS) { 3043 return ret; 3022 3044 } 3023 3045 … … 3033 3055 */ 3034 3056 ret = pam_get_user(pamh, &user, _("Username: ")); 3035 if (ret ) {3057 if (ret != PAM_SUCCESS) { 3036 3058 _pam_log(ctx, LOG_ERR, 3037 3059 "password - could not identify user"); … … 3130 3152 */ 3131 3153 3132 ret = _pam_get_item(pamh, PAM_OLDAUTHTOK,&pass_old);3154 ret = pam_get_item(pamh, PAM_OLDAUTHTOK, (const void **) &pass_old); 3133 3155 3134 3156 if (ret != PAM_SUCCESS) { … … 3180 3202 * rebuild the password database file. 3181 3203 */ 3182 _pam_get_data(pamh, PAM_WINBIND_PWD_LAST_SET,3183 &pwdlastset_update);3204 pam_get_data(pamh, PAM_WINBIND_PWD_LAST_SET, 3205 (const void **) &pwdlastset_update); 3184 3206 3185 3207 /* … … 3194 3216 ret = winbind_chauthtok_request(ctx, user, pass_old, 3195 3217 pass_new, pwdlastset_update); 3196 if (ret ) {3218 if (ret != PAM_SUCCESS) { 3197 3219 pass_old = pass_new = NULL; 3198 3220 goto out;
Note:
See TracChangeset
for help on using the changeset viewer.