3 * see AUTHORS for the list of contributors
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
19 package org.sonews.daemon;
21 import org.sonews.util.Log;
22 import java.io.IOException;
23 import java.net.InetSocketAddress;
24 import java.nio.ByteBuffer;
25 import java.nio.CharBuffer;
26 import java.nio.channels.ClosedChannelException;
27 import java.nio.channels.SelectionKey;
28 import java.nio.channels.SocketChannel;
29 import java.nio.charset.Charset;
30 import java.util.Timer;
31 import java.util.TimerTask;
32 import org.sonews.daemon.command.ArticleCommand;
33 import org.sonews.daemon.command.CapabilitiesCommand;
34 import org.sonews.daemon.command.AbstractCommand;
35 import org.sonews.daemon.command.GroupCommand;
36 import org.sonews.daemon.command.HelpCommand;
37 import org.sonews.daemon.command.ListCommand;
38 import org.sonews.daemon.command.ListGroupCommand;
39 import org.sonews.daemon.command.ModeReaderCommand;
40 import org.sonews.daemon.command.NewGroupsCommand;
41 import org.sonews.daemon.command.NextPrevCommand;
42 import org.sonews.daemon.command.OverCommand;
43 import org.sonews.daemon.command.PostCommand;
44 import org.sonews.daemon.command.QuitCommand;
45 import org.sonews.daemon.command.StatCommand;
46 import org.sonews.daemon.command.UnsupportedCommand;
47 import org.sonews.daemon.command.XDaemonCommand;
48 import org.sonews.daemon.command.XPatCommand;
49 import org.sonews.daemon.storage.Article;
50 import org.sonews.daemon.storage.Group;
51 import org.sonews.util.Stats;
54 * For every SocketChannel (so TCP/IP connection) there is an instance of
56 * @author Christian Lins
59 public final class NNTPConnection
62 public static final String NEWLINE = "\r\n"; // RFC defines this as newline
63 public static final String MESSAGE_ID_PATTERN = "<[^>]+>";
65 private static final Timer cancelTimer = new Timer(true); // Thread-safe? True for run as daemon
67 /** SocketChannel is generally thread-safe */
68 private SocketChannel channel = null;
69 private Charset charset = Charset.forName("UTF-8");
70 private AbstractCommand command = null;
71 private Article currentArticle = null;
72 private Group currentGroup = null;
73 private volatile long lastActivity = System.currentTimeMillis();
74 private ChannelLineBuffers lineBuffers = new ChannelLineBuffers();
75 private int readLock = 0;
76 private final Object readLockGate = new Object();
77 private SelectionKey writeSelKey = null;
79 public NNTPConnection(final SocketChannel channel)
84 throw new IllegalArgumentException("channel is null");
87 this.channel = channel;
88 Stats.getInstance().clientConnect();
92 * Tries to get the read lock for this NNTPConnection. This method is Thread-
93 * safe and returns true of the read lock was successfully set. If the lock
94 * is still hold by another Thread the method returns false.
98 // As synchronizing simple types may cause deadlocks,
99 // we use a gate object.
100 synchronized(readLockGate)
108 readLock = Thread.currentThread().hashCode();
115 * Releases the read lock in a Thread-safe way.
116 * @throws IllegalMonitorStateException if a Thread not holding the lock
117 * tries to release it.
119 void unlockReadLock()
121 synchronized(readLockGate)
123 if(readLock == Thread.currentThread().hashCode())
129 throw new IllegalMonitorStateException();
135 * @return Current input buffer of this NNTPConnection instance.
137 public ByteBuffer getInputBuffer()
139 return this.lineBuffers.getInputBuffer();
143 * @return Output buffer of this NNTPConnection which has at least one byte
146 public ByteBuffer getOutputBuffer()
148 return this.lineBuffers.getOutputBuffer();
152 * @return ChannelLineBuffers instance associated with this NNTPConnection.
154 public ChannelLineBuffers getBuffers()
156 return this.lineBuffers;
160 * @return true if this connection comes from a local remote address.
162 public boolean isLocalConnection()
164 return ((InetSocketAddress)this.channel.socket().getRemoteSocketAddress())
165 .getHostName().equalsIgnoreCase("localhost");
168 void setWriteSelectionKey(SelectionKey selKey)
170 this.writeSelKey = selKey;
173 public void shutdownInput()
177 // Closes the input line of the channel's socket, so no new data
178 // will be received and a timeout can be triggered.
179 this.channel.socket().shutdownInput();
181 catch(IOException ex)
183 Log.msg("Exception in NNTPConnection.shutdownInput(): " + ex, false);
186 ex.printStackTrace();
191 public void shutdownOutput()
193 cancelTimer.schedule(new TimerTask()
200 // Closes the output line of the channel's socket.
201 channel.socket().shutdownOutput();
206 Log.msg("NNTPConnection.shutdownOutput(): " + ex, false);
209 ex.printStackTrace();
216 public SocketChannel getChannel()
221 public Article getCurrentArticle()
223 return this.currentArticle;
226 public Charset getCurrentCharset()
231 public Group getCurrentGroup()
233 return this.currentGroup;
236 public void setCurrentArticle(final Article article)
238 this.currentArticle = article;
241 public void setCurrentGroup(final Group group)
243 this.currentGroup = group;
246 public long getLastActivity()
248 return this.lastActivity;
252 * Due to the readLockGate there is no need to synchronize this method.
254 * @throws IllegalArgumentException if raw is null.
255 * @throws IllegalStateException if calling thread does not own the readLock.
257 void lineReceived(byte[] raw)
261 throw new IllegalArgumentException("raw is null");
264 if(readLock == 0 || readLock != Thread.currentThread().hashCode())
266 throw new IllegalStateException("readLock not properly set");
269 this.lastActivity = System.currentTimeMillis();
271 String line = new String(raw, this.charset);
273 // There might be a trailing \r, but trim() is a bad idea
274 // as it removes also leading spaces from long header lines.
275 if(line.endsWith("\r"))
277 line = line.substring(0, line.length() - 1);
280 Log.msg("<< " + line, true);
284 command = parseCommandLine(line);
285 assert command != null;
290 // The command object will process the line we just received
291 command.processLine(line);
293 catch(ClosedChannelException ex0)
297 Log.msg("Connection to " + channel.socket().getRemoteSocketAddress()
298 + " closed: " + ex0, true);
300 catch(Exception ex0a)
302 ex0a.printStackTrace();
310 ex1.printStackTrace();
311 println("500 Internal server error");
315 ex2.printStackTrace();
319 if(command == null || command.hasFinished())
322 charset = Charset.forName("UTF-8"); // Reset to default
327 * This method performes several if/elseif constructs to determine the
328 * fitting command object.
329 * TODO: This string comparisons are probably slow!
333 private AbstractCommand parseCommandLine(String line)
335 AbstractCommand cmd = new UnsupportedCommand(this);
336 String cmdStr = line.split(" ")[0];
338 if(cmdStr.equalsIgnoreCase("ARTICLE") ||
339 cmdStr.equalsIgnoreCase("BODY"))
341 cmd = new ArticleCommand(this);
343 else if(cmdStr.equalsIgnoreCase("CAPABILITIES"))
345 cmd = new CapabilitiesCommand(this);
347 else if(cmdStr.equalsIgnoreCase("GROUP"))
349 cmd = new GroupCommand(this);
351 else if(cmdStr.equalsIgnoreCase("HEAD"))
353 cmd = new ArticleCommand(this);
355 else if(cmdStr.equalsIgnoreCase("HELP"))
357 cmd = new HelpCommand(this);
359 else if(cmdStr.equalsIgnoreCase("LIST"))
361 cmd = new ListCommand(this);
363 else if(cmdStr.equalsIgnoreCase("LISTGROUP"))
365 cmd = new ListGroupCommand(this);
367 else if(cmdStr.equalsIgnoreCase("MODE"))
369 cmd = new ModeReaderCommand(this);
371 else if(cmdStr.equalsIgnoreCase("NEWGROUPS"))
373 cmd = new NewGroupsCommand(this);
375 else if(cmdStr.equalsIgnoreCase("NEXT") ||
376 cmdStr.equalsIgnoreCase("PREV"))
378 cmd = new NextPrevCommand(this);
380 else if(cmdStr.equalsIgnoreCase("OVER") ||
381 cmdStr.equalsIgnoreCase("XOVER")) // for compatibility with older RFCs
383 cmd = new OverCommand(this);
385 else if(cmdStr.equalsIgnoreCase("POST"))
387 cmd = new PostCommand(this);
389 else if(cmdStr.equalsIgnoreCase("QUIT"))
391 cmd = new QuitCommand(this);
393 else if(cmdStr.equalsIgnoreCase("STAT"))
395 cmd = new StatCommand(this);
397 else if(cmdStr.equalsIgnoreCase("XDAEMON"))
399 cmd = new XDaemonCommand(this);
401 else if(cmdStr.equalsIgnoreCase("XPAT"))
403 cmd = new XPatCommand(this);
410 * Puts the given line into the output buffer, adds a newline character
411 * and returns. The method returns immediately and does not block until
412 * the line was sent. If line is longer than 510 octets it is split up in
413 * several lines. Each line is terminated by \r\n (NNTPConnection.NEWLINE).
416 public void println(final CharSequence line, final Charset charset)
419 writeToChannel(CharBuffer.wrap(line), charset, line);
420 writeToChannel(CharBuffer.wrap(NEWLINE), charset, null);
424 * Encodes the given CharBuffer using the given Charset to a bunch of
425 * ByteBuffers (each 512 bytes large) and enqueues them for writing at the
426 * connected SocketChannel.
427 * @throws java.io.IOException
429 private void writeToChannel(CharBuffer characters, final Charset charset,
430 CharSequence debugLine)
433 if(!charset.canEncode())
435 Log.msg("FATAL: Charset " + charset + " cannot encode!", false);
439 // Write characters to output buffers
440 LineEncoder lenc = new LineEncoder(characters, charset);
441 lenc.encode(lineBuffers);
443 // Enable OP_WRITE events so that the buffers are processed
446 this.writeSelKey.interestOps(SelectionKey.OP_WRITE);
447 ChannelWriter.getInstance().getSelector().wakeup();
449 catch (Exception ex) // CancelledKeyException and ChannelCloseException
451 Log.msg("NNTPConnection.writeToChannel(): " + ex, false);
455 // Update last activity timestamp
456 this.lastActivity = System.currentTimeMillis();
457 if(debugLine != null)
459 Log.msg(">> " + debugLine, true);
463 public void println(final CharSequence line)
466 println(line, charset);
469 public void print(final String line)
472 writeToChannel(CharBuffer.wrap(line), charset, line);
475 public void setCurrentCharset(final Charset charset)
477 this.charset = charset;