# HG changeset patch
# User František Kučera <franta-hg@frantovo.cz>
# Date 1319053251 -7200
# Node ID d54786065fa34753f3d919935b95e30a8855753e
# Parent  08c9fb6fb01795dd6d5d404e1b84a20f709a6562
Drupal: ověřování uživatelů.

diff -r 08c9fb6fb017 -r d54786065fa3 helpers/commands.list
--- a/helpers/commands.list	Wed Oct 19 17:23:53 2011 +0200
+++ b/helpers/commands.list	Wed Oct 19 21:40:51 2011 +0200
@@ -12,4 +12,5 @@
 org.sonews.daemon.command.QuitCommand
 org.sonews.daemon.command.StatCommand
 org.sonews.daemon.command.XDaemonCommand
-org.sonews.daemon.command.XPatCommand
\ No newline at end of file
+org.sonews.daemon.command.XPatCommand
+org.sonews.acl.DrupalAuthInfoCommand
\ No newline at end of file
diff -r 08c9fb6fb017 -r d54786065fa3 src/org/sonews/acl/DrupalAuthInfoCommand.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/sonews/acl/DrupalAuthInfoCommand.java	Wed Oct 19 21:40:51 2011 +0200
@@ -0,0 +1,107 @@
+/*
+ *   SONEWS News Server
+ *   see AUTHORS for the list of contributors
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation, either version 3 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package org.sonews.acl;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import org.sonews.daemon.NNTPConnection;
+import org.sonews.daemon.command.Command;
+import org.sonews.storage.StorageBackendException;
+import org.sonews.storage.StorageManager;
+import org.sonews.storage.StorageProvider;
+import org.sonews.storage.impl.DrupalDatabaseProvider;
+
+/**
+ *
+ * @author František Kučera (frantovo.cz)
+ */
+public class DrupalAuthInfoCommand implements Command {
+
+	private static final Logger log = Logger.getLogger(DrupalAuthInfoCommand.class.getName());
+	private static String[] SUPPORTED_COMMANDS = {"AUTHINFO"};
+
+	@Override
+	public boolean hasFinished() {
+		return true;
+	}
+
+	@Override
+	public String impliedCapability() {
+		return "AUTHINFO";
+	}
+
+	@Override
+	public boolean isStateful() {
+		return false;
+	}
+
+	@Override
+	public String[] getSupportedCommandStrings() {
+		return SUPPORTED_COMMANDS;
+	}
+
+	@Override
+	public void processLine(NNTPConnection conn, String line, byte[] rawLine) throws IOException, StorageBackendException {
+		Pattern commandPattern = Pattern.compile("AUTHINFO (USER|PASS) (.*)", Pattern.CASE_INSENSITIVE);
+		Matcher commandMatcher = commandPattern.matcher(line);
+
+		if (commandMatcher.matches()) {
+
+			if (conn.isUserAuthenticated()) {
+				conn.println("502 Command unavailable (you are already authenticated)");
+			} else if ("USER".equalsIgnoreCase(commandMatcher.group(1))) {
+				conn.setUsername(commandMatcher.group(2));
+				conn.println("381 Password required");
+				log.log(Level.FINE, "User ''{0}'' greets us. We are waiting for his password.", conn.getUsername());
+			} else if ("PASS".equalsIgnoreCase(commandMatcher.group(1))) {
+				if (conn.getUsername() == null) {
+					conn.println("482 Authentication commands issued out of sequence");
+				} else {
+
+					char[] password = commandMatcher.group(2).toCharArray();
+					boolean goodPassword = StorageManager.current().authenticateUser(conn.getUsername(), password);
+					Arrays.fill(password, '*');
+					commandMatcher = null;
+
+					if (goodPassword) {
+						conn.println("281 Authentication accepted");
+						conn.setUserAuthenticated(true);
+						log.log(Level.INFO, "User ''{0}'' has been succesfully authenticated.", conn.getUsername());
+					} else {
+						log.log(Level.INFO, "User ''{0}'' has provided wrong password.", conn.getUsername());
+						conn.setUsername(null);
+						conn.setUserAuthenticated(false);
+						conn.println("481 Authentication failed: wrong password");
+					}
+
+				}
+			} else {
+				// impossible, see commandPattern
+				conn.println("500 Unknown command");
+			}
+
+
+		} else {
+			conn.println("500 Unknown command, expecting AUTHINFO USER username or AUTHINFO PASS password ");
+		}
+	}
+}
diff -r 08c9fb6fb017 -r d54786065fa3 src/org/sonews/daemon/NNTPConnection.java
--- a/src/org/sonews/daemon/NNTPConnection.java	Wed Oct 19 17:23:53 2011 +0200
+++ b/src/org/sonews/daemon/NNTPConnection.java	Wed Oct 19 21:40:51 2011 +0200
@@ -59,6 +59,9 @@
 	private int readLock = 0;
 	private final Object readLockGate = new Object();
 	private SelectionKey writeSelKey = null;
