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