SMTP: correct unescaping of posted messages containing lines with single dot.
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/>.
18 package org.sonews.daemon;
20 import java.io.ByteArrayOutputStream;
21 import java.io.IOException;
22 import java.net.InetSocketAddress;
23 import java.net.SocketException;
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.Arrays;
31 import java.util.Timer;
32 import java.util.TimerTask;
33 import java.util.logging.Level;
34 import org.sonews.acl.User;
35 import org.sonews.daemon.command.Command;
36 import org.sonews.storage.Article;
37 import org.sonews.storage.Group;
38 import org.sonews.storage.StorageBackendException;
39 import org.sonews.util.Log;
40 import org.sonews.util.Stats;
41 import org.sonews.util.io.CRLFOutputStream;
42 import org.sonews.util.io.SMTPOutputStream;
45 * For every SocketChannel (so TCP/IP connection) there is an instance of
47 * @author Christian Lins
50 public final class NNTPConnection {
52 public static final String NEWLINE = "\r\n"; // RFC defines this as newline
53 public static final String MESSAGE_ID_PATTERN = "<[^>]+>";
54 private static final Timer cancelTimer = new Timer(true); // Thread-safe? True for run as daemon
55 /** SocketChannel is generally thread-safe */
56 private SocketChannel channel = null;
57 private Charset charset = Charset.forName("UTF-8");
58 private Command command = null;
59 private Article currentArticle = null;
60 private Group currentGroup = null;
61 private volatile long lastActivity = System.currentTimeMillis();
62 private ChannelLineBuffers lineBuffers = new ChannelLineBuffers();
63 private int readLock = 0;
64 private final Object readLockGate = new Object();
65 private SelectionKey writeSelKey = null;
68 public NNTPConnection(final SocketChannel channel)
70 if (channel == null) {
71 throw new IllegalArgumentException("channel is null");
74 this.channel = channel;
75 Stats.getInstance().clientConnect();
79 * Tries to get the read lock for this NNTPConnection. This method is Thread-
80 * safe and returns true of the read lock was successfully set. If the lock
81 * is still hold by another Thread the method returns false.
83 boolean tryReadLock() {
84 // As synchronizing simple types may cause deadlocks,
85 // we use a gate object.
86 synchronized (readLockGate) {
90 readLock = Thread.currentThread().hashCode();
97 * Releases the read lock in a Thread-safe way.
98 * @throws IllegalMonitorStateException if a Thread not holding the lock
99 * tries to release it.
101 void unlockReadLock() {
102 synchronized (readLockGate) {
103 if (readLock == Thread.currentThread().hashCode()) {
106 throw new IllegalMonitorStateException();
112 * @return Current input buffer of this NNTPConnection instance.
114 public ByteBuffer getInputBuffer() {
115 return this.lineBuffers.getInputBuffer();
119 * @return Output buffer of this NNTPConnection which has at least one byte
122 public ByteBuffer getOutputBuffer() {
123 return this.lineBuffers.getOutputBuffer();
127 * @return ChannelLineBuffers instance associated with this NNTPConnection.
129 public ChannelLineBuffers getBuffers() {
130 return this.lineBuffers;
134 * @return true if this connection comes from a local remote address.
136 public boolean isLocalConnection() {
137 return ((InetSocketAddress) this.channel.socket().getRemoteSocketAddress()).getHostName().equalsIgnoreCase("localhost");
140 void setWriteSelectionKey(SelectionKey selKey) {
141 this.writeSelKey = selKey;
144 public void shutdownInput() {
146 // Closes the input line of the channel's socket, so no new data
147 // will be received and a timeout can be triggered.
148 this.channel.socket().shutdownInput();
149 } catch (IOException ex) {
150 Log.get().log(Level.WARNING, "Exception in NNTPConnection.shutdownInput(): {0}", ex);
154 public void shutdownOutput() {
155 cancelTimer.schedule(new TimerTask() {
159 // Closes the output line of the channel's socket.
160 channel.socket().shutdownOutput();
162 } catch (SocketException ex) {
163 // Socket was already disconnected
164 Log.get().log(Level.INFO, "NNTPConnection.shutdownOutput(): {0}", ex);
165 } catch (Exception ex) {
166 Log.get().log(Level.WARNING, "NNTPConnection.shutdownOutput(): {0}", ex);
172 public SocketChannel getSocketChannel() {
176 public Article getCurrentArticle() {
177 return this.currentArticle;
180 public Charset getCurrentCharset() {
185 * @return The currently selected communication channel (not SocketChannel)
187 public Group getCurrentChannel() {
188 return this.currentGroup;
191 public void setCurrentArticle(final Article article) {
192 this.currentArticle = article;
195 public void setCurrentGroup(final Group group) {
196 this.currentGroup = group;
199 public long getLastActivity() {
200 return this.lastActivity;
204 * Due to the readLockGate there is no need to synchronize this method.
206 * @throws IllegalArgumentException if raw is null.
207 * @throws IllegalStateException if calling thread does not own the readLock.
209 void lineReceived(byte[] raw) {
211 throw new IllegalArgumentException("raw is null");
214 if (readLock == 0 || readLock != Thread.currentThread().hashCode()) {
215 throw new IllegalStateException("readLock not properly set");
218 this.lastActivity = System.currentTimeMillis();
220 String line = new String(raw, this.charset);
222 // There might be a trailing \r, but trim() is a bad idea
223 // as it removes also leading spaces from long header lines.
224 if (line.endsWith("\r")) {
225 line = line.substring(0, line.length() - 1);
226 raw = Arrays.copyOf(raw, raw.length - 1);
229 Log.get().log(Level.FINE, "<< {0}", line);
231 if (command == null) {
232 command = parseCommandLine(line);
233 assert command != null;
237 // The command object will process the line we just received
239 command.processLine(this, line, raw);
240 } catch (StorageBackendException ex) {
241 Log.get().info("Retry command processing after StorageBackendException");
243 // Try it a second time, so that the backend has time to recover
244 command.processLine(this, line, raw);
246 } catch (ClosedChannelException ex0) {
248 StringBuilder strBuf = new StringBuilder();
249 strBuf.append("Connection to ");
250 strBuf.append(channel.socket().getRemoteSocketAddress());
251 strBuf.append(" closed: ");
253 Log.get().info(strBuf.toString());
254 } catch (Exception ex0a) {
255 ex0a.printStackTrace();
257 } catch (Exception ex1) { // This will catch a second StorageBackendException
260 Log.get().log(Level.WARNING, ex1.getLocalizedMessage(), ex1);
261 println("403 Internal server error");
263 // Should we end the connection here?
264 // RFC says we MUST return 400 before closing the connection
267 } catch (Exception ex2) {
268 ex2.printStackTrace();
272 if (command == null || command.hasFinished()) {
274 charset = Charset.forName("UTF-8"); // Reset to default
279 * This method determines the fitting command processing class.
283 private Command parseCommandLine(String line) {
284 String cmdStr = line.split(" ")[0];
285 return CommandSelector.getInstance().get(cmdStr);
289 * Puts the given line into the output buffer, adds a newline character
290 * and returns. The method returns immediately and does not block until
291 * the line was sent. If line is longer than 510 octets it is split up in
292 * several lines. Each line is terminated by \r\n (NNTPConnection.NEWLINE).
295 public void println(final CharSequence line, final Charset charset)
297 writeToChannel(CharBuffer.wrap(line), charset, line);
298 writeToChannel(CharBuffer.wrap(NEWLINE), charset, null);
302 * Writes the given raw lines to the output buffers and finishes with
303 * a newline character (\r\n).
306 public void println(final byte[] rawLines)
308 this.lineBuffers.addOutputBuffer(ByteBuffer.wrap(rawLines));
309 writeToChannel(CharBuffer.wrap(NEWLINE), charset, null);
313 * Same as {@link #println(byte[]) } but escapes lines containing single dot,
314 * which has special meaning in protocol (end of message).
316 * This method is safe to be used for writing messages – if message contains
317 * a line with single dot, it will be doubled and thus not interpreted
318 * by NNTP client as end of message
321 * @throws IOException
323 public void printlnEscapeDots(final byte[] rawLines) throws IOException {
324 // TODO: optimalizace
326 ByteArrayOutputStream baos = new ByteArrayOutputStream(rawLines.length + 10);
327 CRLFOutputStream crlfStream = new CRLFOutputStream(baos);
328 SMTPOutputStream smtpStream = new SMTPOutputStream(crlfStream);
329 smtpStream.write(rawLines);
331 println(baos.toByteArray());
337 * Encodes the given CharBuffer using the given Charset to a bunch of
338 * ByteBuffers (each 512 bytes large) and enqueues them for writing at the
339 * connected SocketChannel.
340 * @throws java.io.IOException
342 private void writeToChannel(CharBuffer characters, final Charset charset,
343 CharSequence debugLine)
345 if (!charset.canEncode()) {
346 Log.get().log(Level.SEVERE, "FATAL: Charset {0} cannot encode!", charset);
350 // Write characters to output buffers
351 LineEncoder lenc = new LineEncoder(characters, charset);
352 lenc.encode(lineBuffers);
354 enableWriteEvents(debugLine);
357 private void enableWriteEvents(CharSequence debugLine) {
358 // Enable OP_WRITE events so that the buffers are processed
360 this.writeSelKey.interestOps(SelectionKey.OP_WRITE);
361 ChannelWriter.getInstance().getSelector().wakeup();
362 } catch (Exception ex) // CancelledKeyException and ChannelCloseException
364 Log.get().log(Level.WARNING, "NNTPConnection.writeToChannel(): {0}", ex);
368 // Update last activity timestamp
369 this.lastActivity = System.currentTimeMillis();
370 if (debugLine != null) {
371 Log.get().log(Level.FINE, ">> {0}", debugLine);
375 public void println(final CharSequence line)
377 println(line, charset);
380 public void print(final String line)
382 writeToChannel(CharBuffer.wrap(line), charset, line);
385 public void setCurrentCharset(final Charset charset) {
386 this.charset = charset;
389 void setLastActivity(long timestamp) {
390 this.lastActivity = timestamp;
394 * @return Currently logged user (but you should check {@link User#isAuthenticated()}, if user is athenticated, or we just trust him)
396 public User getUser() {
401 * This method is to be called from AUTHINFO USER Command implementation.
402 * @param username username from AUTHINFO USER username.
404 public void setUser(User user) {