1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /*
3  * Pan - A Newsreader for Gtk+
4  * Copyright (C) 2002-2006  Charles Kerr <charles@rebelbase.com>
5  *
6  * This program is free software; you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation; version 2 of the License.
9  *
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.
14  *
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/>.
17  *
18  */
19 
20 #include <config.h>
21 #include <cassert>
22 #include <cstdarg>
23 #include <cstdlib> // abort, atoi, strtoul
24 #include <cstdio> // snprintf
25 #include <glib.h>
26 #include <glib/gi18n.h>
27 #include <pan/general/debug.h>
28 #include <pan/general/log.h>
29 #include <pan/general/messages.h>
30 #include "nntp.h"
31 
32 using namespace pan;
33 
34 namespace
35 {
36    std::string
build_command(const char * fmt,...)37    build_command (const char * fmt, ...)
38    {
39       std::string cmd;
40 
41       if (fmt)
42       {
43          va_list args;
44          va_start (args, fmt);
45          char * str = g_strdup_vprintf (fmt, args);
46          va_end (args);
47          cmd = str;
48          g_free (str);
49       }
50 
51       return cmd;
52    }
53 };
54 
55 void
fire_done_func(Health health,const StringView & response)56 NNTP :: fire_done_func (Health health, const StringView& response)
57 {
58    if (_listener)
59    {
60       Listener * l = _listener;
61       debug ("I (" << (void*)this << ") am setting my _listener to 0");
62       _compression = false;
63       _listener = 0;
64       l->on_nntp_done (this, health, response);
65    }
66 }
67 
68 /***
69 ****  WRITING
70 ***/
71 
72 bool
on_socket_response(Socket * sock UNUSED,const StringView & line_in)73 NNTP :: on_socket_response (Socket * sock UNUSED, const StringView& line_in)
74 {
75 
76    enum State { CMD_FAIL, CMD_DONE, CMD_MORE, CMD_NEXT, CMD_RETRY };
77    State state;
78    StringView line (line_in);
79 
80    // strip off trailing \r\n
81    if (line.len>=2 && line.str[line.len-2]=='\r' && line.str[line.len-1]=='\n')
82      line.truncate (line.len-2);
83 
84    if (_nntp_response_text)
85    {
86       state = CMD_MORE;
87 
88       if (line.len==1 && line.str[0]=='.') // end-of-list
89       {
90          state = CMD_DONE;
91          _nntp_response_text = false;
92       }
93       else if (!_compression)
94       {
95          if (line.len>=2 && line.str[0]=='.' && line.str[1]=='.') // rfc 977: 2.4.1
96             line.rtruncate (line.len-1);
97 
98          assert (_listener != 0);
99          if (_listener)
100             _listener->on_nntp_line (this, line);
101       }
102 
103       if (_compression)
104       {
105 
106         StringView l(line);
107         if (_listener)
108           _listener->on_nntp_line (this, l);
109 
110         if (l.str[l.len-1]=='.') // gzip compression, ends with a single "."
111         {
112           _nntp_response_text = false;
113           _compression = false;
114           state = CMD_DONE;
115         }
116       }
117    }
118    else
119    {
120      int code;
121      //check for compression, enable once, disable at end
122      if (!_compression)
123        _compression = strcasestr (line_in.str, COMPRESS_GZIP);
124 
125      code = atoi (line.str);
126      switch (code)
127      {
128         case SERVER_READY:
129         case SERVER_READY_NO_POSTING:
130         case SERVER_READY_STREAMING_OK:
131            state = CMD_DONE;
132            break;
133 
134         case ARTICLE_POSTED_OK:
135         case GOODBYE:
136         case XOVER_NO_ARTICLES:
137            state = CMD_DONE;
138            break;
139 
140         case AUTH_REQUIRED: { // must send username
141            if (!_username.empty()) {
142              _commands.push_front (_previous_command);
143              _socket->write_command_va (this, "AUTHINFO USER %s\r\n", _username.c_str());
144              state = CMD_NEXT;
145            } else {
146              std::string host;
147              _socket->get_host (host);
148              Log::add_err_va (_("%s requires a username, but none is set."), host.c_str());
149              state = CMD_FAIL;
150            }
151            break;
152         }
153 
154         case AUTH_NEED_MORE: { // must send password
155           if (!_password.empty()) {
156              _socket->write_command_va (this, "AUTHINFO PASS %s\r\n", _password.c_str());
157              state = CMD_NEXT;
158           } else {
159              std::string host;
160              _socket->get_host (host);
161              Log::add_err_va (_("%s requires a password, but none is set."), host.c_str());
162              state = CMD_FAIL;
163           }
164           break;
165         }
166 
167         case AUTH_ACCEPTED:
168           CompressionType ctype;
169            _server_info.get_server_compression_type(_server, ctype);
170            if (ctype == HEADER_COMPRESS_XFEATURE)
171            {
172              // try to enable compression xfeature
173              _socket->write_command (ENABLE_COMPRESS_GZIP, this);
174              state = CMD_NEXT;
175            } else
176              state = CMD_DONE;
177            break;
178 
179         case FEATURE_ENABLED:
180            state= CMD_DONE;
181            break;
182 
183         case GROUP_RESPONSE: {
184            // response is of form "211 qty low high group_name"
185            StringView tok, myline (line);
186            myline.pop_token (tok, ' ');
187            myline.pop_token (tok, ' ');
188            const unsigned long aqty (strtoul (tok.str, NULL, 10));
189            myline.pop_token (tok, ' ');
190            const unsigned long alo (strtoul (tok.str, NULL, 10));
191            myline.pop_token (tok, ' ');
192            const unsigned long ahi (strtoul (tok.str, NULL, 10));
193            myline.pop_token (tok, ' ');
194            const pan::Quark group (tok);
195            if (_listener)
196               _listener->on_nntp_group (this, group, aqty, alo, ahi);
197            _group = group;
198             state = CMD_DONE;
199            break;
200         }
201 
202         case SEND_ARTICLE_NOW:
203            // ready to get article; send it now
204            _socket->write_command (_post, this);
205            state = CMD_NEXT;
206            break;
207 
208         case NO_POSTING:
209         case POSTING_FAILED:
210           // if we hit a dupe, we silently continue
211           if (line.strstr("435"))
212           {
213             state = CMD_DONE;
214             break;
215           }
216           break;
217         case GROUP_NONEXISTENT:
218            state = CMD_FAIL;
219            break;
220         case XOVER_FOLLOWS:
221         case ARTICLE_FOLLOWS:
222         case NEWGROUPS_FOLLOWS:
223         case INFORMATION_FOLLOWS:
224            state = CMD_MORE;
225            _nntp_response_text = true;
226            break;
227 
228         case AUTH_REJECTED:
229         case NO_GROUP_SELECTED:
230         case ERROR_CMD_NOT_UNDERSTOOD:
231         case ERROR_CMD_NOT_SUPPORTED:
232         case NO_PERMISSION:
233         case FEATURE_NOT_SUPPORTED: {
234            std::string cmd (_previous_command);
235            if (cmd.size()>=2 && cmd[cmd.size()-1]=='\n' && cmd[cmd.size()-2]=='\r')
236              cmd.resize (cmd.size()-2);
237            if (code == ERROR_CMD_NOT_UNDERSTOOD &&
238                !strcmp (cmd.c_str(), "MODE READER")) {
239              // server clearly doesn't need MODE READER, ignore the error
240              state = CMD_DONE;
241              break;
242            }
243            std::string host;
244            _socket->get_host (host);
245            Log::add_err_va (_("Sending “%s” to %s returned an error: %s"),
246                             cmd.c_str(),
247                             host.c_str(),
248                             line.to_string().c_str());
249            state = CMD_FAIL;
250            break;
251         }
252 
253         case NO_SUCH_ARTICLE_NUMBER:
254         case NO_SUCH_ARTICLE:
255            state = CMD_FAIL;
256            break;
257 
258         case TOO_MANY_CONNECTIONS:
259            state = CMD_RETRY;
260            break;
261 
262         default: {
263            std::string cmd (_previous_command);
264            if (cmd.size()>=2 && cmd[cmd.size()-1]=='\n' && cmd[cmd.size()-2]=='\r')
265              cmd.resize (cmd.size()-2);
266            std::string host;
267            _socket->get_host (host);
268            Log::add_err_va (_("Sending “%s” to %s returned an unrecognized response: “%s”"),
269                             _previous_command.c_str(),
270                             host.c_str(),
271                             line.to_string().c_str());
272            state = CMD_FAIL;
273            break;
274         }
275      }
276    }
277 
278    if ((state == CMD_DONE) && !_commands.empty())
279    {
280      write_next_command ();
281      state = CMD_NEXT;
282    }
283 
284    bool more;
285    switch (state) {
286       case CMD_FAIL: fire_done_func (ERR_COMMAND, line); more = false; break;
287       case CMD_DONE: if (_commands.empty()) fire_done_func (OK, line); more = false; break;
288       case CMD_MORE: more = true; break; // keep listening for more on this command
289       case CMD_NEXT: more = false; break; // no more responses on this command; wait for next...
290       case CMD_RETRY: fire_done_func (ERR_NETWORK, line); more = false; break;
291       default: abort(); break;
292    }
293    return more;
294 }
295 
296 void
on_socket_abort(Socket * sock UNUSED)297 NNTP :: on_socket_abort (Socket * sock UNUSED)
298 {
299    fire_done_func (ERR_NETWORK, StringView());
300 }
301 
302 void
on_socket_error(Socket * sock UNUSED)303 NNTP :: on_socket_error (Socket * sock UNUSED)
304 {
305    _socket_error = true;
306    fire_done_func (ERR_NETWORK, StringView());
307 }
308 
309 /*
310 namespace
311 {
312    void
313    ensure_trailing_crlf (GString * g)
314    {
315       if (g->len<2 || g->str[g->len-2]!='\r' || g->str[g->len-1]!='\n')
316          g_string_append (g, "\r\n");
317    }
318 };
319 */
320 
321 void
write_next_command()322 NNTP :: write_next_command ()
323 {
324    assert (!_commands.empty());
325 
326    //for (strings_t::const_iterator it=_commands.begin(), end=_commands.end(); it!=end; ++it)
327    //   debug ("command [" << *it << ']');
328 
329    _previous_command = _commands.front ();
330    _commands.pop_front ();
331    debug ("nntp " << this << " writing to socket " << _socket << " on server " << _server << " this command: [" << _previous_command << ']');
332    _socket->write_command (_previous_command, this);
333 }
334 
335 /***
336 ****
337 ***/
338 
339 void
help(Listener * l)340 NNTP :: help (Listener * l)
341 {
342    _listener = l;
343    _commands.push_back ("HELP \r\n");
344    write_next_command();
345 }
346 
347 void
enter_group(const Quark & group)348 NNTP :: enter_group (const Quark& group)
349 {
350    if (group != _group)
351     _commands.push_back (build_command ("GROUP %s\r\n",group.c_str()));
352 }
353 
354 
355 void
xover(const Quark & group,uint64_t low,uint64_t high,Listener * l)356 NNTP :: xover (const Quark   & group,
357                uint64_t        low,
358                uint64_t        high,
359                Listener      * l)
360 {
361    _listener = l;
362 
363    enter_group(group);
364    _commands.push_back (build_command ("XOVER %" G_GUINT64_FORMAT"-%" G_GUINT64_FORMAT"\r\n", low, high));
365    write_next_command ();
366 }
367 
368 void
xzver(const Quark & group,uint64_t low,uint64_t high,Listener * l)369 NNTP :: xzver (const Quark   & group,
370                uint64_t        low,
371                uint64_t        high,
372                Listener      * l)
373 {
374    _listener = l;
375 
376    enter_group(group);
377    _commands.push_back (build_command ("XZVER %" G_GUINT64_FORMAT"-%" G_GUINT64_FORMAT"\r\n", low, high));
378    write_next_command ();
379 }
380 
381 //TODO
382 void
xover_count_only(const Quark & group,Listener * l)383 NNTP :: xover_count_only (const Quark   & group,
384                           Listener      * l)
385 {
386    _listener = l;
387 
388    enter_group(group);
389    _commands.push_back (build_command ("XOVER"));
390    write_next_command ();
391 }
392 
393 void
list_newsgroups(Listener * l)394 NNTP :: list_newsgroups (Listener * l)
395 {
396    _listener = l;
397    _commands.push_back ("LIST NEWSGROUPS\r\n");
398    write_next_command ();
399 }
400 
401 void
list(Listener * l)402 NNTP :: list (Listener * l)
403 {
404    _listener = l;
405    _commands.push_back ("LIST\r\n");
406    write_next_command ();
407 }
408 
409 void
article(const Quark & group,uint64_t article_number,Listener * l)410 NNTP :: article (const Quark     & group,
411                  uint64_t          article_number,
412                  Listener        * l)
413 {
414    _listener = l;
415 
416    enter_group(group);
417 
418    _commands.push_back (build_command ("ARTICLE %" G_GUINT64_FORMAT"\r\n", article_number));
419 
420    write_next_command ();
421 }
422 
423 void
article(const Quark & group,const char * message_id,Listener * l)424 NNTP :: article (const Quark     & group,
425                  const char      * message_id,
426                  Listener        * l)
427 {
428    _listener = l;
429 
430    enter_group(group);
431 
432    _commands.push_back (build_command ("ARTICLE %s\r\n", message_id));
433 
434    write_next_command ();
435 }
436 
437 void
get_headers(const Quark & group,const char * message_id,Listener * l)438 NNTP :: get_headers (const Quark     & group,
439                      const char      * message_id,
440                      Listener        * l)
441 {
442   _listener = l;
443 
444    enter_group(group);
445 
446    _commands.push_back (build_command ("HEAD %s\r\n", message_id));
447 
448    write_next_command ();
449 }
450 
451 void
get_headers(const Quark & group,uint64_t article_number,Listener * l)452 NNTP :: get_headers (const Quark     & group,
453                      uint64_t          article_number,
454                      Listener        * l)
455 {
456    _listener = l;
457 
458    enter_group(group);
459 
460    _commands.push_back (build_command ("HEAD %" G_GUINT64_FORMAT"\r\n", article_number));
461 
462    write_next_command ();
463 }
464 
465 void
get_body(const Quark & group,const char * message_id,Listener * l)466 NNTP :: get_body (const Quark     & group,
467                   const char      * message_id,
468                   Listener        * l)
469 {
470   _listener = l;
471 
472    enter_group(group);
473 
474    _commands.push_back (build_command ("BODY %s\r\n", message_id));
475 
476    write_next_command ();
477 }
478 
479 void
get_body(const Quark & group,uint64_t article_number,Listener * l)480 NNTP :: get_body (const Quark     & group,
481                   uint64_t          article_number,
482                   Listener        * l)
483 {
484    _listener = l;
485 
486    enter_group(group);
487 
488    _commands.push_back (build_command ("BODY %" G_GUINT64_FORMAT"\r\n", article_number));
489 
490    write_next_command ();
491 }
492 
493 
494 void
group(const Quark & group,Listener * l)495 NNTP :: group (const Quark  & group,
496                Listener     * l)
497 {
498    _listener = l;
499 
500    _commands.push_back (build_command ("GROUP %s\r\n", group.c_str()));
501    debug ("_commands.size(): " << _commands.size());
502    write_next_command ();
503 }
504 
505 
506 void
goodbye(Listener * l)507 NNTP :: goodbye (Listener * l)
508 {
509    _listener = l;
510    _commands.push_back ("QUIT\r\n");
511    write_next_command ();
512 }
513 
514 void
handshake(Listener * l)515 NNTP :: handshake (Listener * l)
516 {
517   _listener = l;
518 
519   // queue up two or three commands:
520   // (1) handshake, which is an empty string
521   // (2) if we've got a username, offer it to the server.
522   // (3) mode reader.  the `group' command is only available after `mode reader'. (#343814)
523   _commands.push_back ("");
524   if (!_username.empty()) {
525     char buf[512];
526     snprintf (buf, sizeof(buf), "AUTHINFO USER %s\r\n", _username.c_str());
527     _commands.push_back (buf);
528   }
529   _commands.push_back ("MODE READER\r\n");
530 
531   write_next_command ();
532 }
533 
534 void
noop(Listener * l)535 NNTP :: noop (Listener * l)
536 {
537    _listener = l;
538    _commands.push_back ("MODE READER\r\n");
539    write_next_command ();
540 }
541 
542 namespace
543 {
544   // non-recursive search and replace.
replace_linear(std::string & s,const char * old_text,const char * new_text)545   void replace_linear (std::string& s, const char* old_text, const char * new_text)
546   {
547     std::string::size_type pos (0);
548     while (((pos = s.find (old_text, pos))) != std::string::npos) {
549       s.replace (pos, strlen(old_text), new_text);
550       pos += strlen(new_text);
551     }
552   }
553 }
554 
555 void
post(const StringView & msg,Listener * l)556 NNTP :: post (const StringView  & msg,
557               Listener          * l)
558 {
559   _listener = l;
560 
561   std::string s (msg.str, msg.len);
562   if (s.empty() || s[s.size()-1]!='\n') s += '\n';
563   replace_linear (s, "\n.", "\n..");
564   replace_linear (s, "\n", "\r\n");
565   replace_linear (s, "\r\r\n.", "\r\n");
566   s += ".\r\n";
567 
568   // if we're in mute mode, don't post
569   if (0)
570   {
571     std::cerr << LINE_ID
572               << "Mute: Your Message won't be posted." << std::endl
573               << "Your Message:" << std::endl
574               << s << std::endl
575               << "<end of message>" << std::endl;
576   }
577   else
578   {
579     _post = s;
580     _commands.push_back ("POST\r\n");
581     write_next_command ();
582   }
583 }
584