+	
+	private String username;
+	private boolean userAuthenticated = false;
 
 	public NNTPConnection(final SocketChannel channel)
 			throws IOException {
@@ -360,4 +363,36 @@
 	void setLastActivity(long timestamp) {
 		this.lastActivity = timestamp;
 	}
+
+	/**
+	 * @return Current username. 
+	 * But user may not have been authenticated yet.
+	 * You must check {@link #isUserAuthenticated()}
+	 */
+	public String getUsername() {
+		return username;
+	}
+
+	/**
+	 * This method is to be called from AUTHINFO USER Command implementation.
+	 * @param username username from AUTHINFO USER username.
+	 */
+	public void setUsername(String username) {
+		this.username = username;
+	}
+
+	/**
+	 * @return true if current user (see {@link #getUsername()}) has been succesfully authenticated.
+	 */
+	public boolean isUserAuthenticated() {
+		return userAuthenticated;
+	}
+
+	/**
+	 * This method is to be called from AUTHINFO PASS Command implementation.
+	 * @param userAuthenticated true if user has provided right password in AUTHINFO PASS password.
+	 */
+	public void setUserAuthenticated(boolean userAuthenticated) {
+		this.userAuthenticated = userAuthenticated;
+	}
 }
diff -r 08c9fb6fb017 -r d54786065fa3 src/org/sonews/daemon/command/PostCommand.java
--- a/src/org/sonews/daemon/command/PostCommand.java	Wed Oct 19 17:23:53 2011 +0200
+++ b/src/org/sonews/daemon/command/PostCommand.java	Wed Oct 19 21:40:51 2011 +0200
@@ -210,6 +210,10 @@
 
 	private void postArticle(NNTPConnection conn, Article article)
 			throws IOException {
+		if (conn.isUserAuthenticated()) {
+			article.setAuthenticatedUser(conn.getUsername());
+		}
+		
 		if (article.getHeader(Headers.CONTROL)[0].length() > 0) {
 			controlMessage(conn, article);
 		} else if (article.getHeader(Headers.SUPERSEDES)[0].length() > 0) {
@@ -218,7 +222,7 @@
 			// Circle check; note that Path can already contain the hostname here
 			String host = Config.inst().get(Config.HOSTNAME, "localhost");
 			if (article.getHeader(Headers.PATH)[0].indexOf(host + "!", 1) > 0) {
-				Log.get().info(article.getMessageID() + " skipped for host " + host);
+				Log.get().log(Level.INFO, "{0} skipped for host {1}", new Object[]{article.getMessageID(), host});
 				conn.println("441 I know this article already");
 				return;
 			}
diff -r 08c9fb6fb017 -r d54786065fa3 src/org/sonews/storage/Article.java
--- a/src/org/sonews/storage/Article.java	Wed Oct 19 17:23:53 2011 +0200
+++ b/src/org/sonews/storage/Article.java	Wed Oct 19 21:40:51 2011 +0200
@@ -41,6 +41,8 @@
  * @since n3tpd/0.1
  */
 public class Article extends ArticleHead {
+	
+	private String authenticatedUser;
 
 	/**
 	 * Loads the Article identified by the given ID from the JDBCDatabase.
@@ -220,4 +222,19 @@
 	public String toString() {
 		return getMessageID();
 	}
+
+	/**
+	 * @return username of currently logged user – or null, if user is not authenticated.
+	 */
+	public String getAuthenticatedUser() {
+		return authenticatedUser;
+	}
+	
+	/**
+	 * This method is to be called from POST Command implementation.
+	 * @param authenticatedUser current username – or null, if user is not authenticated.
+	 */
+	public void setAuthenticatedUser(String authenticatedUser) {
+		this.authenticatedUser = authenticatedUser;
+	}
 }
diff -r 08c9fb6fb017 -r d54786065fa3 src/org/sonews/storage/Storage.java
--- a/src/org/sonews/storage/Storage.java	Wed Oct 19 17:23:53 2011 +0200
+++ b/src/org/sonews/storage/Storage.java	Wed Oct 19 21:40:51 2011 +0200
@@ -144,4 +144,7 @@
 
 	boolean update(Group group)
 			throws StorageBackendException;
+
+	public boolean authenticateUser(String username, char[] password)
+			throws StorageBackendException;
 }
diff -r 08c9fb6fb017 -r d54786065fa3 src/org/sonews/storage/impl/DrupalDatabase.java
--- a/src/org/sonews/storage/impl/DrupalDatabase.java	Wed Oct 19 17:23:53 2011 +0200
+++ b/src/org/sonews/storage/impl/DrupalDatabase.java	Wed Oct 19 21:40:51 2011 +0200
@@ -28,6 +28,7 @@
 import java.util.logging.Level;
 import java.util.logging.Logger;
 import org.sonews.config.Config;
+import org.sonews.daemon.Connections;
 import org.sonews.feed.Subscription;
 import org.sonews.storage.Article;
 import org.sonews.storage.ArticleHead;
@@ -387,9 +388,41 @@
 		return Collections.emptyList();
 	}
 
