/*
 * Licensed to The OpenNMS Group, Inc (TOG) under one or more
 * contributor license agreements.  See the LICENSE.md file
 * distributed with this work for additional information
 * regarding copyright ownership.
 *
 * TOG licenses this file to You under the GNU Affero General
 * Public License Version 3 (the "License") or (at your option)
 * any later version.  You may not use this file except in
 * compliance with the License.  You may obtain a copy of the
 * License at:
 *
 *      https://www.gnu.org/licenses/agpl-3.0.txt
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 * either express or implied.  See the License for the specific
 * language governing permissions and limitations under the
 * License.
 */
package org.opennms.netmgt.poller.monitors;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.InterruptedIOException;
import java.net.ConnectException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.NoRouteToHostException;
import java.net.Socket;
import java.util.Map;

import org.opennms.core.utils.InetAddressUtils;
import org.opennms.core.utils.ParameterMap;
import org.opennms.core.utils.TimeoutTracker;
import org.opennms.netmgt.poller.MonitoredService;
import org.opennms.netmgt.poller.PollStatus;
import org.opennms.netmgt.poller.monitors.support.FtpResponse;
import org.opennms.netmgt.poller.monitors.support.ParameterSubstitutingMonitor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * This class is designed to be used by the service poller framework to test the
 * availability of the FTP service on remote interfaces. The class implements
 * the ServiceMonitor interface that allows it to be used along with other
 * plug-ins by the service poller framework.
 *
 * @author <A HREF="mailto:tarus@opennms.org">Tarus Balog </A>
 * @author <A HREF="http://www.opennms.org/">OpenNMS </A>
 */
public class FtpMonitor extends ParameterSubstitutingMonitor {
    
    
    public static final Logger LOG = LoggerFactory.getLogger(FtpMonitor.class);

    /**
     * Default FTP port.
     */
    private static final int DEFAULT_PORT = 21;

    /**
     * Default retries.
     */
    private static final int DEFAULT_RETRY = 0;

    /**
     * Default timeout. Specifies how long (in milliseconds) to block waiting
     * for data from the monitored interface.
     */
    private static final int DEFAULT_TIMEOUT = 3000; // 3 second timeout on
                                                        // read()

