| 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 |