Handle hostnames with upper-case letters
[webmin.git] / password_change.cgi
1 #!/usr/local/bin/perl
2 # password_change.cgi
3 # Actually update a user's password by directly modifying /etc/shadow
4
5 BEGIN { push(@INC, ".."); };
6 use WebminCore;
7
8 $ENV{'MINISERV_INTERNAL'} || die "Can only be called by miniserv.pl";
9 &init_config();
10 &ReadParse();
11 &get_miniserv_config(\%miniserv);
12 $miniserv{'passwd_mode'} == 2 || die "Password changing is not enabled!";
13
14 # Validate inputs
15 $in{'new1'} ne '' || &pass_error($text{'password_enew1'});
16 $in{'new1'} eq $in{'new2'} || &pass_error($text{'password_enew2'});
17
18 # Is this a Webmin user?
19 if (&foreign_check("acl")) {
20         &foreign_require("acl", "acl-lib.pl");
21         ($wuser) = grep { $_->{'name'} eq $in{'user'} } &acl::list_users();
22         if ($wuser->{'pass'} eq 'x') {
23                 # A Webmin user, but using Unix authentication
24                 $wuser = undef;
25                 }
26         elsif ($wuser->{'pass'} eq '*LK*' ||
27                $wuser->{'pass'} =~ /^\!/) {
28                 &pass_error("Webmin users with locked accounts cannot change ".
29                             "their passwords!");
30                 }
31         }
32 if (!$in{'pam'} && !$wuser) {
33         $miniserv{'passwd_cindex'} ne '' && $miniserv{'passwd_mindex'} ne '' || 
34                 die "Missing password file configuration";
35         }
36
37 if ($wuser) {
38         # Update Webmin user's password
39         $enc = &acl::encrypt_password($in{'old'}, $wuser->{'pass'});
40         $enc eq $wuser->{'pass'} || &pass_error($text{'password_eold'});
41         $perr = &acl::check_password_restrictions($in{'user'}, $in{'new1'});
42         $perr && &pass_error(&text('password_enewpass', $perr));
43         $wuser->{'pass'} = &acl::encrypt_password($in{'new1'});
44         $wuser->{'temppass'} = 0;
45         &acl::modify_user($wuser->{'name'}, $wuser);
46         &reload_miniserv();
47         }
48 elsif ($gconfig{'passwd_cmd'}) {
49         # Use some configured command
50         $passwd_cmd = &has_command($gconfig{'passwd_cmd'});
51         $passwd_cmd || &pass_error("The password change command <tt>$gconfig{'passwd_cmd'}</tt> was not found");
52
53         &foreign_require("proc", "proc-lib.pl");
54         &clean_environment();
55         $ENV{'REMOTE_USER'} = $in{'user'};      # some programs need this
56         $passwd_cmd .= " ".quotemeta($in{'user'});
57         ($fh, $fpid) = &proc::pty_process_exec($passwd_cmd, 0, 0);
58         &reset_environment();
59         while(1) {
60                 local $rv = &wait_for($fh,
61                            '(new|re-enter).*:',
62                            '(old|current|login).*:',
63                            'pick a password',
64                            'too\s+many\s+failures',
65                            'attributes\s+changed\s+on|successfully\s+changed',
66                            'pick your passwords');
67                 $out .= $wait_for_input;
68                 sleep(1);
69                 if ($rv == 0) {
70                         # Prompt for the new password
71                         syswrite($fh, $in{'new1'}."\n", length($in{'new1'})+1);
72                         }
73                 elsif ($rv == 1) {
74                         # Prompt for the old password
75                         syswrite($fh, $in{'old'}."\n", length($in{'old'})+1);
76                         }
77                 elsif ($rv == 2) {
78                         # Request for a menu option (SCO?)
79                         syswrite($fh, "1\n", 2);
80                         }
81                 elsif ($rv == 3) {
82                         # Failed too many times
83                         last;
84                         }
85                 elsif ($rv == 4) {
86                         # All done
87                         last;
88                         }
89                 elsif ($rv == 5) {
90                         # Request for a menu option (HP/UX)
91                         syswrite($fh, "p\n", 2);
92                         }
93                 else {
94                         last;
95                         }
96                 last if (++$count > 10);
97                 }
98         $crv = close($fh);
99         sleep(1);
100         waitpid($fpid, 1);
101         if ($? || $count > 10 ||
102             $out =~ /error|failed/i || $out =~ /bad\s+password/i) {
103                 &pass_error("<tt>".&html_escape($out)."</tt>");
104                 }
105         }
106 elsif ($in{'pam'}) {
107         # Use PAM to make the change..
108         eval "use Authen::PAM;";
109         if ($@) {
110                 &pass_error(&text('password_emodpam', $@));
111                 }
112
113         # Check if the old password is correct
114         $service = $miniserv{'pam'} ? $miniserv{'pam'} : "webmin";
115         $pamh = new Authen::PAM($service, $in{'user'}, \&pam_check_func);
116         $rv = $pamh->pam_authenticate();
117         $rv == PAM_SUCCESS() ||
118                 &pass_error($text{'password_eold'});
119         $pamh = undef;
120
121         # Change the password with PAM, in a sub-process. This is needed because
122         # the UID must be changed to properly signal to the PAM libraries that
123         # the password change is not being done by the root user.
124         $temp = &transname();
125         $pid = fork();
126         @uinfo = getpwnam($in{'user'});
127         if (!$pid) {
128                 ($>, $<) = (0, $uinfo[2]);
129                 $pamh = new Authen::PAM("passwd", $in{'user'}, \&pam_change_func);
130                 $rv = $pamh->pam_chauthtok();
131                 open(TEMP, ">$temp");
132                 print TEMP "$rv\n";
133                 print TEMP ($messages || $pamh->pam_strerror($rv)),"\n";
134                 close(TEMP);
135                 exit(0);
136                 }
137         waitpid($pid, 0);
138         open(TEMP, $temp);
139         chop($rv = <TEMP>);
140         chop($messages = <TEMP>);
141         close(TEMP);
142         unlink($temp);
143         $rv == PAM_SUCCESS || &pass_error(&text('password_epam', $messages));
144         $pamh = undef;
145         }
146 else {
147         # Directly update password file
148
149         # Read shadow file and find user
150         &lock_file($miniserv{'passwd_file'});
151         $lref = &read_file_lines($miniserv{'passwd_file'});
152         for($i=0; $i<@$lref; $i++) {
153                 @line = split(/:/, $lref->[$i], -1);
154                 local $u = $line[$miniserv{'passwd_uindex'}];
155                 if ($u eq $in{'user'}) {
156                         $idx = $i;
157                         last;
158                         }
159                 }
160         defined($idx) || &pass_error($text{'password_euser'});
161
162         # Validate old password
163         &unix_crypt($in{'old'}, $line[$miniserv{'passwd_pindex'}]) eq
164                 $line[$miniserv{'passwd_pindex'}] ||
165                         &pass_error($text{'password_eold'});
166
167         # Make sure new password meets restrictions
168         if (&foreign_check("changepass")) {
169                 &foreign_require("changepass", "changepass-lib.pl");
170                 $err = &changepass::check_password($in{'new1'}, $in{'user'});
171                 &pass_error($err) if ($err);
172                 }
173         elsif (&foreign_check("useradmin")) {
174                 &foreign_require("useradmin", "user-lib.pl");
175                 $err = &useradmin::check_password_restrictions(
176                                 $in{'new1'}, $in{'user'});
177                 &pass_error($err) if ($err);
178                 }
179
180         # Set new password and save file
181         $salt = chr(int(rand(26))+65) . chr(int(rand(26))+65);
182         $line[$miniserv{'passwd_pindex'}] = &unix_crypt($in{'new1'}, $salt);
183         $days = int(time()/(24*60*60));
184         $line[$miniserv{'passwd_cindex'}] = $days;
185         $lref->[$idx] = join(":", @line);
186         &flush_file_lines();
187         &unlock_file($miniserv{'passwd_file'});
188         }
189
190 # Change password in Usermin too
191 if (&get_product_name() eq 'usermin' &&
192     &foreign_check("changepass")) {
193         # XXX remote user??
194         &foreign_require("changepass", "changepass-lib.pl");
195         &changepass::change_mailbox_passwords(
196                 $in{'user'}, $in{'old'}, $in{'new1'});
197         }
198
199 # Show ok page
200 &header(undef, undef, undef, undef, 1, 1);
201
202 print "<center><h3>",&text('password_done', "/"),"</h3></center>\n";
203
204 &footer();
205
206 sub pass_error
207 {
208 &header(undef, undef, undef, undef, 1, 1);
209 print &ui_hr();
210
211 print "<center><h3>",$text{'password_err'}," : ",@_,"</h3></center>\n";
212
213 print &ui_hr();
214 &footer();
215 exit;
216 }
217
218 sub pam_check_func
219 {
220 my @res;
221 while ( @_ ) {
222         my $code = shift;
223         my $msg = shift;
224         my $ans = "";
225
226         $ans = $in{'user'} if ($code == PAM_PROMPT_ECHO_ON());
227         $ans = $in{'old'} if ($code == PAM_PROMPT_ECHO_OFF());
228
229         push @res, PAM_SUCCESS();
230         push @res, $ans;
231         }
232 push @res, PAM_SUCCESS();
233 return @res;
234 }
235
236 sub pam_change_func
237 {
238 my @res;
239 while ( @_ ) {
240         my $code = shift;
241         my $msg = shift;
242         my $ans = "";
243         $messages = $msg;
244
245         if ($code == PAM_PROMPT_ECHO_ON()) {
246                 # Assume asking for username
247                 push @res, PAM_SUCCESS();
248                 push @res, $in{'user'};
249                 }
250         elsif ($code == PAM_PROMPT_ECHO_OFF()) {
251                 # Assume asking for a password (old first, then new)
252                 push @res, PAM_SUCCESS();
253                 if ($msg =~ /old|current/i) {
254                         push @res, $in{'old'};
255                         }
256                 else {
257                         push @res, $in{'new1'};
258                         }
259                 }
260         else {
261                 # Some message .. ignore it
262                 push @res, PAM_SUCCESS();
263                 push @res, undef;
264                 }
265         }
266 push @res, PAM_SUCCESS();
267 return @res;
268 }
269