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 }