    /**
     * {@inheritDoc}
     *
     * Poll the specified address for FTP service availability.
     *
     * During the poll an attempt is made to connect on the specified port (by
     * default TCP port 21). If the connection request is successful, the banner
     * line generated by the interface is parsed and if the extracted return
     * code indicates that we are talking to an FTP server we continue. Next, an
     * FTP 'QUIT' command is sent. Provided that the interface's response is
     * valid we set the service status to SERVICE_AVAILABLE and return.
     */
    @Override
    public PollStatus poll(MonitoredService svc, Map<String, Object> parameters) {
        // Get the parameters
        TimeoutTracker tracker = new TimeoutTracker(parameters, DEFAULT_RETRY, DEFAULT_TIMEOUT);
        int port = ParameterMap.getKeyedInteger(parameters, "port", DEFAULT_PORT);
        String userid = resolveKeyedString(parameters, "userid", null);
        String password = resolveKeyedString(parameters, "password", null);

        // Extract the address
        InetAddress ipAddr = svc.getAddress();

        PollStatus serviceStatus = PollStatus.unavailable();
        for (tracker.reset(); tracker.shouldRetry() && !serviceStatus.isAvailable(); tracker.nextAttempt()) {

            LOG.debug("FtpMonitor.poll: Polling interface: {} {}", InetAddressUtils.str(ipAddr), tracker);

            Socket socket = null;
            try {
                // create a connected socket
                tracker.startAttempt();

                socket = new Socket();
                socket.connect(new InetSocketAddress(ipAddr, port), tracker.getConnectionTimeout());
                socket.setSoTimeout(tracker.getSoTimeout());
                LOG.debug("FtpMonitor: connected to host: {} on port: {}", ipAddr, port);

                // We're connected, so upgrade status to unresponsive
                serviceStatus = PollStatus.unresponsive();

                BufferedReader lineRdr = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                
                FtpResponse bannerResponse = FtpResponse.readResponse(lineRdr);

                if (bannerResponse.isSuccess()) {
                    // Attempt to login if userid and password available
                    boolean loggedInSuccessfully = false;
                    LOG.debug("FtpMonitor: Banner response successful.");
                    if (userid == null || userid.length() == 0 || password == null || password.length() == 0) {
                        loggedInSuccessfully = true;
                    } else {
                        FtpResponse.sendCommand(socket, "USER " + userid);

                        FtpResponse userResponse = FtpResponse.readResponse(lineRdr);

                        if (userResponse.isSuccess() || userResponse.isIntermediate()) {
                            LOG.debug("FtpMonitor: User response successful.");
                            FtpResponse.sendCommand(socket, "PASS " + password);
                            
                            FtpResponse passResponse = FtpResponse.readResponse(lineRdr);
                            if (passResponse.isSuccess()) {
                                LOG.debug("FtpMonitor.poll: Login successful, parsed return code: {}", passResponse.getCode());
                                loggedInSuccessfully = true;
                            } else {
                                LOG.debug("FtpMonitor.poll: Login failed, parsed return code: {}, full response: {}", passResponse.getCode(), passResponse);
                                loggedInSuccessfully = false;
                            }
                        }
                    }

                    // Store the response time before we try to quit
                    double responseTime = tracker.elapsedTimeInMillis();

                    if (loggedInSuccessfully) {
                        FtpResponse.sendCommand(socket, "QUIT");

                        FtpResponse quitResponse = FtpResponse.readResponse(lineRdr);

                        /*
                         * Special Cases for success:
                         * 
                         * Also want to accept the following
                         * ERROR message generated by some FTP servers
                         * following a QUIT command without a previous
                         * successful login:
                         *
                         * "530 QUIT : User not logged in. Please login with
                         * USER and PASS first."
                         * 
                         * Also want to accept the following ERROR
                         * message generated by some FTP servers following a
                         * QUIT command without a previously successful login:
                         *
                         * "425 Session is disconnected."
                         */
                        if (quitResponse.isSuccess()
                                || (quitResponse.getCode() == 530 ) || (quitResponse.getCode() == 425 )) {
                            serviceStatus = PollStatus.available(responseTime);
                        }
                    }
                }

                /*
                 * If we get this far and the status has not been set
                 * to available, then something didn't verify during
                 * the banner checking or login/QUIT command process.
                 */
                if (!serviceStatus.isAvailable()) {
                    serviceStatus = PollStatus.unavailable();
                }
            } catch (NumberFormatException e) {
            	String reason = "NumberFormatException while polling address: " + ipAddr;
                LOG.debug(reason, e);
                serviceStatus = PollStatus.unavailable(reason);
            } catch (NoRouteToHostException e) {
            	String reason = "No route to host exception for address: " + ipAddr;
                LOG.debug(reason, e);
                serviceStatus = PollStatus.unavailable(reason);
            } catch (InterruptedIOException e) {
            	String reason = "did not connect to host with " + tracker;
                LOG.debug(reason);
                serviceStatus = PollStatus.unavailable(reason);
            } catch (ConnectException e) {
            	String reason = "Connection exception for address: " + ipAddr;
                LOG.debug(reason, e);
                serviceStatus = PollStatus.unavailable(reason);
            } catch (IOException e) {
            	String reason = "IOException while polling address: " + ipAddr;
                LOG.debug(reason, e);
                serviceStatus = PollStatus.unavailable(reason);
            } finally {
                try {
                    // Close the socket
                    if (socket != null) {
                        socket.close();
                    }
                } catch (IOException e) {
                    LOG.debug("FtpMonitor.poll: Error closing socket", e);
                }
            }
        }

        return serviceStatus;
    }
}
