-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathfunctions.php
More file actions
220 lines (200 loc) · 7.63 KB
/
functions.php
File metadata and controls
220 lines (200 loc) · 7.63 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
<?php
// Shared utilities for authentication and command execution
// show any PHP errors so login problems are visible immediately
ini_set('display_errors', '1');
ini_set('display_startup_errors', '1');
error_reporting(E_ALL);
session_start();
// hold a human-readable error from the last authentication attempt
$lastAuthError = '';
// record a failed authentication attempt; message is logged to PHP/syslog
function log_auth_failure(string $user, string $reason): void
{
error_log("[lvm_nfs] login failure user=$user reason=$reason");
}
/**
* Authenticate a system user using /etc/shadow via getent.
* Returns true on success, false otherwise. On failure a message
* is stored in global $lastAuthError which callers can inspect.
*/
function authenticate(string $user, string $password): bool
{
global $lastAuthError;
// getent shadow requires root privileges. we run via sudo so the
// webserver user can be granted just this command. Always use absolute
// paths (sudo may have a restricted PATH) and log the command output for
// troubleshooting.
$escaped = escapeshellarg($user);
$cmd = "/usr/bin/sudo /usr/bin/getent shadow $escaped";
// execute and capture output
$output = [];
$status = null;
exec($cmd . ' 2>&1', $output, $status);
if ($status !== 0) {
$msg = date('[Y-m-d H:i:s] ') . "cmd=$cmd status=$status output=" .
implode("|", $output) . "\n";
// attempt to write to debug file, fall back to error_log if not writable
if (@file_put_contents('/tmp/lvm_nfs_debug.log', $msg, FILE_APPEND) === false) {
error_log("[lvm_nfs] failed to write debug log: $msg");
}
$lastAuthError = "getent failed (status $status) - check permissions or sudoers entry";
log_auth_failure($user, $lastAuthError);
// include the raw output for visibility
if (!empty($output)) {
$lastAuthError .= ' (output: ' . htmlspecialchars(implode(' | ', $output)) . ')';
}
return false;
}
if (count($output) === 0) {
$lastAuthError = "user not found in shadow";
log_auth_failure($user, $lastAuthError);
return false;
}
// line looks like: username:hash:...
$parts = explode(':', $output[0]);
if (count($parts) < 2 || empty($parts[1])) {
$lastAuthError = "no hash available in shadow entry";
log_auth_failure($user, $lastAuthError);
return false;
}
$hash = $parts[1];
// verify using PHP's crypt()
$computed = @crypt($password, $hash);
if ($computed === $hash) {
// successful password; enforce nfs group membership
if (!user_in_group($user, 'nfs')) {
$lastAuthError = "user $user is not authorized to use this interface";
log_auth_failure($user, $lastAuthError);
return false;
}
$_SESSION['user'] = $user;
return true;
}
// if PHP failed (or produced a trivial value), try external helpers
$helpers = [];
// first try python3 if available
$helpers[] = "/usr/bin/sudo /usr/bin/python3 -c \"import crypt,sys;print(crypt.crypt(sys.argv[1],sys.argv[2]))\"";
// then try perl which has built-in crypt
$helpers[] = "/usr/bin/sudo /usr/bin/perl -e \"print crypt(\$ARGV[0],\$ARGV[1])\"";
$helperResults = [];
foreach ($helpers as $helper) {
$cmd = $helper . ' ' . escapeshellarg($password) . ' ' . escapeshellarg($hash);
$out = [];
$st = null;
exec($cmd . ' 2>&1', $out, $st);
$helperResults[] = [
'cmd' => $cmd,
'status' => $st,
'output' => $out,
];
if ($st === 0 && count($out) > 0 && trim($out[0]) === $hash) {
if (!user_in_group($user, 'nfs')) {
$lastAuthError = "user $user is not authorized to use this interface";
log_auth_failure($user, $lastAuthError);
return false;
}
$_SESSION['user'] = $user;
return true;
}
}
// if previous helpers didn't match, try pamtester if installed
if (file_exists('/usr/bin/pamtester')) {
// feed password on stdin
$cmd = "/bin/sh -c " . escapeshellarg("printf '%s\\n' " . escapeshellarg($password) . " | sudo pamtester login " . escapeshellarg($user) . " authenticate");
$out = [];
$st = null;
exec($cmd . ' 2>&1', $out, $st);
$helperResults[] = [
'cmd' => $cmd,
'status' => $st,
'output' => $out,
];
// pamtester returns 0 on success
if ($st === 0) {
if (!user_in_group($user, 'nfs')) {
$lastAuthError = "user $user is not authorized to use this interface";
log_auth_failure($user, $lastAuthError);
return false;
}
$_SESSION['user'] = $user;
return true;
}
}
// build failure message with details
$lastAuthError = "password did not match";
log_auth_failure($user, $lastAuthError);
$lastAuthError .= ' (stored='.htmlspecialchars(substr($hash,0,20)).'..., computed='.htmlspecialchars(substr($computed,0,20)).'...)';
// append helper diagnostic info
foreach ($helperResults as $hr) {
$lastAuthError .= ' [' . htmlspecialchars($hr['cmd']) . ' => status=' . $hr['status'] . ' output="' . htmlspecialchars(implode(' | ', $hr['output'])) . '"]';
}
return false;
}
function require_login()
{
// decide whether this is an AJAX/JSON request; many of our
// client-side calls include an "ajax=1" parameter but we also
// accept XMLHttpRequest headers for future compatibility.
$isAjax = !empty($_REQUEST['ajax'])
|| !empty($_REQUEST['json'])
|| (isset($_SERVER['HTTP_X_REQUESTED_WITH'])
&& strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest');
if (empty($_SESSION['user'])) {
if ($isAjax) {
// caller will handle redirect client-side
header('HTTP/1.1 401 Unauthorized');
} else {
header('Location: login.php');
}
exit;
}
// if membership was revoked while session active, treat as logged out
if (!user_in_group($_SESSION['user'], 'nfs')) {
session_destroy();
if ($isAjax) {
header('HTTP/1.1 401 Unauthorized');
} else {
header('Location: login.php');
}
exit;
}
}
/**
* Return true if the specified user belongs to the given group.
* This uses the `id -nG` command which reads /etc/group and does not
* require special privileges.
*/
function user_in_group(string $user, string $group): bool
{
// `id -nG` prints space-separated group names for the account.
$cmd = 'id -nG ' . escapeshellarg($user);
$out = [];
$status = null;
exec($cmd . ' 2>&1', $out, $status);
if ($status !== 0 || count($out) === 0) {
return false;
}
$groups = preg_split('/\s+/', trim($out[0]));
return in_array($group, $groups, true);
}
/**
* Run a shell command safely and return output lines. In production
* you may want to log these and validate parameters more carefully.
*/
function run_cmd(string $cmd): array
{
// if we're invoking sudo, add -n to avoid password prompts in a noninteractive
// environment; if sudo would ask for a password it will instead fail quickly.
if (strpos($cmd, 'sudo ') === 0) {
$cmd = str_replace('sudo ', 'sudo -n ', $cmd);
}
$output = [];
$status = null;
exec($cmd . ' 2>&1', $output, $status);
if ($status !== 0) {
// include status line for debugging
$output[] = "(exit $status)";
}
return $output;
}
?>