1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 package org.codelibs.fess.auth.chain;
17
18 import static org.codelibs.core.stream.StreamUtil.stream;
19
20 import java.io.BufferedReader;
21 import java.io.File;
22 import java.io.IOException;
23 import java.io.InputStream;
24 import java.io.InputStreamReader;
25 import java.io.UnsupportedEncodingException;
26 import java.nio.charset.Charset;
27 import java.util.LinkedList;
28 import java.util.List;
29
30 import org.apache.logging.log4j.LogManager;
31 import org.apache.logging.log4j.Logger;
32 import org.codelibs.core.exception.InterruptedRuntimeException;
33 import org.codelibs.core.lang.StringUtil;
34 import org.codelibs.core.lang.ThreadUtil;
35 import org.codelibs.fess.crawler.Constants;
36 import org.codelibs.fess.crawler.exception.CrawlerSystemException;
37 import org.codelibs.fess.exception.CommandExecutionException;
38 import org.codelibs.fess.opensearch.user.exentity.User;
39
40
41
42
43
44 public class CommandChain implements AuthenticationChain {
45
46 private static final Logger logger = LogManager.getLogger(CommandChain.class);
47
48
49 protected File workingDirectory = null;
50
51
52 protected int maxOutputLine = 1000;
53
54
55 protected long executionTimeout = 30L * 1000L;
56
57
58 protected String commandOutputEncoding = Charset.defaultCharset().displayName();
59
60
61 protected String[] updateCommand;
62
63
64 protected String[] deleteCommand;
65
66
67 protected String[] targetUsers;
68
69 @Override
70 public void update(final User user) {
71 final String username = user.getName();
72 final String password = user.getOriginalPassword();
73 changePassword(username, password);
74 }
75
76 @Override
77 public void delete(final User user) {
78 final String username = user.getName();
79 if (isTargetUser(username)) {
80 executeCommand(deleteCommand, username, StringUtil.EMPTY);
81 }
82 }
83
84 @Override
85 public boolean changePassword(final String username, final String password) {
86 if (isTargetUser(username) && StringUtil.isNotBlank(password)) {
87 return executeCommand(updateCommand, username, password) == 0;
88 }
89 return true;
90 }
91
92 @Override
93 public User load(final User user) {
94 return user;
95 }
96
97
98
99
100 public CommandChain() {
101
102 }
103
104
105
106
107
108
109 protected boolean isTargetUser(final String username) {
110 if (targetUsers == null) {
111 return true;
112 }
113 return stream(targetUsers).get(stream -> stream.anyMatch(s -> s.equals(username)));
114 }
115
116
117
118
119
120
121
122
123 protected int executeCommand(final String[] commands, final String username, final String password) {
124 if (commands == null || commands.length == 0) {
125 throw new CommandExecutionException("Command array is null or empty. At least one command must be provided.");
126 }
127
128
129 if (logger.isDebugEnabled()) {
130 final String commandStr = stream(commands).get(stream -> stream.map(s -> {
131 if ("$PASSWORD".equals(s)) {
132 return "***MASKED***";
133 }
134 if ("$USERNAME".equals(s)) {
135 return username;
136 }
137 return s;
138 }).collect(java.util.stream.Collectors.joining(" ")));
139 logger.debug("Executing command for user: username={}, command={}", username, commandStr);
140 }
141
142 final String[] cmds = stream(commands).get(stream -> stream.map(s -> {
143 if ("$USERNAME".equals(s)) {
144 return username;
145 }
146 if ("$PASSWORD".equals(s)) {
147 return password;
148 }
149 return s;
150 }).toArray(n -> new String[n]));
151 final ProcessBuilder pb = new ProcessBuilder(cmds);
152 if (workingDirectory != null) {
153 pb.directory(workingDirectory);
154 }
155 pb.redirectErrorStream(true);
156
157 Process currentProcess = null;
158 MonitorThread mt = null;
159 try {
160 currentProcess = pb.start();
161
162
163 mt = new MonitorThread(currentProcess, executionTimeout);
164 mt.start();
165
166 final InputStreamThread it = new InputStreamThread(currentProcess.getInputStream(), commandOutputEncoding, maxOutputLine);
167 it.start();
168
169 currentProcess.waitFor();
170 it.join(5000);
171
172 if (mt.isTeminated()) {
173 if (logger.isDebugEnabled()) {
174 logger.debug("Command execution timeout for user: username={}", username);
175 }
176 throw new CommandExecutionException("The command execution is timeout for user: " + username);
177 }
178
179 final int exitValue = currentProcess.exitValue();
180
181 if (logger.isInfoEnabled()) {
182 logger.info("Command execution completed for user: username={}, exitCode={}", username, exitValue);
183 }
184 if (logger.isDebugEnabled()) {
185 logger.debug("Process output:\n{}", it.getOutput());
186 }
187 if (exitValue == 143 && mt.isTeminated()) {
188 if (logger.isDebugEnabled()) {
189 logger.debug("Command execution timeout (exit 143) for user: username={}", username);
190 }
191 throw new CommandExecutionException("The command execution is timeout for user: " + username);
192 }
193 return exitValue;
194 } catch (final CrawlerSystemException e) {
195 throw e;
196 } catch (final InterruptedException e) {
197 if (mt != null && mt.isTeminated()) {
198 if (logger.isDebugEnabled()) {
199 logger.debug("Command execution interrupted due to timeout for user: username={}", username, e);
200 }
201 throw new CommandExecutionException("The command execution is timeout for user: " + username, e);
202 }
203 throw new InterruptedRuntimeException(e);
204 } catch (final Exception e) {
205 if (logger.isDebugEnabled()) {
206 logger.debug("Command execution failed for user: username={}, error={}", username, e.getMessage(), e);
207 }
208 throw new CommandExecutionException("Process terminated for user: " + username, e);
209 } finally {
210 if (mt != null) {
211 mt.setFinished(true);
212 try {
213 mt.interrupt();
214 } catch (final Exception e) {
215
216 }
217 }
218 if (currentProcess != null) {
219 try {
220 currentProcess.destroy();
221 } catch (final Exception e) {
222
223 }
224 }
225 currentProcess = null;
226
227 }
228 }
229
230
231
232
233
234 protected static class MonitorThread extends Thread {
235
236 private final Process process;
237
238
239 private final long timeout;
240
241
242 private boolean finished = false;
243
244
245 private boolean teminated = false;
246
247
248
249
250
251
252 public MonitorThread(final Process process, final long timeout) {
253 this.process = process;
254 this.timeout = timeout;
255 }
256
257
258
259
260 @Override
261 public void run() {
262 ThreadUtil.sleepQuietly(timeout);
263
264 if (!finished) {
265 try {
266 process.destroy();
267 teminated = true;
268 } catch (final Exception e) {
269 if (logger.isInfoEnabled()) {
270 logger.info("Could not kill subprocess: process={}", process, e);
271 }
272 }
273 }
274 }
275
276
277
278
279
280 public void setFinished(final boolean finished) {
281 this.finished = finished;
282 }
283
284
285
286
287
288 public boolean isTeminated() {
289 return teminated;
290 }
291 }
292
293
294
295
296
297 protected static class InputStreamThread extends Thread {
298
299
300 private BufferedReader br;
301
302
303 private final List<String> list = new LinkedList<>();
304
305
306 private final int maxLineBuffer;
307
308
309
310
311
312
313
314 public InputStreamThread(final InputStream is, final String charset, final int maxOutputLineBuffer) {
315 try {
316 br = new BufferedReader(new InputStreamReader(is, charset));
317 } catch (final UnsupportedEncodingException e) {
318 br = new BufferedReader(new InputStreamReader(is, Constants.UTF_8_CHARSET));
319 }
320 maxLineBuffer = maxOutputLineBuffer;
321 }
322
323
324
325
326 @Override
327 public void run() {
328 for (;;) {
329 try {
330 final String line = br.readLine();
331 if (line == null) {
332 break;
333 }
334 if (logger.isDebugEnabled()) {
335 logger.debug(line);
336 }
337 list.add(line);
338 if (list.size() > maxLineBuffer) {
339 list.remove(0);
340 }
341 } catch (final IOException e) {
342 throw new CrawlerSystemException(e);
343 }
344 }
345 }
346
347
348
349
350
351 public String getOutput() {
352 final StringBuilder buf = new StringBuilder(100);
353 for (final String value : list) {
354 buf.append(value).append("\n");
355 }
356 return buf.toString();
357 }
358
359 }
360
361
362
363
364
365 public void setWorkingDirectory(final File workingDirectory) {
366 this.workingDirectory = workingDirectory;
367 }
368
369
370
371
372
373 public void setMaxOutputLine(final int maxOutputLine) {
374 this.maxOutputLine = maxOutputLine;
375 }
376
377
378
379
380
381 public void setExecutionTimeout(final long executionTimeout) {
382 this.executionTimeout = executionTimeout;
383 }
384
385
386
387
388
389 public void setCommandOutputEncoding(final String commandOutputEncoding) {
390 this.commandOutputEncoding = commandOutputEncoding;
391 }
392
393
394
395
396
397 public void setUpdateCommand(final String[] updateCommand) {
398 this.updateCommand = updateCommand;
399 }
400
401
402
403
404
405 public void setDeleteCommand(final String[] deleteCommand) {
406 this.deleteCommand = deleteCommand;
407 }
408
409
410
411
412
413 public void setTargetUsers(final String[] targetUsers) {
414 this.targetUsers = targetUsers;
415 }
416
417 }