gpopserverprotocol.cpp
Go to the documentation of this file.
1 //
2 // Copyright (C) 2001-2013 Graeme Walker <graeme_walker@users.sourceforge.net>
3 //
4 // This program is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // This program is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 // GNU General Public License for more details.
13 //
14 // You should have received a copy of the GNU General Public License
15 // along with this program. If not, see <http://www.gnu.org/licenses/>.
16 // ===
17 //
18 // gpopserverprotocol.cpp
19 //
20 
21 #include "gdef.h"
22 #include "gpop.h"
23 #include "gpopserverprotocol.h"
24 #include "gstr.h"
25 #include "gmemory.h"
26 #include "gbase64.h"
27 #include "gassert.h"
28 #include "glog.h"
29 #include <sstream>
30 
31 GPop::ServerProtocol::ServerProtocol( Sender & sender , Security & security , Store & store , const Secrets & secrets ,
32  const Text & text , GNet::Address peer_address , Config ) :
33  m_text(text) ,
34  m_sender(sender) ,
35  m_security(security) ,
36  m_store(store) ,
37  m_store_lock(m_store) ,
38  m_secrets(secrets) ,
39  m_auth(m_secrets) ,
40  m_peer_address(peer_address) ,
41  m_fsm(sStart,sEnd,s_Same,s_Any) ,
42  m_body_limit(-1L) ,
43  m_in_body(false) ,
44  m_secure(false)
45 {
46  // (dont send anything to the peer from this ctor -- the Sender object is not fuly constructed)
47 
48  m_fsm.addTransition( eStat , sActive , sActive , &GPop::ServerProtocol::doStat ) ;
49  m_fsm.addTransition( eList , sActive , sActive , &GPop::ServerProtocol::doList ) ;
50  m_fsm.addTransition( eRetr , sActive , sData , &GPop::ServerProtocol::doRetr , sActive ) ;
51  m_fsm.addTransition( eTop , sActive , sData , &GPop::ServerProtocol::doTop , sActive ) ;
52  m_fsm.addTransition( eDele , sActive , sActive , &GPop::ServerProtocol::doDele ) ;
53  m_fsm.addTransition( eNoop , sActive , sActive , &GPop::ServerProtocol::doNoop ) ;
54  m_fsm.addTransition( eRset , sActive , sActive , &GPop::ServerProtocol::doRset ) ;
55  m_fsm.addTransition( eUidl , sActive , sActive , &GPop::ServerProtocol::doUidl ) ;
56  m_fsm.addTransition( eSent , sData , sActive , &GPop::ServerProtocol::doNothing ) ;
57  m_fsm.addTransition( eUser , sStart , sStart , &GPop::ServerProtocol::doUser ) ;
58  m_fsm.addTransition( ePass , sStart , sActive , &GPop::ServerProtocol::doPass , sStart ) ;
59  m_fsm.addTransition( eApop , sStart , sActive , &GPop::ServerProtocol::doApop , sStart ) ;
60  m_fsm.addTransition( eQuit , sStart , sEnd , &GPop::ServerProtocol::doQuitEarly ) ;
61  m_fsm.addTransition( eCapa , sStart , sStart , &GPop::ServerProtocol::doCapa ) ;
62  m_fsm.addTransition( eCapa , sActive , sActive , &GPop::ServerProtocol::doCapa ) ;
63  if( m_security.securityEnabled() )
64  m_fsm.addTransition( eStls , sStart , sStart , &GPop::ServerProtocol::doStls , sStart ) ;
65  m_fsm.addTransition( eAuth , sStart , sAuth , &GPop::ServerProtocol::doAuth , sStart ) ;
66  m_fsm.addTransition( eAuthData , sAuth , sActive , &GPop::ServerProtocol::doAuthData , sStart ) ;
67  m_fsm.addTransition( eCapa , sActive , sActive , &GPop::ServerProtocol::doCapa ) ;
68  m_fsm.addTransition( eQuit , sActive , sEnd , &GPop::ServerProtocol::doQuit ) ;
69 }
70 
72 {
73  sendInit() ;
74 }
75 
77 {
78 }
79 
80 void GPop::ServerProtocol::sendInit()
81 {
82  std::string greeting = std::string() + "+OK " + m_text.greeting() ;
83  std::string apop_challenge = m_auth.challenge() ;
84  if( ! apop_challenge.empty() )
85  {
86  greeting.append( " " ) ;
87  greeting.append( apop_challenge ) ;
88  }
89  send( greeting ) ;
90 }
91 
92 void GPop::ServerProtocol::sendOk()
93 {
94  send( "+OK" ) ;
95 }
96 
97 void GPop::ServerProtocol::sendError( const std::string & more )
98 {
99  if( more.empty() )
100  sendError() ;
101  else
102  send( std::string() + "-ERR " + more ) ;
103 }
104 
105 void GPop::ServerProtocol::sendError()
106 {
107  send( "-ERR" ) ;
108 }
109 
110 void GPop::ServerProtocol::apply( const std::string & line )
111 {
112  // decode the event
113  Event event = m_fsm.state() == sAuth ? eAuthData : commandEvent(commandWord(line)) ;
114 
115  // log the input
116  std::string log_text = G::Str::printable(line) ;
117  if( event == ePass )
118  log_text = (commandPart(line,0U)+" [password not logged]") ;
119  if( event == eAuthData )
120  log_text = ("[password not logged]") ;
121  if( event == eAuth && !commandPart(line,1U).empty() )
122  log_text = (commandPart(line,0U)+" "+commandPart(line,1U) + " [password not logged]") ;
123  G_LOG( "GPop::ServerProtocol: rx<<: \"" << log_text << "\"" ) ;
124 
125  // apply the event to the state machine
126  State new_state = m_fsm.apply( *this , event , line ) ;
127  const bool protocol_error = new_state == s_Any ;
128  if( protocol_error )
129  sendError() ;
130 
131  // squirt data down the pipe if appropriate
132  if( new_state == sData )
133  sendContent() ;
134 }
135 
136 void GPop::ServerProtocol::sendContent()
137 {
138  // send until no more content or until blocked by flow-control
139  std::string line( 200 , '.' ) ;
140  size_t n = 0 ;
141  bool end_of_content = false ;
142  while( sendContentLine(line,end_of_content) )
143  n++ ;
144 
145  G_LOG( "GPop::ServerProtocol: tx>>: [" << n << " line(s) of content]" ) ;
146 
147  if( end_of_content )
148  {
149  G_LOG( "GPop::ServerProtocol: tx>>: ." ) ;
150  m_content <<= 0 ; // free up resources
151  m_fsm.apply( *this , eSent , "" ) ; // sData -> sActive
152  }
153 }
154 
156 {
157  G_DEBUG( "GPop::ServerProtocol::resume: flow control released" ) ;
158  if( m_fsm.state() == sData )
159  sendContent() ;
160 }
161 
162 bool GPop::ServerProtocol::sendContentLine( std::string & line , bool & stop )
163 {
164  G_ASSERT( m_content.get() != NULL ) ;
165 
166  // maintain the line limit
167  bool limited = m_in_body && m_body_limit == 0L ;
168  if( m_body_limit > 0L && m_in_body )
169  m_body_limit-- ;
170 
171  // read the line of text
172  line.erase( 1U ) ; // leave "."
173  G::Str::readLineFrom( *(m_content.get()) , crlf() , line , false ) ;
174 
175  // add crlf and choose an offset
176  bool eof = m_content->fail() || m_content->bad() ;
177  size_t offset = 0U ;
178  if( eof || limited )
179  {
180  line.erase( 1U ) ;
181  line.append( crlf() ) ;
182  }
183  else
184  {
185  line.append( crlf() ) ;
186  offset = line.at(1U) == '.' ? 0U : 1U ;
187  }
188 
189  // maintain the in-body flag
190  if( !m_in_body && line.length() == (offset+2U) )
191  m_in_body = true ;
192 
193  // send it
194  bool line_fully_sent = m_sender.protocolSend( line , offset ) ;
195 
196  // continue to send while not finished or blocked by flow-control
197  stop = ( limited || eof ) && line_fully_sent ;
198  const bool pause = limited || eof || ! line_fully_sent ;
199  return !pause ;
200 }
201 
202 int GPop::ServerProtocol::commandNumber( const std::string & line , int default_ , size_t index ) const
203 {
204  int number = default_ ;
205  try
206  {
207  number = G::Str::toInt( commandParameter(line,index) ) ;
208  }
209  catch( G::Str::Overflow & ) // defaulted
210  {
211  }
212  catch( G::Str::InvalidFormat & ) // defaulted
213  {
214  }
215  return number ;
216 }
217 
218 std::string GPop::ServerProtocol::commandWord( const std::string & line ) const
219 {
220  return G::Str::upper(commandPart(line,0U)) ;
221 }
222 
223 std::string GPop::ServerProtocol::commandPart( const std::string & line , size_t index ) const
224 {
225  G::Strings part ;
226  G::Str::splitIntoTokens( line , part , " \t\r\n" ) ;
227  if( index >= part.size() ) return std::string() ;
228  G::Strings::iterator p = part.begin() ;
229  for( ; index > 0 ; ++p , index-- ) ;
230  return *p ;
231 }
232 
233 std::string GPop::ServerProtocol::commandParameter( const std::string & line_in , size_t index ) const
234 {
235  return commandPart( line_in , index ) ;
236 }
237 
238 GPop::ServerProtocol::Event GPop::ServerProtocol::commandEvent( const std::string & command ) const
239 {
240  if( command == "QUIT" ) return eQuit ;
241  if( command == "STAT" ) return eStat ;
242  if( command == "LIST" ) return eList ;
243  if( command == "RETR" ) return eRetr ;
244  if( command == "DELE" ) return eDele ;
245  if( command == "NOOP" ) return eNoop ;
246  if( command == "RSET" ) return eRset ;
247  //
248  if( command == "TOP" ) return eTop ;
249  if( command == "UIDL" ) return eUidl ;
250  if( command == "USER" ) return eUser ;
251  if( command == "PASS" ) return ePass ;
252  if( command == "APOP" ) return eApop ;
253  if( command == "AUTH" ) return eAuth ;
254  if( command == "CAPA" ) return eCapa ;
255  if( command == "STLS" ) return eStls ;
256 
257  return eUnknown ;
258 }
259 
260 void GPop::ServerProtocol::doQuitEarly( const std::string & , bool & )
261 {
262  send( std::string() + "+OK " + m_text.quit() ) ;
263  throw ProtocolDone() ;
264 }
265 
266 void GPop::ServerProtocol::doQuit( const std::string & , bool & )
267 {
268  m_store_lock.commit() ;
269  send( std::string() + "+OK " + m_text.quit() ) ;
270  throw ProtocolDone() ;
271 }
272 
273 void GPop::ServerProtocol::doStat( const std::string & , bool & )
274 {
275  std::ostringstream ss ;
276  ss << "+OK " << m_store_lock.messageCount() << " " << m_store_lock.totalByteCount() ;
277  send( ss.str() ) ;
278 }
279 
280 void GPop::ServerProtocol::doUidl( const std::string & line , bool & )
281 {
282  sendList( line , true ) ;
283 }
284 
285 void GPop::ServerProtocol::doList( const std::string & line , bool & )
286 {
287  sendList( line , false ) ;
288 }
289 
290 void GPop::ServerProtocol::sendList( const std::string & line , bool uidl )
291 {
292  std::string id_string = commandParameter( line ) ;
293 
294  // parse and check the id if supplied
295  int id = -1 ;
296  if( ! id_string.empty() )
297  {
298  id = commandNumber( line , -1 ) ;
299  if( !m_store_lock.valid(id) )
300  {
301  sendError( "invalid id" ) ;
302  return ;
303  }
304  }
305 
306  // send back the list with sizes or uidls
307  bool multi_line = id == -1 ;
308  GPop::StoreLock::List list = m_store_lock.list( id ) ;
309  std::ostringstream ss ;
310  ss << "+OK " ;
311  if( multi_line ) ss << list.size() << " message(s)" << crlf() ;
312  for( GPop::StoreLock::List::iterator p = list.begin() ; p != list.end() ; ++p )
313  {
314  ss << (*p).id << " " ;
315  if( uidl ) ss << (*p).uidl ;
316  if( !uidl ) ss << (*p).size ;
317  if( multi_line ) ss << crlf() ;
318  }
319  if( multi_line ) ss << "." ;
320  send( ss.str() ) ;
321 }
322 
323 void GPop::ServerProtocol::doRetr( const std::string & line , bool & more )
324 {
325  int id = commandNumber( line , -1 ) ;
326  if( id == -1 || ! m_store_lock.valid(id) )
327  {
328  more = false ; // stay in the same state
329  sendError() ;
330  }
331  else
332  {
333  std::auto_ptr<std::istream> content( m_store_lock.get(id) ) ; // for gcc2.95
334  m_content <<= content.release() ;
335  m_body_limit = -1L ;
336 
337  std::ostringstream ss ;
338  ss << "+OK " << m_store_lock.byteCount(id) << " octets" ;
339  send( ss.str() ) ;
340  }
341 }
342 
343 void GPop::ServerProtocol::doTop( const std::string & line , bool & more )
344 {
345  int id = commandNumber( line , -1 , 1U ) ;
346  int n = commandNumber( line , -1 , 2U ) ;
347  G_DEBUG( "ServerProtocol::doTop: " << id << ", " << n ) ;
348  if( id == -1 || ! m_store_lock.valid(id) || n < 0 )
349  {
350  more = false ; // stay in the same state
351  sendError() ;
352  }
353  else
354  {
355  std::auto_ptr<std::istream> content( m_store_lock.get(id) ) ; // for gcc2.95
356  m_content <<= content.release() ;
357  m_body_limit = n ;
358  m_in_body = false ;
359  sendOk() ;
360  }
361 }
362 
363 void GPop::ServerProtocol::doDele( const std::string & line , bool & )
364 {
365  int id = commandNumber( line , -1 ) ;
366  if( id == -1 || ! m_store_lock.valid(id) )
367  {
368  sendError() ;
369  }
370  else
371  {
372  m_store_lock.remove( id ) ;
373  sendOk() ;
374  }
375 }
376 
377 void GPop::ServerProtocol::doRset( const std::string & , bool & )
378 {
379  m_store_lock.rollback() ;
380  sendOk() ;
381 }
382 
383 void GPop::ServerProtocol::doNoop( const std::string & , bool & )
384 {
385  sendOk() ;
386 }
387 
388 void GPop::ServerProtocol::doNothing( const std::string & , bool & )
389 {
390 }
391 
392 void GPop::ServerProtocol::doAuth( const std::string & line , bool & ok )
393 {
394  std::string mechanism = G::Str::upper( commandParameter(line) ) ;
395 
396  if( mechanism.empty() )
397  {
398  // completely non-standard behaviour, but required by some clients
399  ok = false ; // dont change state
400  std::string list = m_auth.mechanisms() ;
401  G::Str::replaceAll(list," ",crlf()) ;
402  send( "+OK" ) ;
403  send( list ) ;
404  send( "." ) ;
405  }
406  else if( m_auth.sensitive() && ! m_secure )
407  {
408  // reject authentication over an unencrypted transport
409  // if authentication is sensitive
410  ok = false ;
411  sendError( "must use STLS before authentication" ) ;
412  }
413  else
414  {
415  std::string initial_response = commandParameter(line,2) ;
416  if( initial_response == "=" )
417  initial_response = std::string() ; // RFC 5034
418 
419  // reject the LOGIN mechanism here since we did not avertise it
420  // and we only want a one-step challenge-response dialogue
421 
422  bool supported = mechanism != "LOGIN" && m_auth.init( mechanism ) ;
423  if( !supported )
424  {
425  ok = false ;
426  sendError( "invalid mechanism" ) ;
427  }
428  else if( m_auth.mustChallenge() && !initial_response.empty() )
429  {
430  ok = false ;
431  sendError( "invalid initial response" ) ;
432  }
433  else if( !initial_response.empty() )
434  {
435  m_fsm.apply( *this , eAuthData , initial_response ) ;
436  }
437  else
438  {
439  std::string initial_challenge = m_auth.challenge() ;
440  send( std::string() + "+ " + G::Base64::encode(initial_challenge) ) ;
441  }
442  }
443 }
444 
445 void GPop::ServerProtocol::doAuthData( const std::string & line , bool & ok )
446 {
447  // (only one-step sasl authentication supported for now)
448  ok = m_auth.authenticated( G::Base64::decode(line) , std::string() ) ;
449  if( ok )
450  {
451  sendOk() ;
452  m_user = m_auth.id() ;
453  lockStore() ;
454  }
455  else
456  {
457  sendError() ;
458  }
459 }
460 
461 void GPop::ServerProtocol::lockStore()
462 {
463  m_store_lock.lock( m_user ) ;
464  G_LOG_S( "GPop::ServerProtocol: pop authentication of " << m_user
465  << " connected from " << m_peer_address.displayString() ) ;
466 }
467 
468 void GPop::ServerProtocol::doStls( const std::string & , bool & )
469 {
470  G_ASSERT( m_security.securityEnabled() ) ;
471  sendOk() ; // "please start tls"
472  m_security.securityStart() ;
473 }
474 
476 {
477  m_secure = true ;
478  sendOk() ; // "hello (again)"
479 }
480 
481 bool GPop::ServerProtocol::mechanismsIncludePlain() const
482 {
483  return m_auth.valid() && m_auth.mechanisms().find("PLAIN") != std::string::npos ;
484 }
485 
486 std::string GPop::ServerProtocol::mechanismsWithoutLogin() const
487 {
488  std::string result ;
489  if( m_auth.valid() )
490  {
491  result = m_auth.mechanisms() ;
492  G::Str::replace( result , "LOGIN" , "" ) ; // could do better
493  G::Str::replace( result , " " , " " ) ;
494  }
495  return result ;
496 }
497 
498 void GPop::ServerProtocol::doCapa( const std::string & , bool & )
499 {
500  send( std::string() + "+OK " + m_text.capa() ) ;
501 
502  // USER/PASS POP3 authentication uses the PLAIN SASL mechanism
503  // so only advertise it if it is available
504  if( mechanismsIncludePlain() )
505  send( "USER" ) ;
506 
507  send( "CAPA" ) ;
508  send( "TOP" ) ;
509  send( "UIDL" ) ;
510 
511  if( m_security.securityEnabled() )
512  send( "STLS" ) ;
513 
514  // don't advertise LOGIN since we cannot do multi-challenge
515  // mechanisms and USER/PASS provides the same functionality
516  //
517  std::string mechanisms = std::string(1U,' ') + mechanismsWithoutLogin() ;
518  if( mechanisms.length() > 1U )
519  send( std::string() + "SASL" + mechanisms ) ;
520 
521  send( "." ) ;
522 }
523 
524 void GPop::ServerProtocol::doUser( const std::string & line , bool & )
525 {
526  if( mechanismsIncludePlain() )
527  {
528  m_user = commandParameter(line) ;
529  send( std::string() + "+OK " + m_text.user(commandParameter(line)) ) ;
530  }
531  else
532  {
533  sendError( "no SASL PLAIN mechanism to do USER/PASS authentication" ) ;
534  }
535 }
536 
537 void GPop::ServerProtocol::doPass( const std::string & line , bool & ok )
538 {
539  // note that USER/PASS POP3 authentication uses the PLAIN SASL mechanism
540  std::string rsp = m_user + std::string(1U,'\0') + m_user + std::string(1U,'\0') + commandParameter(line) ;
541  ok = !m_user.empty() && m_auth.valid() && m_auth.init("PLAIN") &&
542  m_auth.authenticated( rsp , std::string() ) ;
543  if( ok )
544  {
545  lockStore() ;
546  sendOk() ;
547  }
548  else
549  {
550  sendError() ;
551  }
552 }
553 
554 void GPop::ServerProtocol::doApop( const std::string & line , bool & ok )
555 {
556  m_user = commandParameter(line,1) ;
557  std::string rsp = m_user + " " + commandParameter(line,2) ;
558  ok = m_auth.valid() && m_auth.init("APOP") && m_auth.authenticated(rsp,std::string()) ;
559  if( ok )
560  {
561  lockStore() ;
562  sendOk() ;
563  }
564  else
565  {
566  m_user = std::string() ;
567  sendError() ;
568  }
569 }
570 
571 void GPop::ServerProtocol::send( std::string line )
572 {
573  G_LOG( "GPop::ServerProtocol: tx>>: \"" << G::Str::printable(line) << "\"" ) ;
574  line.append( crlf() ) ;
575  m_sender.protocolSend( line , 0U ) ;
576 }
577 
578 const std::string & GPop::ServerProtocol::crlf()
579 {
580  static const std::string s( "\015\012" ) ;
581  return s ;
582 }
583 
584 // ===
585 
587 {
588 }
589 
591 {
592  return "POP3 server ready" ;
593 }
594 
596 {
597  return "signing off" ;
598 }
599 
601 {
602  return "capability list follows" ;
603 }
604 
605 std::string GPop::ServerProtocolText::user( const std::string & id ) const
606 {
607  return std::string() + "user: " + id ;
608 }
609 
610 // ===
611 
613 {
614 }
615 
616 // ===
617 
619 {
620 }
621 
622 // ===
623 
625 {
626 }
627 
628 // ===
629 
631 {
632 }
633 
#define G_LOG_S(expr)
Definition: glog.h:103
static std::string printable(const std::string &in, char escape= '\\')
Returns a printable represention of the given input string.
Definition: gstr.cpp:507
virtual bool securityEnabled() const =0
A simple interface to a store of secrets as used in authentication.
Definition: gpopsecrets.h:44
virtual std::string quit() const
Final override from GPop::ServerProtocol::Text.
static int toInt(const std::string &s)
Converts string 's' to an int.
Definition: gstr.cpp:310
std::list< std::string > Strings
A std::list of std::strings.
Definition: gstrings.h:39
virtual std::string greeting() const
Final override from GPop::ServerProtocol::Text.
The Address class encapsulates an IP transport address.
Definition: gaddress.h:48
virtual std::string capa() const
Final override from GPop::ServerProtocol::Text.
static void splitIntoTokens(const std::string &in, Strings &out, const std::string &ws)
Splits the string into 'ws'-delimited tokens.
Definition: gstr.cpp:714
void init()
Starts the protocol.
virtual ~ServerProtocol()
Destructor.
An interface used by ServerProtocol to send protocol replies.
A message store.
Definition: gpopstore.h:46
An interface used by ServerProtocol to enable TLS.
ServerProtocolText(GNet::Address peer)
Constructor.
#define G_ASSERT(test)
Definition: gassert.h:30
static bool replace(std::string &s, const std::string &from, const std::string &to, size_type *pos_p=NULL)
Replaces 'from' with 'to', starting at offset '*pos_p'.
Definition: gstr.cpp:55
static unsigned int replaceAll(std::string &s, const std::string &from, const std::string &to)
Does a global replace on string 's', replacing all occurences of sub-string 'from' with 'to'...
Definition: gstr.cpp:78
An interface used by ServerProtocol to provide response text strings.
virtual std::string user(const std::string &id) const
Final override from GPop::ServerProtocol::Text.
#define G_LOG(expr)
Definition: glog.h:98
static std::string upper(const std::string &s)
Returns a copy of 's' in which all lowercase characters have been replaced by uppercase characters...
Definition: gstr.cpp:426
static std::string readLineFrom(std::istream &stream, const std::string &eol=std::string())
Reads a line from the stream using the given line terminator.
Definition: gstr.cpp:536
#define G_DEBUG(expr)
Definition: glog.h:95
std::list< Entry > List
Definition: gpopstore.h:106
ServerProtocol(Sender &sender, Security &security, Store &store, const Secrets &secrets, const Text &text, GNet::Address peer_address, Config config)
Constructor.
A structure containing configuration parameters for ServerProtocol. NOT USED.
void addTransition(Event event, State from, State to, Action action)
Adds a transition.
void secure()
Called when the server connection becomes secure.
static std::string decode(const std::string &)
Decodes the given string.
Definition: gbase64.cpp:131
void apply(const std::string &line)
Called on receipt of a string from the client.
static std::string encode(const std::string &s, const std::string &line_break)
Encodes the given string.
Definition: gbase64.cpp:68
void resume()
Called when the Sender can send again.