1   package com.trendmicro.grid.acl.ds.cifs;
2   
3   import com.trendmicro.grid.acl.commons.Processes;
4   import jcifs.UniAddress;
5   import jcifs.smb.*;
6   import org.slf4j.Logger;
7   import org.slf4j.LoggerFactory;
8   
9   import java.net.MalformedURLException;
10  import java.net.UnknownHostException;
11  import java.util.concurrent.Semaphore;
12  import java.util.concurrent.TimeUnit;
13  
14  /**
15   * Handles login and re-login to a cifs host using an asynchronous operation.
16   *
17   * @author juergen_kellerer, 2010-12-01
18   * @version 1.0
19   */
20  public class CIFSLoginHandler {
21  
22  	private static final Logger log = LoggerFactory.getLogger(CIFSLoginHandler.class);
23  
24  	private final String hostname;
25  	private final String shareName;
26  
27  	private String cifsRepositoryBaseURI;
28  	private SmbFile repositoryBasePath;
29  
30  	private UniAddress cifsAddress;
31  	private NtlmPasswordAuthentication authentication;
32  
33  	private final Semaphore loginLock = new Semaphore(1);
34  
35  	/**
36  	 * Constructs a new login handler for the given parameters.
37  	 *
38  	 * @param hostname  The hostname of the CIFS host to connect to (may include the port number).
39  	 * @param shareName The name of the share on the CIFS host to login to.
40  	 */
41  	public CIFSLoginHandler(String hostname, String shareName) {
42  		this.hostname = hostname;
43  		this.shareName = shareName;
44  	}
45  
46  	/**
47  	 * Returns the authenticated base path to use for accessing the repository.
48  	 *
49  	 * @return the authenticated base path to use for accessing the repository.
50  	 * @throws SmbException		  In case of the authentication failed.
51  	 * @throws MalformedURLException In case of the SMB address has an invalid format.
52  	 */
53  	public SmbFile getAuthenticatedRepositoryBasePath() throws MalformedURLException, SmbException {
54  		reLoginIfRequired();
55  		return repositoryBasePath;
56  	}
57  
58  	/**
59  	 * Returns the authenticated base uri to use for accessing the repository.
60  	 *
61  	 * @return the authenticated base uri to use for accessing the repository.
62  	 * @throws SmbException		  In case of the authentication failed.
63  	 * @throws MalformedURLException In case of the SMB address has an invalid format.
64  	 */
65  	public String getAuthenticatedRepositoryBaseURI() throws MalformedURLException, SmbException {
66  		reLoginIfRequired();
67  		return cifsRepositoryBaseURI;
68  	}
69  
70  	/**
71  	 * Initiates a login and waits until it succeeded.
72  	 *
73  	 * @param domain   the domain of the user to login.
74  	 * @param username the username used for authentication.
75  	 * @param password the password used for authentication.
76  	 * @throws SmbException		  In case of the authentication failed.
77  	 * @throws UnknownHostException  In case of the host in the given SMB address is invalid.
78  	 * @throws MalformedURLException In case of the SMB address has an invalid format.
79  	 */
80  	public synchronized void login(String domain, String username, String password)
81  		throws MalformedURLException, UnknownHostException, SmbException {
82  		doLogin(domain, username, password);
83  	}
84  
85  	/**
86  	 * Initiates a login and returns immediately.
87  	 * <p/>
88  	 * All errors that may occure are logged.
89  	 *
90  	 * @param domain   the domain of the user to login.
91  	 * @param username the username used for authentication.
92  	 * @param password the password used for authentication.
93  	 */
94  	public void loginAsynchronously(final String domain, final String username, final String password) {
95  		acquireLoginLock();
96  		Processes.getInstance().submit(new Runnable() {
97  
98  			long retryWaitMinutes;
99  			boolean needsUnlock = true;
100 
101 			public void run() {
102 				try {
103 					doLogin(domain, username, password);
104 					log.info("TMACL-01190:Successfully connected with CIFS host {}, repository {}",
105 						hostname, cifsRepositoryBaseURI);
106 				} catch (UnknownHostException e) {
107 					log.error("TMACL-01200:Failed to connect the CIFS host '" + hostname +
108 						"', the hostname is unknown");
109 				} catch (Exception e) {
110 					CIFSFileRepository.fixExceptionChain(e);
111 					final Object cifsHost = cifsAddress == null ? hostname : cifsAddress;
112 
113 					if (e.getCause() != null && e.getCause().getCause() instanceof java.net.ConnectException) {
114 						log.error("TMACL-01210:Failed to connect the CIFS host '{}', " +
115 							"the socket connection could not be established, reason: \"{}\". " +
116 							"This is usually caused by a firewall or an invalid IP address.",
117 							cifsHost, e.getCause().getCause().getMessage());
118 					} else {
119 						log.error("TMACL-01220:Failed to connect the CIFS host '" + cifsHost +
120 							"' the remote file repostory may be unavailable", e);
121 					}
122 				} finally {
123 					if (cifsRepositoryBaseURI == null) {
124 						retryWaitMinutes += 5;
125 						Processes.getInstance().schedule(this, retryWaitMinutes, TimeUnit.MINUTES);
126 						if (log.isDebugEnabled()) {
127 							log.debug("Scheduled a retry to connect to CIFS host '{}' in {} minutes",
128 								hostname, retryWaitMinutes);
129 						}
130 					} else {
131 						if (needsUnlock) {
132 							loginLock.release();
133 							needsUnlock = false;
134 						}
135 					}
136 				}
137 			}
138 		});
139 	}
140 
141 	/**
142 	 * Forces the next call to perform a re-login.
143 	 */
144 	public void forceReLogin() {
145 		acquireLoginLock();
146 		try {
147 			repositoryBasePath = null;
148 		} finally {
149 			loginLock.release();
150 		}
151 	}
152 
153 	private void reLoginIfRequired() throws MalformedURLException, SmbException {
154 		if (repositoryBasePath == null) {
155 			acquireLoginLock();
156 			try {
157 				loginAndSetBasePathIfRequired();
158 			} finally {
159 				loginLock.release();
160 			}
161 		}
162 	}
163 
164 	private void acquireLoginLock() {
165 		try {
166 			loginLock.acquire();
167 		} catch (InterruptedException e) {
168 			Thread.interrupted();
169 			throw new RuntimeException(e);
170 		}
171 	}
172 
173 	private synchronized void doLogin(String domain, String username, String password)
174 		throws SmbException, UnknownHostException, MalformedURLException {
175 
176 		if (log.isTraceEnabled())
177 			log.trace("Resolving CIFS address for hostname '{}'", hostname);
178 
179 		cifsAddress = UniAddress.getByName(hostname);
180 
181 		if (username == null || username.isEmpty()) {
182 			authentication = new NtlmPasswordAuthentication(null);
183 			if (log.isTraceEnabled()) {
184 				log.trace("Logon to CIFS host using default credentials: " +
185 					"domain: '{}' username: '{}' and password: '{}'", new Object[]{
186 					authentication.getDomain(), authentication.getUsername(), authentication.getPassword()
187 				});
188 			}
189 		} else {
190 			// Normalizing the domain value to "?" for "no-domain", as otherwise jCIFS has a
191 			// serious performance issue as it will try to find "forbidden" domains by opening 2
192 			// new threads per connection, though this doesn't work in a non-domain env.
193 			domain = (domain == null || domain.isEmpty() || domain.equals("*")) ? "?" : domain;
194 			authentication = new NtlmPasswordAuthentication(domain, username, password);
195 		}
196 
197 		log.info("TMACL-01160:Connecting with CIFS server 'smb://{}/{}', using '{}\\\\{}'",
198 			new Object[]{hostname, shareName, authentication.getDomain(), authentication.getUsername()});
199 
200 		loginAndSetBasePathIfRequired();
201 		cifsRepositoryBaseURI = repositoryBasePath.getCanonicalPath();
202 	}
203 
204 	private void loginAndSetBasePathIfRequired() throws SmbException, MalformedURLException {
205 		if (repositoryBasePath == null) {
206 			synchronized (this) {
207 				if (cifsAddress == null) {
208 					throw new IllegalStateException("A connection with the CIFS server was not initiated. " +
209 						"No valid address to the cifs server was defined. " +
210 						"See previous log output for further details what failed.");
211 				}
212 
213 				if (repositoryBasePath == null) {
214 					try {
215 						if (log.isTraceEnabled())
216 							log.trace("Trying to logon to {}, using {}", cifsAddress, authentication);
217 						SmbSession.logon(cifsAddress, authentication);
218 					} catch (SmbException e) {
219 						logLoginException(e);
220 						throw e;
221 					}
222 
223 					String baseURI = String.format("smb://%s/%s", cifsAddress.getHostAddress(), shareName);
224 					if (log.isTraceEnabled()) log.trace("Looking up base path under {}", baseURI);
225 					SmbFile repositoryBasePath = new SmbFile(baseURI, authentication);
226 
227 					boolean basePathExists = false;
228 					try {
229 						basePathExists = repositoryBasePath.isDirectory();
230 					} catch (SmbException e) {
231 						logAccessException(baseURI, e);
232 					}
233 
234 					if (!basePathExists) {
235 						log.warn("TMACL-01180:The repository base path doesn't exist under '{}', " +
236 							"file operations will probably fail.", repositoryBasePath);
237 					}
238 
239 					this.repositoryBasePath = repositoryBasePath;
240 				}
241 			}
242 		}
243 	}
244 
245 	private void logAccessException(String baseURI, SmbException e) throws SmbException {
246 		switch (e.getNtStatus()) {
247 			case NtStatus.NT_STATUS_BAD_NETWORK_NAME:
248 				log.error("TMACL-01170:The specified CIFS share '{}' is not known. " +
249 					"Please create the share on the CIFS server before submitting " +
250 					"files to the ACL.", baseURI);
251 				break;
252 			case NtStatus.NT_STATUS_ACCESS_DENIED:
253 				log.error("TMACL-01290:Access was denied to the specified CIFS share '{}'. " +
254 					"Please GRANT read & write access to the user '{}'.", baseURI, authentication);
255 				break;
256 			default:
257 				throw e;
258 		}
259 	}
260 
261 	private void logLoginException(SmbException e) {
262 		switch (e.getNtStatus()) {
263 			case NtStatus.NT_STATUS_LOGON_FAILURE:
264 				log.error("TMACL-01330:The logon failed with user '{}'." +
265 					"Please verify domain, username and password and retry.",
266 					authentication);
267 				break;
268 			case NtStatus.NT_STATUS_WRONG_PASSWORD:
269 				log.error("TMACL-01340:The specified password is wrong for user '{}'." +
270 					"Please use a correct password to access the CIFS share.",
271 					authentication);
272 				break;
273 			case NtStatus.NT_STATUS_PASSWORD_EXPIRED:
274 			case NtStatus.NT_STATUS_PASSWORD_MUST_CHANGE:
275 				log.error("TMACL-01350:The specified password is expired or must be changed for " +
276 					"user '{}'. Specify a new password to access the CIFS share.",
277 					authentication);
278 				break;
279 			case NtStatus.NT_STATUS_ACCOUNT_LOCKED_OUT:
280 			case NtStatus.NT_STATUS_ACCOUNT_DISABLED:
281 				log.error("TMACL-01300:The account of user '{}' was disabled or locked out." +
282 					"Please re-enable it or use another user to access the CIFS share.",
283 					authentication);
284 				break;
285 			case NtStatus.NT_STATUS_INVALID_LOGON_HOURS:
286 				log.error("TMACL-01310:The account of user '{}' is used within invalid logon hours." +
287 					"Please remove logon hour limits in order to access the CIFS share.",
288 					authentication);
289 				break;
290 			case NtStatus.NT_STATUS_INVALID_WORKSTATION:
291 				log.error("TMACL-01320:This machine was not granted access to the CIFS share." +
292 					"Please grant access to the CIFS share for this workstation " +
293 					"(e.g. using its FQDN).");
294 				break;
295 		}
296 	}
297 }