View Javadoc
1   /*
2    * Copyright 2012-2021 CodeLibs Project and the Others.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
13   * either express or implied. See the License for the specific language
14   * governing permissions and limitations under the License.
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.util.LinkedList;
27  import java.util.List;
28  
29  import org.apache.logging.log4j.LogManager;
30  import org.apache.logging.log4j.Logger;
31  import org.codelibs.core.lang.StringUtil;
32  import org.codelibs.core.lang.ThreadUtil;
33  import org.codelibs.fess.crawler.Constants;
34  import org.codelibs.fess.crawler.exception.CrawlerSystemException;
35  import org.codelibs.fess.es.user.exentity.User;
36  import org.codelibs.fess.exception.CommandExecutionException;
37  
38  public class CommandChain implements AuthenticationChain {
39  
40      private static final Logger logger = LogManager.getLogger(CommandChain.class);
41  
42      protected File workingDirectory = null;
43  
44      protected int maxOutputLine = 1000;
45  
46      protected long executionTimeout = 30L * 1000L; // 30sec
47  
48      protected String commandOutputEncoding = System.getProperty("file.encoding");
49  
50      protected String[] updateCommand;
51  
52      protected String[] deleteCommand;
53  
54      protected String[] targetUsers;
55  
56      @Override
57      public void update(final User user) {
58          final String username = user.getName();
59          final String password = user.getOriginalPassword();
60          changePassword(username, password);
61      }
62  
63      @Override
64      public void delete(final User user) {
65          final String username = user.getName();
66          if (isTargetUser(username)) {
67              executeCommand(deleteCommand, username, StringUtil.EMPTY);
68          }
69      }
70  
71      @Override
72      public boolean changePassword(final String username, final String password) {
73          if (isTargetUser(username) && StringUtil.isNotBlank(password)) {
74              return executeCommand(updateCommand, username, password) == 0;
75          }
76          return true;
77      }
78  
79      @Override
80      public User load(final User user) {
81          return user;
82      }
83  
84      protected boolean isTargetUser(final String username) {
85          if (targetUsers == null) {
86              return true;
87          }
88          return stream(targetUsers).get(stream -> stream.anyMatch(s -> s.equals(username)));
89      }
90  
91      protected int executeCommand(final String[] commands, final String username, final String password) {
92          if (commands == null || commands.length == 0) {
93              throw new CommandExecutionException("command is empty.");
94          }
95  
96          if (logger.isInfoEnabled()) {
97              logger.info("Command: {}", String.join(" ", commands));
98          }
99  
100         final String[] cmds = stream(commands).get(stream -> stream.map(s -> {
101             if ("$USERNAME".equals(s)) {
102                 return username;
103             }
104             if ("$PASSWORD".equals(s)) {
105                 return password;
106             }
107             return s;
108         }).toArray(n -> new String[n]));
109         final ProcessBuilder pb = new ProcessBuilder(cmds);
110         if (workingDirectory != null) {
111             pb.directory(workingDirectory);
112         }
113         pb.redirectErrorStream(true);
114 
115         Process currentProcess = null;
116         MonitorThread mt = null;
117         try {
118             currentProcess = pb.start();
119 
120             // monitoring
121             mt = new MonitorThread(currentProcess, executionTimeout);
122             mt.start();
123 
124             final InputStreamThread it = new InputStreamThread(currentProcess.getInputStream(), commandOutputEncoding, maxOutputLine);
125             it.start();
126 
127             currentProcess.waitFor();
128             it.join(5000);
129 
130             if (mt.isTeminated()) {
131                 throw new CommandExecutionException("The command execution is timeout: " + String.join(" ", commands));
132             }
133 
134             final int exitValue = currentProcess.exitValue();
135 
136             if (logger.isInfoEnabled()) {
137                 logger.info("Exit Code: {} - Process Output:\n{}", exitValue, it.getOutput());
138             }
139             if (exitValue == 143 && mt.isTeminated()) {
140                 throw new CommandExecutionException("The command execution is timeout: " + String.join(" ", commands));
141             }
142             return exitValue;
143         } catch (final CrawlerSystemException e) {
144             throw e;
145         } catch (final InterruptedException e) {
146             if (mt != null && mt.isTeminated()) {
147                 throw new CommandExecutionException("The command execution is timeout: " + String.join(" ", commands), e);
148             }
149             throw new CommandExecutionException("Process terminated.", e);
150         } catch (final Exception e) {
151             throw new CommandExecutionException("Process terminated.", e);
152         } finally {
153             if (mt != null) {
154                 mt.setFinished(true);
155                 try {
156                     mt.interrupt();
157                 } catch (final Exception e) {
158                     // ignore
159                 }
160             }
161             if (currentProcess != null) {
162                 try {
163                     currentProcess.destroy();
164                 } catch (final Exception e) {
165                     // ignore
166                 }
167             }
168             currentProcess = null;
169 
170         }
171     }
172 
173     protected static class MonitorThread extends Thread {
174         private final Process process;
175 
176         private final long timeout;
177 
178         private boolean finished = false;
179 
180         private boolean teminated = false;
181 
182         public MonitorThread(final Process process, final long timeout) {
183             this.process = process;
184             this.timeout = timeout;
185         }
186 
187         @Override
188         public void run() {
189             ThreadUtil.sleepQuietly(timeout);
190 
191             if (!finished) {
192                 try {
193                     process.destroy();
194                     teminated = true;
195                 } catch (final Exception e) {
196                     if (logger.isInfoEnabled()) {
197                         logger.info("Could not kill the subprocess.", e);
198                     }
199                 }
200             }
201         }
202 
203         /**
204          * @param finished
205          *            The finished to set.
206          */
207         public void setFinished(final boolean finished) {
208             this.finished = finished;
209         }
210 
211         /**
212          * @return Returns the teminated.
213          */
214         public boolean isTeminated() {
215             return teminated;
216         }
217     }
218 
219     protected static class InputStreamThread extends Thread {
220 
221         private BufferedReader br;
222 
223         private final List<String> list = new LinkedList<>();
224 
225         private final int maxLineBuffer;
226 
227         public InputStreamThread(final InputStream is, final String charset, final int maxOutputLineBuffer) {
228             try {
229                 br = new BufferedReader(new InputStreamReader(is, charset));
230             } catch (final UnsupportedEncodingException e) {
231                 br = new BufferedReader(new InputStreamReader(is, Constants.UTF_8_CHARSET));
232             }
233             maxLineBuffer = maxOutputLineBuffer;
234         }
235 
236         @Override
237         public void run() {
238             for (;;) {
239                 try {
240                     final String line = br.readLine();
241                     if (line == null) {
242                         break;
243                     }
244                     if (logger.isDebugEnabled()) {
245                         logger.debug(line);
246                     }
247                     list.add(line);
248                     if (list.size() > maxLineBuffer) {
249                         list.remove(0);
250                     }
251                 } catch (final IOException e) {
252                     throw new CrawlerSystemException(e);
253                 }
254             }
255         }
256 
257         public String getOutput() {
258             final StringBuilder buf = new StringBuilder(100);
259             for (final String value : list) {
260                 buf.append(value).append("\n");
261             }
262             return buf.toString();
263         }
264 
265     }
266 
267     public void setWorkingDirectory(final File workingDirectory) {
268         this.workingDirectory = workingDirectory;
269     }
270 
271     public void setMaxOutputLine(final int maxOutputLine) {
272         this.maxOutputLine = maxOutputLine;
273     }
274 
275     public void setExecutionTimeout(final long executionTimeout) {
276         this.executionTimeout = executionTimeout;
277     }
278 
279     public void setCommandOutputEncoding(final String commandOutputEncoding) {
280         this.commandOutputEncoding = commandOutputEncoding;
281     }
282 
283     public void setUpdateCommand(final String[] updateCommand) {
284         this.updateCommand = updateCommand;
285     }
286 
287     public void setDeleteCommand(final String[] deleteCommand) {
288         this.deleteCommand = deleteCommand;
289     }
290 
291     public void setTargetUsers(final String[] targetUsers) {
292         this.targetUsers = targetUsers;
293     }
294 
295 }