+	/**
+	 * Checks username and password.
+	 * @param username
+	 * @param password
+	 * @return true if credentials are valid | false otherwise
+	 * @throws StorageBackendException it there is any error during authentication process 
+	 * (but should not be thrown if only bad thing is wrong username or password)
+	 */
+	@Override
+	public boolean authenticateUser(String username, char[] password) throws StorageBackendException {
+		PreparedStatement ps = null;
+		ResultSet rs = null;
+		try {
+			ps = conn.prepareStatement("SELECT nntp_login(?, ?)");
+			ps.setString(1, username);
+			ps.setString(2, String.copyValueOf(password));
+			rs = ps.executeQuery();
+			rs.next();
+			return rs.getInt(1) == 1;
+		} catch (Exception e) {
+			throw new StorageBackendException(e);
+		} finally {
+			close(null, ps, rs);
+		}
+	}
+
 	@Override
 	public void addArticle(Article art) throws StorageBackendException {
-		log.log(Level.SEVERE, "TODO: addArticle {0}", new Object[]{art});
+		if (art.getAuthenticatedUser() == null) {
+			log.log(Level.SEVERE, "User was not authenticated, so his article was rejected.");
+			throw new StorageBackendException("User must be authenticated to post articles");
+		} else {
+
+			log.log(Level.INFO, "User ''{0}'' has posted an article", art.getAuthenticatedUser());
+		}
 	}
 
 	@Override
diff -r 08c9fb6fb017 -r d54786065fa3 src/org/sonews/storage/impl/JDBCDatabase.java
--- a/src/org/sonews/storage/impl/JDBCDatabase.java	Wed Oct 19 17:23:53 2011 +0200
+++ b/src/org/sonews/storage/impl/JDBCDatabase.java	Wed Oct 19 21:40:51 2011 +0200
@@ -1468,4 +1468,10 @@
 			return update(group);
 		}
 	}
+
+	@Override
+	public boolean authenticateUser(String username, char[] password) 
+			throws StorageBackendException {
+		throw new StorageBackendException("Not supported yet.");
+	}
 }