OLD | NEW |
| (Empty) |
1 # -*- test-case-name: twisted.news.test.test_nntp -*- | |
2 # Copyright (c) 2001-2004 Twisted Matrix Laboratories. | |
3 # See LICENSE for details. | |
4 | |
5 | |
6 """ | |
7 NNTP protocol support. | |
8 | |
9 Maintainer: U{Jp Calderone<mailto:exarkun@twistedmatrix.com>} | |
10 | |
11 The following protocol commands are currently understood:: | |
12 | |
13 LIST LISTGROUP XOVER XHDR | |
14 POST GROUP ARTICLE STAT HEAD | |
15 BODY NEXT MODE STREAM MODE READER SLAVE | |
16 LAST QUIT HELP IHAVE XPATH | |
17 XINDEX XROVER TAKETHIS CHECK | |
18 | |
19 The following protocol commands require implementation:: | |
20 | |
21 NEWNEWS | |
22 XGTITLE XPAT | |
23 XTHREAD AUTHINFO NEWGROUPS | |
24 | |
25 | |
26 Other desired features: | |
27 | |
28 - A real backend | |
29 - More robust client input handling | |
30 - A control protocol | |
31 """ | |
32 | |
33 import time | |
34 import types | |
35 | |
36 try: | |
37 import cStringIO as StringIO | |
38 except: | |
39 import StringIO | |
40 | |
41 from twisted.protocols import basic | |
42 from twisted.python import log | |
43 | |
44 def parseRange(text): | |
45 articles = text.split('-') | |
46 if len(articles) == 1: | |
47 try: | |
48 a = int(articles[0]) | |
49 return a, a | |
50 except ValueError, e: | |
51 return None, None | |
52 elif len(articles) == 2: | |
53 try: | |
54 if len(articles[0]): | |
55 l = int(articles[0]) | |
56 else: | |
57 l = None | |
58 if len(articles[1]): | |
59 h = int(articles[1]) | |
60 else: | |
61 h = None | |
62 except ValueError, e: | |
63 return None, None | |
64 return l, h | |
65 | |
66 | |
67 def extractCode(line): | |
68 line = line.split(' ', 1) | |
69 if len(line) != 2: | |
70 return None | |
71 try: | |
72 return int(line[0]), line[1] | |
73 except ValueError: | |
74 return None | |
75 | |
76 | |
77 class NNTPError(Exception): | |
78 def __init__(self, string): | |
79 self.string = string | |
80 | |
81 def __str__(self): | |
82 return 'NNTPError: %s' % self.string | |
83 | |
84 | |
85 class NNTPClient(basic.LineReceiver): | |
86 MAX_COMMAND_LENGTH = 510 | |
87 | |
88 def __init__(self): | |
89 self.currentGroup = None | |
90 | |
91 self._state = [] | |
92 self._error = [] | |
93 self._inputBuffers = [] | |
94 self._responseCodes = [] | |
95 self._responseHandlers = [] | |
96 | |
97 self._postText = [] | |
98 | |
99 self._newState(self._statePassive, None, self._headerInitial) | |
100 | |
101 | |
102 def gotAllGroups(self, groups): | |
103 "Override for notification when fetchGroups() action is completed" | |
104 | |
105 | |
106 def getAllGroupsFailed(self, error): | |
107 "Override for notification when fetchGroups() action fails" | |
108 | |
109 | |
110 def gotOverview(self, overview): | |
111 "Override for notification when fetchOverview() action is completed" | |
112 | |
113 | |
114 def getOverviewFailed(self, error): | |
115 "Override for notification when fetchOverview() action fails" | |
116 | |
117 | |
118 def gotSubscriptions(self, subscriptions): | |
119 "Override for notification when fetchSubscriptions() action is completed
" | |
120 | |
121 | |
122 def getSubscriptionsFailed(self, error): | |
123 "Override for notification when fetchSubscriptions() action fails" | |
124 | |
125 | |
126 def gotGroup(self, group): | |
127 "Override for notification when fetchGroup() action is completed" | |
128 | |
129 | |
130 def getGroupFailed(self, error): | |
131 "Override for notification when fetchGroup() action fails" | |
132 | |
133 | |
134 def gotArticle(self, article): | |
135 "Override for notification when fetchArticle() action is completed" | |
136 | |
137 | |
138 def getArticleFailed(self, error): | |
139 "Override for notification when fetchArticle() action fails" | |
140 | |
141 | |
142 def gotHead(self, head): | |
143 "Override for notification when fetchHead() action is completed" | |
144 | |
145 | |
146 def getHeadFailed(self, error): | |
147 "Override for notification when fetchHead() action fails" | |
148 | |
149 | |
150 def gotBody(self, info): | |
151 "Override for notification when fetchBody() action is completed" | |
152 | |
153 | |
154 def getBodyFailed(self, body): | |
155 "Override for notification when fetchBody() action fails" | |
156 | |
157 | |
158 def postedOk(self): | |
159 "Override for notification when postArticle() action is successful" | |
160 | |
161 | |
162 def postFailed(self, error): | |
163 "Override for notification when postArticle() action fails" | |
164 | |
165 | |
166 def gotXHeader(self, headers): | |
167 "Override for notification when getXHeader() action is successful" | |
168 | |
169 | |
170 def getXHeaderFailed(self, error): | |
171 "Override for notification when getXHeader() action fails" | |
172 | |
173 | |
174 def gotNewNews(self, news): | |
175 "Override for notification when getNewNews() action is successful" | |
176 | |
177 | |
178 def getNewNewsFailed(self, error): | |
179 "Override for notification when getNewNews() action fails" | |
180 | |
181 | |
182 def gotNewGroups(self, groups): | |
183 "Override for notification when getNewGroups() action is successful" | |
184 | |
185 | |
186 def getNewGroupsFailed(self, error): | |
187 "Override for notification when getNewGroups() action fails" | |
188 | |
189 | |
190 def setStreamSuccess(self): | |
191 "Override for notification when setStream() action is successful" | |
192 | |
193 | |
194 def setStreamFailed(self, error): | |
195 "Override for notification when setStream() action fails" | |
196 | |
197 | |
198 def fetchGroups(self): | |
199 """ | |
200 Request a list of all news groups from the server. gotAllGroups() | |
201 is called on success, getGroupsFailed() on failure | |
202 """ | |
203 self.sendLine('LIST') | |
204 self._newState(self._stateList, self.getAllGroupsFailed) | |
205 | |
206 | |
207 def fetchOverview(self): | |
208 """ | |
209 Request the overview format from the server. gotOverview() is called | |
210 on success, getOverviewFailed() on failure | |
211 """ | |
212 self.sendLine('LIST OVERVIEW.FMT') | |
213 self._newState(self._stateOverview, self.getOverviewFailed) | |
214 | |
215 | |
216 def fetchSubscriptions(self): | |
217 """ | |
218 Request a list of the groups it is recommended a new user subscribe to. | |
219 gotSubscriptions() is called on success, getSubscriptionsFailed() on | |
220 failure | |
221 """ | |
222 self.sendLine('LIST SUBSCRIPTIONS') | |
223 self._newState(self._stateSubscriptions, self.getSubscriptionsFailed) | |
224 | |
225 | |
226 def fetchGroup(self, group): | |
227 """ | |
228 Get group information for the specified group from the server. gotGroup
() | |
229 is called on success, getGroupFailed() on failure. | |
230 """ | |
231 self.sendLine('GROUP %s' % (group,)) | |
232 self._newState(None, self.getGroupFailed, self._headerGroup) | |
233 | |
234 | |
235 def fetchHead(self, index = ''): | |
236 """ | |
237 Get the header for the specified article (or the currently selected | |
238 article if index is '') from the server. gotHead() is called on | |
239 success, getHeadFailed() on failure | |
240 """ | |
241 self.sendLine('HEAD %s' % (index,)) | |
242 self._newState(self._stateHead, self.getHeadFailed) | |
243 | |
244 | |
245 def fetchBody(self, index = ''): | |
246 """ | |
247 Get the body for the specified article (or the currently selected | |
248 article if index is '') from the server. gotBody() is called on | |
249 success, getBodyFailed() on failure | |
250 """ | |
251 self.sendLine('BODY %s' % (index,)) | |
252 self._newState(self._stateBody, self.getBodyFailed) | |
253 | |
254 | |
255 def fetchArticle(self, index = ''): | |
256 """ | |
257 Get the complete article with the specified index (or the currently | |
258 selected article if index is '') or Message-ID from the server. | |
259 gotArticle() is called on success, getArticleFailed() on failure. | |
260 """ | |
261 self.sendLine('ARTICLE %s' % (index,)) | |
262 self._newState(self._stateArticle, self.getArticleFailed) | |
263 | |
264 | |
265 def postArticle(self, text): | |
266 """ | |
267 Attempt to post an article with the specified text to the server. 'text
' | |
268 must consist of both head and body data, as specified by RFC 850. If th
e | |
269 article is posted successfully, postedOk() is called, otherwise postFail
ed() | |
270 is called. | |
271 """ | |
272 self.sendLine('POST') | |
273 self._newState(None, self.postFailed, self._headerPost) | |
274 self._postText.append(text) | |
275 | |
276 | |
277 def fetchNewNews(self, groups, date, distributions = ''): | |
278 """ | |
279 Get the Message-IDs for all new news posted to any of the given | |
280 groups since the specified date - in seconds since the epoch, GMT - | |
281 optionally restricted to the given distributions. gotNewNews() is | |
282 called on success, getNewNewsFailed() on failure. | |
283 | |
284 One invocation of this function may result in multiple invocations | |
285 of gotNewNews()/getNewNewsFailed(). | |
286 """ | |
287 date, timeStr = time.strftime('%y%m%d %H%M%S', time.gmtime(date)).split(
) | |
288 line = 'NEWNEWS %%s %s %s %s' % (date, timeStr, distributions) | |
289 groupPart = '' | |
290 while len(groups) and len(line) + len(groupPart) + len(groups[-1]) + 1 <
NNTPClient.MAX_COMMAND_LENGTH: | |
291 group = groups.pop() | |
292 groupPart = groupPart + ',' + group | |
293 | |
294 self.sendLine(line % (groupPart,)) | |
295 self._newState(self._stateNewNews, self.getNewNewsFailed) | |
296 | |
297 if len(groups): | |
298 self.fetchNewNews(groups, date, distributions) | |
299 | |
300 | |
301 def fetchNewGroups(self, date, distributions): | |
302 """ | |
303 Get the names of all new groups created/added to the server since | |
304 the specified date - in seconds since the ecpoh, GMT - optionally | |
305 restricted to the given distributions. gotNewGroups() is called | |
306 on success, getNewGroupsFailed() on failure. | |
307 """ | |
308 date, timeStr = time.strftime('%y%m%d %H%M%S', time.gmtime(date)).split(
) | |
309 self.sendLine('NEWGROUPS %s %s %s' % (date, timeStr, distributions)) | |
310 self._newState(self._stateNewGroups, self.getNewGroupsFailed) | |
311 | |
312 | |
313 def fetchXHeader(self, header, low = None, high = None, id = None): | |
314 """ | |
315 Request a specific header from the server for an article or range | |
316 of articles. If 'id' is not None, a header for only the article | |
317 with that Message-ID will be requested. If both low and high are | |
318 None, a header for the currently selected article will be selected; | |
319 If both low and high are zero-length strings, headers for all articles | |
320 in the currently selected group will be requested; Otherwise, high | |
321 and low will be used as bounds - if one is None the first or last | |
322 article index will be substituted, as appropriate. | |
323 """ | |
324 if id is not None: | |
325 r = header + ' <%s>' % (id,) | |
326 elif low is high is None: | |
327 r = header | |
328 elif high is None: | |
329 r = header + ' %d-' % (low,) | |
330 elif low is None: | |
331 r = header + ' -%d' % (high,) | |
332 else: | |
333 r = header + ' %d-%d' % (low, high) | |
334 self.sendLine('XHDR ' + r) | |
335 self._newState(self._stateXHDR, self.getXHeaderFailed) | |
336 | |
337 | |
338 def setStream(self): | |
339 """ | |
340 Set the mode to STREAM, suspending the normal "lock-step" mode of | |
341 communications. setStreamSuccess() is called on success, | |
342 setStreamFailed() on failure. | |
343 """ | |
344 self.sendLine('MODE STREAM') | |
345 self._newState(None, self.setStreamFailed, self._headerMode) | |
346 | |
347 | |
348 def quit(self): | |
349 self.sendLine('QUIT') | |
350 self.transport.loseConnection() | |
351 | |
352 | |
353 def _newState(self, method, error, responseHandler = None): | |
354 self._inputBuffers.append([]) | |
355 self._responseCodes.append(None) | |
356 self._state.append(method) | |
357 self._error.append(error) | |
358 self._responseHandlers.append(responseHandler) | |
359 | |
360 | |
361 def _endState(self): | |
362 buf = self._inputBuffers[0] | |
363 del self._responseCodes[0] | |
364 del self._inputBuffers[0] | |
365 del self._state[0] | |
366 del self._error[0] | |
367 del self._responseHandlers[0] | |
368 return buf | |
369 | |
370 | |
371 def _newLine(self, line, check = 1): | |
372 if check and line and line[0] == '.': | |
373 line = line[1:] | |
374 self._inputBuffers[0].append(line) | |
375 | |
376 | |
377 def _setResponseCode(self, code): | |
378 self._responseCodes[0] = code | |
379 | |
380 | |
381 def _getResponseCode(self): | |
382 return self._responseCodes[0] | |
383 | |
384 | |
385 def lineReceived(self, line): | |
386 if not len(self._state): | |
387 self._statePassive(line) | |
388 elif self._getResponseCode() is None: | |
389 code = extractCode(line) | |
390 if code is None or not (200 <= code[0] < 400): # An error! | |
391 self._error[0](line) | |
392 self._endState() | |
393 else: | |
394 self._setResponseCode(code) | |
395 if self._responseHandlers[0]: | |
396 self._responseHandlers[0](code) | |
397 else: | |
398 self._state[0](line) | |
399 | |
400 | |
401 def _statePassive(self, line): | |
402 log.msg('Server said: %s' % line) | |
403 | |
404 | |
405 def _passiveError(self, error): | |
406 log.err('Passive Error: %s' % (error,)) | |
407 | |
408 | |
409 def _headerInitial(self, (code, message)): | |
410 if code == 200: | |
411 self.canPost = 1 | |
412 else: | |
413 self.canPost = 0 | |
414 self._endState() | |
415 | |
416 | |
417 def _stateList(self, line): | |
418 if line != '.': | |
419 data = filter(None, line.strip().split()) | |
420 self._newLine((data[0], int(data[1]), int(data[2]), data[3]), 0) | |
421 else: | |
422 self.gotAllGroups(self._endState()) | |
423 | |
424 | |
425 def _stateOverview(self, line): | |
426 if line != '.': | |
427 self._newLine(filter(None, line.strip().split()), 0) | |
428 else: | |
429 self.gotOverview(self._endState()) | |
430 | |
431 | |
432 def _stateSubscriptions(self, line): | |
433 if line != '.': | |
434 self._newLine(line.strip(), 0) | |
435 else: | |
436 self.gotSubscriptions(self._endState()) | |
437 | |
438 | |
439 def _headerGroup(self, (code, line)): | |
440 self.gotGroup(tuple(line.split())) | |
441 self._endState() | |
442 | |
443 | |
444 def _stateArticle(self, line): | |
445 if line != '.': | |
446 if line.startswith('.'): | |
447 line = line[1:] | |
448 self._newLine(line, 0) | |
449 else: | |
450 self.gotArticle('\n'.join(self._endState())+'\n') | |
451 | |
452 | |
453 def _stateHead(self, line): | |
454 if line != '.': | |
455 self._newLine(line, 0) | |
456 else: | |
457 self.gotHead('\n'.join(self._endState())) | |
458 | |
459 | |
460 def _stateBody(self, line): | |
461 if line != '.': | |
462 if line.startswith('.'): | |
463 line = line[1:] | |
464 self._newLine(line, 0) | |
465 else: | |
466 self.gotBody('\n'.join(self._endState())+'\n') | |
467 | |
468 | |
469 def _headerPost(self, (code, message)): | |
470 if code == 340: | |
471 self.transport.write(self._postText[0].replace('\n', '\r\n').replace
('\r\n.', '\r\n..')) | |
472 if self._postText[0][-1:] != '\n': | |
473 self.sendLine('') | |
474 self.sendLine('.') | |
475 del self._postText[0] | |
476 self._newState(None, self.postFailed, self._headerPosted) | |
477 else: | |
478 self.postFailed('%d %s' % (code, message)) | |
479 self._endState() | |
480 | |
481 | |
482 def _headerPosted(self, (code, message)): | |
483 if code == 240: | |
484 self.postedOk() | |
485 else: | |
486 self.postFailed('%d %s' % (code, message)) | |
487 self._endState() | |
488 | |
489 | |
490 def _stateXHDR(self, line): | |
491 if line != '.': | |
492 self._newLine(line.split(), 0) | |
493 else: | |
494 self._gotXHeader(self._endState()) | |
495 | |
496 | |
497 def _stateNewNews(self, line): | |
498 if line != '.': | |
499 self._newLine(line, 0) | |
500 else: | |
501 self.gotNewNews(self._endState()) | |
502 | |
503 | |
504 def _stateNewGroups(self, line): | |
505 if line != '.': | |
506 self._newLine(line, 0) | |
507 else: | |
508 self.gotNewGroups(self._endState()) | |
509 | |
510 | |
511 def _headerMode(self, (code, message)): | |
512 if code == 203: | |
513 self.setStreamSuccess() | |
514 else: | |
515 self.setStreamFailed((code, message)) | |
516 self._endState() | |
517 | |
518 | |
519 class NNTPServer(basic.LineReceiver): | |
520 COMMANDS = [ | |
521 'LIST', 'GROUP', 'ARTICLE', 'STAT', 'MODE', 'LISTGROUP', 'XOVER', | |
522 'XHDR', 'HEAD', 'BODY', 'NEXT', 'LAST', 'POST', 'QUIT', 'IHAVE', | |
523 'HELP', 'SLAVE', 'XPATH', 'XINDEX', 'XROVER', 'TAKETHIS', 'CHECK' | |
524 ] | |
525 | |
526 def __init__(self): | |
527 self.servingSlave = 0 | |
528 | |
529 | |
530 def connectionMade(self): | |
531 self.inputHandler = None | |
532 self.currentGroup = None | |
533 self.currentIndex = None | |
534 self.sendLine('200 server ready - posting allowed') | |
535 | |
536 def lineReceived(self, line): | |
537 if self.inputHandler is not None: | |
538 self.inputHandler(line) | |
539 else: | |
540 parts = line.strip().split() | |
541 if len(parts): | |
542 cmd, parts = parts[0].upper(), parts[1:] | |
543 if cmd in NNTPServer.COMMANDS: | |
544 func = getattr(self, 'do_%s' % cmd) | |
545 try: | |
546 func(*parts) | |
547 except TypeError: | |
548 self.sendLine('501 command syntax error') | |
549 log.msg("501 command syntax error") | |
550 log.msg("command was", line) | |
551 log.deferr() | |
552 except: | |
553 self.sendLine('503 program fault - command not performed
') | |
554 log.msg("503 program fault") | |
555 log.msg("command was", line) | |
556 log.deferr() | |
557 else: | |
558 self.sendLine('500 command not recognized') | |
559 | |
560 | |
561 def do_LIST(self, subcmd = '', *dummy): | |
562 subcmd = subcmd.strip().lower() | |
563 if subcmd == 'newsgroups': | |
564 # XXX - this could use a real implementation, eh? | |
565 self.sendLine('215 Descriptions in form "group description"') | |
566 self.sendLine('.') | |
567 elif subcmd == 'overview.fmt': | |
568 defer = self.factory.backend.overviewRequest() | |
569 defer.addCallbacks(self._gotOverview, self._errOverview) | |
570 log.msg('overview') | |
571 elif subcmd == 'subscriptions': | |
572 defer = self.factory.backend.subscriptionRequest() | |
573 defer.addCallbacks(self._gotSubscription, self._errSubscription) | |
574 log.msg('subscriptions') | |
575 elif subcmd == '': | |
576 defer = self.factory.backend.listRequest() | |
577 defer.addCallbacks(self._gotList, self._errList) | |
578 else: | |
579 self.sendLine('500 command not recognized') | |
580 | |
581 | |
582 def _gotList(self, list): | |
583 self.sendLine('215 newsgroups in form "group high low flags"') | |
584 for i in list: | |
585 self.sendLine('%s %d %d %s' % tuple(i)) | |
586 self.sendLine('.') | |
587 | |
588 | |
589 def _errList(self, failure): | |
590 print 'LIST failed: ', failure | |
591 self.sendLine('503 program fault - command not performed') | |
592 | |
593 | |
594 def _gotSubscription(self, parts): | |
595 self.sendLine('215 information follows') | |
596 for i in parts: | |
597 self.sendLine(i) | |
598 self.sendLine('.') | |
599 | |
600 | |
601 def _errSubscription(self, failure): | |
602 print 'SUBSCRIPTIONS failed: ', failure | |
603 self.sendLine('503 program fault - comand not performed') | |
604 | |
605 | |
606 def _gotOverview(self, parts): | |
607 self.sendLine('215 Order of fields in overview database.') | |
608 for i in parts: | |
609 self.sendLine(i + ':') | |
610 self.sendLine('.') | |
611 | |
612 | |
613 def _errOverview(self, failure): | |
614 print 'LIST OVERVIEW.FMT failed: ', failure | |
615 self.sendLine('503 program fault - command not performed') | |
616 | |
617 | |
618 def do_LISTGROUP(self, group = None): | |
619 group = group or self.currentGroup | |
620 if group is None: | |
621 self.sendLine('412 Not currently in newsgroup') | |
622 else: | |
623 defer = self.factory.backend.listGroupRequest(group) | |
624 defer.addCallbacks(self._gotListGroup, self._errListGroup) | |
625 | |
626 | |
627 def _gotListGroup(self, (group, articles)): | |
628 self.currentGroup = group | |
629 if len(articles): | |
630 self.currentIndex = int(articles[0]) | |
631 else: | |
632 self.currentIndex = None | |
633 | |
634 self.sendLine('211 list of article numbers follow') | |
635 for i in articles: | |
636 self.sendLine(str(i)) | |
637 self.sendLine('.') | |
638 | |
639 | |
640 def _errListGroup(self, failure): | |
641 print 'LISTGROUP failed: ', failure | |
642 self.sendLine('502 no permission') | |
643 | |
644 | |
645 def do_XOVER(self, range): | |
646 if self.currentGroup is None: | |
647 self.sendLine('412 No news group currently selected') | |
648 else: | |
649 l, h = parseRange(range) | |
650 defer = self.factory.backend.xoverRequest(self.currentGroup, l, h) | |
651 defer.addCallbacks(self._gotXOver, self._errXOver) | |
652 | |
653 | |
654 def _gotXOver(self, parts): | |
655 self.sendLine('224 Overview information follows') | |
656 for i in parts: | |
657 self.sendLine('\t'.join(map(str, i))) | |
658 self.sendLine('.') | |
659 | |
660 | |
661 def _errXOver(self, failure): | |
662 print 'XOVER failed: ', failure | |
663 self.sendLine('420 No article(s) selected') | |
664 | |
665 | |
666 def xhdrWork(self, header, range): | |
667 if self.currentGroup is None: | |
668 self.sendLine('412 No news group currently selected') | |
669 else: | |
670 if range is None: | |
671 if self.currentIndex is None: | |
672 self.sendLine('420 No current article selected') | |
673 return | |
674 else: | |
675 l = h = self.currentIndex | |
676 else: | |
677 # FIXME: articles may be a message-id | |
678 l, h = parseRange(range) | |
679 | |
680 if l is h is None: | |
681 self.sendLine('430 no such article') | |
682 else: | |
683 return self.factory.backend.xhdrRequest(self.currentGroup, l, h,
header) | |
684 | |
685 | |
686 def do_XHDR(self, header, range = None): | |
687 d = self.xhdrWork(header, range) | |
688 if d: | |
689 d.addCallbacks(self._gotXHDR, self._errXHDR) | |
690 | |
691 | |
692 def _gotXHDR(self, parts): | |
693 self.sendLine('221 Header follows') | |
694 for i in parts: | |
695 self.sendLine('%d %s' % i) | |
696 self.sendLine('.') | |
697 | |
698 def _errXHDR(self, failure): | |
699 print 'XHDR failed: ', failure | |
700 self.sendLine('502 no permission') | |
701 | |
702 | |
703 def do_XROVER(self, header, range = None): | |
704 d = self.xhdrWork(header, range) | |
705 if d: | |
706 d.addCallbacks(self._gotXROVER, self._errXROVER) | |
707 | |
708 | |
709 def _gotXROVER(self, parts): | |
710 self.sendLine('224 Overview information follows') | |
711 for i in parts: | |
712 self.sendLine('%d %s' % i) | |
713 self.sendLine('.') | |
714 | |
715 | |
716 def _errXROVER(self, failure): | |
717 print 'XROVER failed: ', | |
718 self._errXHDR(failure) | |
719 | |
720 | |
721 def do_POST(self): | |
722 self.inputHandler = self._doingPost | |
723 self.message = '' | |
724 self.sendLine('340 send article to be posted. End with <CR-LF>.<CR-LF>'
) | |
725 | |
726 | |
727 def _doingPost(self, line): | |
728 if line == '.': | |
729 self.inputHandler = None | |
730 group, article = self.currentGroup, self.message | |
731 self.message = '' | |
732 | |
733 defer = self.factory.backend.postRequest(article) | |
734 defer.addCallbacks(self._gotPost, self._errPost) | |
735 else: | |
736 self.message = self.message + line + '\r\n' | |
737 | |
738 | |
739 def _gotPost(self, parts): | |
740 self.sendLine('240 article posted ok') | |
741 | |
742 | |
743 def _errPost(self, failure): | |
744 print 'POST failed: ', failure | |
745 self.sendLine('441 posting failed') | |
746 | |
747 | |
748 def do_CHECK(self, id): | |
749 d = self.factory.backend.articleExistsRequest(id) | |
750 d.addCallbacks(self._gotCheck, self._errCheck) | |
751 | |
752 | |
753 def _gotCheck(self, result): | |
754 if result: | |
755 self.sendLine("438 already have it, please don't send it to me") | |
756 else: | |
757 self.sendLine('238 no such article found, please send it to me') | |
758 | |
759 | |
760 def _errCheck(self, failure): | |
761 print 'CHECK failed: ', failure | |
762 self.sendLine('431 try sending it again later') | |
763 | |
764 | |
765 def do_TAKETHIS(self, id): | |
766 self.inputHandler = self._doingTakeThis | |
767 self.message = '' | |
768 | |
769 | |
770 def _doingTakeThis(self, line): | |
771 if line == '.': | |
772 self.inputHandler = None | |
773 article = self.message | |
774 self.message = '' | |
775 d = self.factory.backend.postRequest(article) | |
776 d.addCallbacks(self._didTakeThis, self._errTakeThis) | |
777 else: | |
778 self.message = self.message + line + '\r\n' | |
779 | |
780 | |
781 def _didTakeThis(self, result): | |
782 self.sendLine('239 article transferred ok') | |
783 | |
784 | |
785 def _errTakeThis(self, failure): | |
786 print 'TAKETHIS failed: ', failure | |
787 self.sendLine('439 article transfer failed') | |
788 | |
789 | |
790 def do_GROUP(self, group): | |
791 defer = self.factory.backend.groupRequest(group) | |
792 defer.addCallbacks(self._gotGroup, self._errGroup) | |
793 | |
794 | |
795 def _gotGroup(self, (name, num, high, low, flags)): | |
796 self.currentGroup = name | |
797 self.currentIndex = low | |
798 self.sendLine('211 %d %d %d %s group selected' % (num, low, high, name)) | |
799 | |
800 | |
801 def _errGroup(self, failure): | |
802 print 'GROUP failed: ', failure | |
803 self.sendLine('411 no such group') | |
804 | |
805 | |
806 def articleWork(self, article, cmd, func): | |
807 if self.currentGroup is None: | |
808 self.sendLine('412 no newsgroup has been selected') | |
809 else: | |
810 if not article: | |
811 if self.currentIndex is None: | |
812 self.sendLine('420 no current article has been selected') | |
813 else: | |
814 article = self.currentIndex | |
815 else: | |
816 if article[0] == '<': | |
817 return func(self.currentGroup, index = None, id = article) | |
818 else: | |
819 try: | |
820 article = int(article) | |
821 return func(self.currentGroup, article) | |
822 except ValueError, e: | |
823 self.sendLine('501 command syntax error') | |
824 | |
825 | |
826 def do_ARTICLE(self, article = None): | |
827 defer = self.articleWork(article, 'ARTICLE', self.factory.backend.articl
eRequest) | |
828 if defer: | |
829 defer.addCallbacks(self._gotArticle, self._errArticle) | |
830 | |
831 | |
832 def _gotArticle(self, (index, id, article)): | |
833 if isinstance(article, types.StringType): | |
834 import warnings | |
835 warnings.warn( | |
836 "Returning the article as a string from `articleRequest' " | |
837 "is deprecated. Return a file-like object instead." | |
838 ) | |
839 article = StringIO.StringIO(article) | |
840 self.currentIndex = index | |
841 self.sendLine('220 %d %s article' % (index, id)) | |
842 s = basic.FileSender() | |
843 d = s.beginFileTransfer(article, self.transport) | |
844 d.addCallback(self.finishedFileTransfer) | |
845 | |
846 ## | |
847 ## Helper for FileSender | |
848 ## | |
849 def finishedFileTransfer(self, lastsent): | |
850 if lastsent != '\n': | |
851 line = '\r\n.' | |
852 else: | |
853 line = '.' | |
854 self.sendLine(line) | |
855 ## | |
856 | |
857 def _errArticle(self, failure): | |
858 print 'ARTICLE failed: ', failure | |
859 self.sendLine('423 bad article number') | |
860 | |
861 | |
862 def do_STAT(self, article = None): | |
863 defer = self.articleWork(article, 'STAT', self.factory.backend.articleRe
quest) | |
864 if defer: | |
865 defer.addCallbacks(self._gotStat, self._errStat) | |
866 | |
867 | |
868 def _gotStat(self, (index, id, article)): | |
869 self.currentIndex = index | |
870 self.sendLine('223 %d %s article retreived - request text separately' %
(index, id)) | |
871 | |
872 | |
873 def _errStat(self, failure): | |
874 print 'STAT failed: ', failure | |
875 self.sendLine('423 bad article number') | |
876 | |
877 | |
878 def do_HEAD(self, article = None): | |
879 defer = self.articleWork(article, 'HEAD', self.factory.backend.headReque
st) | |
880 if defer: | |
881 defer.addCallbacks(self._gotHead, self._errHead) | |
882 | |
883 | |
884 def _gotHead(self, (index, id, head)): | |
885 self.currentIndex = index | |
886 self.sendLine('221 %d %s article retrieved' % (index, id)) | |
887 self.transport.write(head + '\r\n') | |
888 self.sendLine('.') | |
889 | |
890 | |
891 def _errHead(self, failure): | |
892 print 'HEAD failed: ', failure | |
893 self.sendLine('423 no such article number in this group') | |
894 | |
895 | |
896 def do_BODY(self, article): | |
897 defer = self.articleWork(article, 'BODY', self.factory.backend.bodyReque
st) | |
898 if defer: | |
899 defer.addCallbacks(self._gotBody, self._errBody) | |
900 | |
901 | |
902 def _gotBody(self, (index, id, body)): | |
903 if isinstance(body, types.StringType): | |
904 import warnings | |
905 warnings.warn( | |
906 "Returning the article as a string from `articleRequest' " | |
907 "is deprecated. Return a file-like object instead." | |
908 ) | |
909 body = StringIO.StringIO(body) | |
910 self.currentIndex = index | |
911 self.sendLine('221 %d %s article retrieved' % (index, id)) | |
912 self.lastsent = '' | |
913 s = basic.FileSender() | |
914 d = s.beginFileTransfer(body, self.transport) | |
915 d.addCallback(self.finishedFileTransfer) | |
916 | |
917 def _errBody(self, failure): | |
918 print 'BODY failed: ', failure | |
919 self.sendLine('423 no such article number in this group') | |
920 | |
921 | |
922 # NEXT and LAST are just STATs that increment currentIndex first. | |
923 # Accordingly, use the STAT callbacks. | |
924 def do_NEXT(self): | |
925 i = self.currentIndex + 1 | |
926 defer = self.factory.backend.articleRequest(self.currentGroup, i) | |
927 defer.addCallbacks(self._gotStat, self._errStat) | |
928 | |
929 | |
930 def do_LAST(self): | |
931 i = self.currentIndex - 1 | |
932 defer = self.factory.backend.articleRequest(self.currentGroup, i) | |
933 defer.addCallbacks(self._gotStat, self._errStat) | |
934 | |
935 | |
936 def do_MODE(self, cmd): | |
937 cmd = cmd.strip().upper() | |
938 if cmd == 'READER': | |
939 self.servingSlave = 0 | |
940 self.sendLine('200 Hello, you can post') | |
941 elif cmd == 'STREAM': | |
942 self.sendLine('500 Command not understood') | |
943 else: | |
944 # This is not a mistake | |
945 self.sendLine('500 Command not understood') | |
946 | |
947 | |
948 def do_QUIT(self): | |
949 self.sendLine('205 goodbye') | |
950 self.transport.loseConnection() | |
951 | |
952 | |
953 def do_HELP(self): | |
954 self.sendLine('100 help text follows') | |
955 self.sendLine('Read the RFC.') | |
956 self.sendLine('.') | |
957 | |
958 | |
959 def do_SLAVE(self): | |
960 self.sendLine('202 slave status noted') | |
961 self.servingeSlave = 1 | |
962 | |
963 | |
964 def do_XPATH(self, article): | |
965 # XPATH is a silly thing to have. No client has the right to ask | |
966 # for this piece of information from me, and so that is what I'll | |
967 # tell them. | |
968 self.sendLine('502 access restriction or permission denied') | |
969 | |
970 | |
971 def do_XINDEX(self, article): | |
972 # XINDEX is another silly command. The RFC suggests it be relegated | |
973 # to the history books, and who am I to disagree? | |
974 self.sendLine('502 access restriction or permission denied') | |
975 | |
976 | |
977 def do_XROVER(self, range = None): | |
978 self.do_XHDR(self, 'References', range) | |
979 | |
980 | |
981 def do_IHAVE(self, id): | |
982 self.factory.backend.articleExistsRequest(id).addCallback(self._foundArt
icle) | |
983 | |
984 | |
985 def _foundArticle(self, result): | |
986 if result: | |
987 self.sendLine('437 article rejected - do not try again') | |
988 else: | |
989 self.sendLine('335 send article to be transferred. End with <CR-LF>
.<CR-LF>') | |
990 self.inputHandler = self._handleIHAVE | |
991 self.message = '' | |
992 | |
993 | |
994 def _handleIHAVE(self, line): | |
995 if line == '.': | |
996 self.inputHandler = None | |
997 self.factory.backend.postRequest( | |
998 self.message | |
999 ).addCallbacks(self._gotIHAVE, self._errIHAVE) | |
1000 | |
1001 self.message = '' | |
1002 else: | |
1003 self.message = self.message + line + '\r\n' | |
1004 | |
1005 | |
1006 def _gotIHAVE(self, result): | |
1007 self.sendLine('235 article transferred ok') | |
1008 | |
1009 | |
1010 def _errIHAVE(self, failure): | |
1011 print 'IHAVE failed: ', failure | |
1012 self.sendLine('436 transfer failed - try again later') | |
1013 | |
1014 | |
1015 class UsenetClientProtocol(NNTPClient): | |
1016 """ | |
1017 A client that connects to an NNTP server and asks for articles new | |
1018 since a certain time. | |
1019 """ | |
1020 | |
1021 def __init__(self, groups, date, storage): | |
1022 """ | |
1023 Fetch all new articles from the given groups since the | |
1024 given date and dump them into the given storage. groups | |
1025 is a list of group names. date is an integer or floating | |
1026 point representing seconds since the epoch (GMT). storage is | |
1027 any object that implements the NewsStorage interface. | |
1028 """ | |
1029 NNTPClient.__init__(self) | |
1030 self.groups, self.date, self.storage = groups, date, storage | |
1031 | |
1032 | |
1033 def connectionMade(self): | |
1034 NNTPClient.connectionMade(self) | |
1035 log.msg("Initiating update with remote host: " + str(self.transport.getP
eer())) | |
1036 self.setStream() | |
1037 self.fetchNewNews(self.groups, self.date, '') | |
1038 | |
1039 | |
1040 def articleExists(self, exists, article): | |
1041 if exists: | |
1042 self.fetchArticle(article) | |
1043 else: | |
1044 self.count = self.count - 1 | |
1045 self.disregard = self.disregard + 1 | |
1046 | |
1047 | |
1048 def gotNewNews(self, news): | |
1049 self.disregard = 0 | |
1050 self.count = len(news) | |
1051 log.msg("Transfering " + str(self.count) + " articles from remote host:
" + str(self.transport.getPeer())) | |
1052 for i in news: | |
1053 self.storage.articleExistsRequest(i).addCallback(self.articleExists,
i) | |
1054 | |
1055 | |
1056 def getNewNewsFailed(self, reason): | |
1057 log.msg("Updated failed (" + reason + ") with remote host: " + str(self.
transport.getPeer())) | |
1058 self.quit() | |
1059 | |
1060 | |
1061 def gotArticle(self, article): | |
1062 self.storage.postRequest(article) | |
1063 self.count = self.count - 1 | |
1064 if not self.count: | |
1065 log.msg("Completed update with remote host: " + str(self.transport.g
etPeer())) | |
1066 if self.disregard: | |
1067 log.msg("Disregarded %d articles." % (self.disregard,)) | |
1068 self.factory.updateChecks(self.transport.getPeer()) | |
1069 self.quit() | |
OLD | NEW |