1 /**
2 	(module summary)
3 
4 	Copyright: © 2012-2016 RejectedSoftware e.K.
5 	License: Subject to the terms of the General Public License version 3, as written in the included LICENSE.txt file.
6 	Authors: Sönke Ludwig
7 */
8 module vibenews.nntp.server;
9 
10 import vibenews.nntp.common;
11 import vibenews.nntp.status;
12 
13 import vibe.core.log;
14 import vibe.core.net;
15 import vibe.stream.counting;
16 import vibe.stream.operations;
17 import vibe.stream.tls;
18 
19 import std.algorithm;
20 import std.conv;
21 import std.exception;
22 import std..string;
23 
24 
25 void listenNNTP(NNTPServerSettings settings, void delegate(NNTPServerRequest, NNTPServerResponse) command_handler)
26 {
27 	void handleNNTPConnection(TCPConnection conn)
28 	@trusted {
29 		StreamProxy stream = conn;
30 
31 		bool tls_active = false;
32 
33 		assert(!settings.requireSSL, "requreSsl option is not yet supported.");
34 
35 		void acceptSsl()
36 		{
37 			TLSContext ctx;
38 			if (settings.sslContext) ctx = settings.sslContext;
39 			else {
40 				ctx = createTLSContext(TLSContextKind.server);
41 				ctx.useCertificateChainFile(settings._sslCertFile);
42 				ctx.usePrivateKeyFile(settings._sslKeyFile);
43 			}
44 			logTrace("accepting SSL");
45 			stream = createTLSStream(stream, ctx, TLSStreamState.accepting);
46 			logTrace("accepted SSL");
47 			tls_active = true;
48 		}
49 
50 		if (settings.sslContext || settings._enableSsl) acceptSsl();
51 
52 		stream.write("200 Welcome on VibeNews!\r\n");
53 		logDebug("welcomed");
54 
55 		while(!stream.empty){
56 			OutputStreamProxy os;
57 			os = stream;
58 			auto res = new NNTPServerResponse(os);
59 			logTrace("waiting for request");
60 			auto ln = cast(string)stream.readLine();
61 			logDebug("REQUEST: %s", !ln.startsWith("AUTHINFO") ? ln : "AUTHINFO (...)");
62 			auto params = ln.spaceSplit();
63 			if( params.length < 1 ){
64 				res.status = NNTPStatus.badCommand;
65 				res.statusText = "Expected command";
66 				res.writeVoidBody();
67 				res.finalize();
68 				continue;
69 			}
70 			auto cmd = params[0].toLower();
71 			params = params[1 .. $];
72 
73 			if( cmd == "quit" ){
74 				res.status = NNTPStatus.closingConnection;
75 				res.statusText = "Bye bye!";
76 				res.writeVoidBody();
77 				res.finalize();
78 				stream.finalize();
79 				conn.close();
80 				return;
81 			}
82 
83 			if( cmd == "starttls" ){
84 				if (tls_active) {
85 					res.status = NNTPStatus.commandUnavailable;
86 					res.statusText = "TLS already active.";
87 					res.writeVoidBody();
88 					res.finalize();
89 					continue;
90 				}
91 
92 				if (!settings.sslContext && !settings._enableSsl) {
93 					res.status = NNTPStatus.tlsFailed;
94 					res.statusText = "TLS is not configured for this server.";
95 					res.writeVoidBody();
96 					res.finalize();
97 					continue;
98 				}
99 
100 				res.status = NNTPStatus.continueWithTLS;
101 				res.statusText = "Continue with TLS negotiation";
102 				res.writeVoidBody();
103 				res.finalize();
104 
105 				acceptSsl();
106 			}
107 
108 			InputStreamProxy is_;
109 			is_ = stream;
110 			auto req = new NNTPServerRequest(is_);
111 			req.command = cmd;
112 			req.parameters = params;
113 			req.peerAddress = conn.peerAddress;
114 			try {
115 				command_handler(req, res);
116 			} catch( NNTPStatusException e ){
117 				res.status = e.status;
118 				res.statusText = e.statusText;
119 				res.writeVoidBody();
120 			} catch( Exception e ){
121 				logWarn("NNTP request exception: %s", e.toString());
122 				if( !res.m_headerWritten ){
123 					res.status = NNTPStatus.internalError;
124 					res.statusText = "Internal error: " ~ e.msg;
125 					res.writeVoidBody();
126 				}
127 			}
128 			res.finalize();
129 		}
130 		logDebug("disconnected");
131 	}
132 
133 	void handleNNTPConnectionNothrow(TCPConnection conn)
134 	@safe nothrow {
135 		try handleNNTPConnection(conn);
136 		catch (Exception e) {
137 			logError("Failed to handle NTTP connection: %s", e.msg);
138 		}
139 	}
140 
141 
142 	foreach( addr; settings.bindAddresses ){
143 		try {
144 			listenTCP(settings.port, &handleNNTPConnectionNothrow, addr);
145 			logInfo("Listening for NNTP%s requests on %s:%s", settings.sslContext || settings._enableSsl ? "S" : "", addr, settings.port);
146 		} catch( Exception e ) logWarn("Failed to listen on %s:%s", addr, settings.port);
147 	}
148 }
149 
150 
151 class NNTPServerSettings {
152 	ushort port = 119; // SSL port is 563
153 	string[] bindAddresses = ["0.0.0.0"];
154 	string host = "localhost"; // host name
155 	TLSContext sslContext;
156 	bool requireSSL = false; // require STARTTLS on unencrypted connections
157 
158 	deprecated @property ref inout(bool) enableSsl() inout { return _enableSsl; }
159 	deprecated @property ref inout(string) sslCertFile() inout { return _sslCertFile; }
160 	deprecated @property ref inout(string) sslKeyFile() inout { return _sslKeyFile; }
161 	deprecated @property ref inout(bool) requireSsl() inout { return requireSSL; }
162 
163 	private bool _enableSsl = false;
164 	private string _sslCertFile;
165 	private string _sslKeyFile;
166 }
167 
168 deprecated alias NntpServerSettings = NNTPServerSettings;
169 
170 
171 class NNTPServerRequest {
172 	private {
173 		InputStreamProxy m_stream;
174 		NNTPBodyReader m_reader;
175 	}
176 
177 	string command;
178 	string[] parameters;
179 	string peerAddress;
180 
181 	this(InputStreamProxy str)
182 	{
183 		m_stream = str;
184 	}
185 
186 	void enforceNParams(size_t n, string syntax = null) {
187 		enforce(parameters.length == n, NNTPStatus.commandSyntaxError, syntax ? "Expected "~syntax : "Wrong number of arguments.");
188 	}
189 
190 	void enforceNParams(size_t nmin, size_t nmax, string syntax = null) {
191 		enforce(parameters.length >= nmin && parameters.length <= nmax,
192 			NNTPStatus.commandSyntaxError, syntax ? "Expected "~syntax : "Wrong number of arguments.");
193 	}
194 
195 	void enforce(bool cond, NNTPStatus status, string message)
196 	{
197 		.enforce(cond, message);
198 	}
199 
200 	@property InputStream bodyReader()
201 	{
202 		if( !m_reader ) m_reader = new NNTPBodyReader(m_stream);
203 		return m_reader;
204 	}
205 }
206 
207 deprecated alias NntpServerRequest = NNTPServerRequest;
208 
209 
210 class NNTPServerResponse {
211 	private {
212 		OutputStreamProxy m_stream;
213 		NNTPBodyWriter m_bodyWriter;
214 		bool m_headerWritten = false;
215 		bool m_bodyWritten = false;
216 	}
217 
218 	int status;
219 	string statusText;
220 
221 	this(OutputStreamProxy stream)
222 	{
223 		m_stream = stream;
224 	}
225 
226 	void restart()
227 	{
228 		finalize();
229 		m_headerWritten = false;
230 	}
231 
232 	void writeVoidBody()
233 	{
234 		assert(!m_bodyWritten);
235 		assert(!m_headerWritten);
236 		writeHeader();
237 	}
238 
239 	@property OutputStream bodyWriter()
240 	{
241 		if( !m_headerWritten ) writeHeader();
242 		if( !m_bodyWriter ) m_bodyWriter = new NNTPBodyWriter(m_stream);
243 		return m_bodyWriter;
244 	}
245 
246 	private void writeHeader()
247 	{
248 		assert(!m_bodyWritten);
249 		assert(!m_headerWritten);
250 		m_headerWritten = true;
251 		//if( !statusText.length ) statusText = getNNTPStatusString(status);
252 		m_stream.write(to!string(status) ~ " " ~ statusText ~ "\r\n");
253 		logDebug("%s %s", status, statusText);
254 	}
255 
256 	private void finalize()
257 	{
258 		if( m_bodyWriter ){
259 			m_bodyWriter.finalize();
260 			m_bodyWriter = null;
261 		}
262 	}
263 }
264 
265 deprecated alias NntpServerResponse = NNTPServerResponse;
266 
267 
268 private string[] spaceSplit(string str)
269 {
270 	string[] ret;
271 	str = stripLeft(str);
272 	while(str.length){
273 		auto idx = str.countUntil(' ');
274 		if( idx > 0 ){
275 			ret ~= str[0 .. idx];
276 			str = str[idx+1 .. $];
277 		} else {
278 			ret ~= str;
279 			break;
280 		}
281 		str = stripLeft(str);
282 	}
283 	return ret;
284 }