123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050 |
- # -*- test-case-name: twisted.news.test.test_nntp -*-
- # Copyright (c) Twisted Matrix Laboratories.
- # See LICENSE for details.
- """
- NNTP protocol support.
- The following protocol commands are currently understood::
- LIST LISTGROUP XOVER XHDR
- POST GROUP ARTICLE STAT HEAD
- BODY NEXT MODE STREAM MODE READER SLAVE
- LAST QUIT HELP IHAVE XPATH
- XINDEX XROVER TAKETHIS CHECK
- The following protocol commands require implementation::
- NEWNEWS
- XGTITLE XPAT
- XTHREAD AUTHINFO NEWGROUPS
- Other desired features:
- - A real backend
- - More robust client input handling
- - A control protocol
- """
- from __future__ import print_function
- import time
- from twisted.protocols import basic
- from twisted.python import log
- def parseRange(text):
- articles = text.split('-')
- if len(articles) == 1:
- try:
- a = int(articles[0])
- return a, a
- except ValueError:
- return None, None
- elif len(articles) == 2:
- try:
- if len(articles[0]):
- l = int(articles[0])
- else:
- l = None
- if len(articles[1]):
- h = int(articles[1])
- else:
- h = None
- except ValueError:
- return None, None
- return l, h
- def extractCode(line):
- line = line.split(' ', 1)
- if len(line) != 2:
- return None
- try:
- return int(line[0]), line[1]
- except ValueError:
- return None
- class NNTPError(Exception):
- def __init__(self, string):
- self.string = string
- def __str__(self):
- return 'NNTPError: %s' % self.string
- class NNTPClient(basic.LineReceiver):
- MAX_COMMAND_LENGTH = 510
- def __init__(self):
- self.currentGroup = None
- self._state = []
- self._error = []
- self._inputBuffers = []
- self._responseCodes = []
- self._responseHandlers = []
- self._postText = []
- self._newState(self._statePassive, None, self._headerInitial)
- def gotAllGroups(self, groups):
- "Override for notification when fetchGroups() action is completed"
- def getAllGroupsFailed(self, error):
- "Override for notification when fetchGroups() action fails"
- def gotOverview(self, overview):
- "Override for notification when fetchOverview() action is completed"
- def getOverviewFailed(self, error):
- "Override for notification when fetchOverview() action fails"
- def gotSubscriptions(self, subscriptions):
- "Override for notification when fetchSubscriptions() action is completed"
- def getSubscriptionsFailed(self, error):
- "Override for notification when fetchSubscriptions() action fails"
- def gotGroup(self, group):
- "Override for notification when fetchGroup() action is completed"
- def getGroupFailed(self, error):
- "Override for notification when fetchGroup() action fails"
- def gotArticle(self, article):
- "Override for notification when fetchArticle() action is completed"
- def getArticleFailed(self, error):
- "Override for notification when fetchArticle() action fails"
- def gotHead(self, head):
- "Override for notification when fetchHead() action is completed"
- def getHeadFailed(self, error):
- "Override for notification when fetchHead() action fails"
- def gotBody(self, info):
- "Override for notification when fetchBody() action is completed"
- def getBodyFailed(self, body):
- "Override for notification when fetchBody() action fails"
- def postedOk(self):
- "Override for notification when postArticle() action is successful"
- def postFailed(self, error):
- "Override for notification when postArticle() action fails"
- def gotXHeader(self, headers):
- "Override for notification when getXHeader() action is successful"
- def getXHeaderFailed(self, error):
- "Override for notification when getXHeader() action fails"
- def gotNewNews(self, news):
- "Override for notification when getNewNews() action is successful"
- def getNewNewsFailed(self, error):
- "Override for notification when getNewNews() action fails"
- def gotNewGroups(self, groups):
- "Override for notification when getNewGroups() action is successful"
- def getNewGroupsFailed(self, error):
- "Override for notification when getNewGroups() action fails"
- def setStreamSuccess(self):
- "Override for notification when setStream() action is successful"
- def setStreamFailed(self, error):
- "Override for notification when setStream() action fails"
- def fetchGroups(self):
- """
- Request a list of all news groups from the server. gotAllGroups()
- is called on success, getGroupsFailed() on failure
- """
- self.sendLine('LIST')
- self._newState(self._stateList, self.getAllGroupsFailed)
- def fetchOverview(self):
- """
- Request the overview format from the server. gotOverview() is called
- on success, getOverviewFailed() on failure
- """
- self.sendLine('LIST OVERVIEW.FMT')
- self._newState(self._stateOverview, self.getOverviewFailed)
- def fetchSubscriptions(self):
- """
- Request a list of the groups it is recommended a new user subscribe to.
- gotSubscriptions() is called on success, getSubscriptionsFailed() on
- failure
- """
- self.sendLine('LIST SUBSCRIPTIONS')
- self._newState(self._stateSubscriptions, self.getSubscriptionsFailed)
- def fetchGroup(self, group):
- """
- Get group information for the specified group from the server. gotGroup()
- is called on success, getGroupFailed() on failure.
- """
- self.sendLine('GROUP %s' % (group,))
- self._newState(None, self.getGroupFailed, self._headerGroup)
- def fetchHead(self, index = ''):
- """
- Get the header for the specified article (or the currently selected
- article if index is '') from the server. gotHead() is called on
- success, getHeadFailed() on failure
- """
- self.sendLine('HEAD %s' % (index,))
- self._newState(self._stateHead, self.getHeadFailed)
- def fetchBody(self, index = ''):
- """
- Get the body for the specified article (or the currently selected
- article if index is '') from the server. gotBody() is called on
- success, getBodyFailed() on failure
- """
- self.sendLine('BODY %s' % (index,))
- self._newState(self._stateBody, self.getBodyFailed)
- def fetchArticle(self, index = ''):
- """
- Get the complete article with the specified index (or the currently
- selected article if index is '') or Message-ID from the server.
- gotArticle() is called on success, getArticleFailed() on failure.
- """
- self.sendLine('ARTICLE %s' % (index,))
- self._newState(self._stateArticle, self.getArticleFailed)
- def postArticle(self, text):
- """
- Attempt to post an article with the specified text to the server. 'text'
- must consist of both head and body data, as specified by RFC 850. If the
- article is posted successfully, postedOk() is called, otherwise postFailed()
- is called.
- """
- self.sendLine('POST')
- self._newState(None, self.postFailed, self._headerPost)
- self._postText.append(text)
- def fetchNewNews(self, groups, date, distributions = ''):
- """
- Get the Message-IDs for all new news posted to any of the given
- groups since the specified date - in seconds since the epoch, GMT -
- optionally restricted to the given distributions. gotNewNews() is
- called on success, getNewNewsFailed() on failure.
- One invocation of this function may result in multiple invocations
- of gotNewNews()/getNewNewsFailed().
- """
- date, timeStr = time.strftime('%y%m%d %H%M%S', time.gmtime(date)).split()
- line = 'NEWNEWS %%s %s %s %s' % (date, timeStr, distributions)
- groupPart = ''
- while len(groups) and len(line) + len(groupPart) + len(groups[-1]) + 1 < NNTPClient.MAX_COMMAND_LENGTH:
- group = groups.pop()
- groupPart = groupPart + ',' + group
- self.sendLine(line % (groupPart,))
- self._newState(self._stateNewNews, self.getNewNewsFailed)
- if len(groups):
- self.fetchNewNews(groups, date, distributions)
- def fetchNewGroups(self, date, distributions):
- """
- Get the names of all new groups created/added to the server since
- the specified date - in seconds since the ecpoh, GMT - optionally
- restricted to the given distributions. gotNewGroups() is called
- on success, getNewGroupsFailed() on failure.
- """
- date, timeStr = time.strftime('%y%m%d %H%M%S', time.gmtime(date)).split()
- self.sendLine('NEWGROUPS %s %s %s' % (date, timeStr, distributions))
- self._newState(self._stateNewGroups, self.getNewGroupsFailed)
- def fetchXHeader(self, header, low = None, high = None, id = None):
- """
- Request a specific header from the server for an article or range
- of articles. If 'id' is not None, a header for only the article
- with that Message-ID will be requested. If both low and high are
- None, a header for the currently selected article will be selected;
- If both low and high are zero-length strings, headers for all articles
- in the currently selected group will be requested; Otherwise, high
- and low will be used as bounds - if one is None the first or last
- article index will be substituted, as appropriate.
- """
- if id is not None:
- r = header + ' <%s>' % (id,)
- elif low is high is None:
- r = header
- elif high is None:
- r = header + ' %d-' % (low,)
- elif low is None:
- r = header + ' -%d' % (high,)
- else:
- r = header + ' %d-%d' % (low, high)
- self.sendLine('XHDR ' + r)
- self._newState(self._stateXHDR, self.getXHeaderFailed)
- def setStream(self):
- """
- Set the mode to STREAM, suspending the normal "lock-step" mode of
- communications. setStreamSuccess() is called on success,
- setStreamFailed() on failure.
- """
- self.sendLine('MODE STREAM')
- self._newState(None, self.setStreamFailed, self._headerMode)
- def quit(self):
- self.sendLine('QUIT')
- self.transport.loseConnection()
- def _newState(self, method, error, responseHandler = None):
- self._inputBuffers.append([])
- self._responseCodes.append(None)
- self._state.append(method)
- self._error.append(error)
- self._responseHandlers.append(responseHandler)
- def _endState(self):
- buf = self._inputBuffers[0]
- del self._responseCodes[0]
- del self._inputBuffers[0]
- del self._state[0]
- del self._error[0]
- del self._responseHandlers[0]
- return buf
- def _newLine(self, line, check = 1):
- if check and line and line[0] == '.':
- line = line[1:]
- self._inputBuffers[0].append(line)
- def _setResponseCode(self, code):
- self._responseCodes[0] = code
- def _getResponseCode(self):
- return self._responseCodes[0]
- def lineReceived(self, line):
- if not len(self._state):
- self._statePassive(line)
- elif self._getResponseCode() is None:
- code = extractCode(line)
- if code is None or not (200 <= code[0] < 400): # An error!
- self._error[0](line)
- self._endState()
- else:
- self._setResponseCode(code)
- if self._responseHandlers[0]:
- self._responseHandlers[0](code)
- else:
- self._state[0](line)
- def _statePassive(self, line):
- log.msg('Server said: %s' % line)
- def _passiveError(self, error):
- log.err('Passive Error: %s' % (error,))
- def _headerInitial(self, response):
- (code, message) = response
- if code == 200:
- self.canPost = 1
- else:
- self.canPost = 0
- self._endState()
- def _stateList(self, line):
- if line != '.':
- data = filter(None, line.strip().split())
- self._newLine((data[0], int(data[1]), int(data[2]), data[3]), 0)
- else:
- self.gotAllGroups(self._endState())
- def _stateOverview(self, line):
- if line != '.':
- self._newLine(filter(None, line.strip().split()), 0)
- else:
- self.gotOverview(self._endState())
- def _stateSubscriptions(self, line):
- if line != '.':
- self._newLine(line.strip(), 0)
- else:
- self.gotSubscriptions(self._endState())
- def _headerGroup(self, response):
- (code, line) = response
- self.gotGroup(tuple(line.split()))
- self._endState()
- def _stateArticle(self, line):
- if line != '.':
- if line.startswith('.'):
- line = line[1:]
- self._newLine(line, 0)
- else:
- self.gotArticle('\n'.join(self._endState())+'\n')
- def _stateHead(self, line):
- if line != '.':
- self._newLine(line, 0)
- else:
- self.gotHead('\n'.join(self._endState()))
- def _stateBody(self, line):
- if line != '.':
- if line.startswith('.'):
- line = line[1:]
- self._newLine(line, 0)
- else:
- self.gotBody('\n'.join(self._endState())+'\n')
- def _headerPost(self, response):
- (code, message) = response
- if code == 340:
- self.transport.write(self._postText[0].replace('\n', '\r\n').replace('\r\n.', '\r\n..'))
- if self._postText[0][-1:] != '\n':
- self.sendLine('')
- self.sendLine('.')
- del self._postText[0]
- self._newState(None, self.postFailed, self._headerPosted)
- else:
- self.postFailed('%d %s' % (code, message))
- self._endState()
- def _headerPosted(self, response):
- (code, message) = response
- if code == 240:
- self.postedOk()
- else:
- self.postFailed('%d %s' % (code, message))
- self._endState()
- def _stateXHDR(self, line):
- if line != '.':
- self._newLine(line.split(), 0)
- else:
- self._gotXHeader(self._endState())
- def _stateNewNews(self, line):
- if line != '.':
- self._newLine(line, 0)
- else:
- self.gotNewNews(self._endState())
- def _stateNewGroups(self, line):
- if line != '.':
- self._newLine(line, 0)
- else:
- self.gotNewGroups(self._endState())
- def _headerMode(self, response):
- (code, message) = response
- if code == 203:
- self.setStreamSuccess()
- else:
- self.setStreamFailed((code, message))
- self._endState()
- class NNTPServer(basic.LineReceiver):
- COMMANDS = [
- 'LIST', 'GROUP', 'ARTICLE', 'STAT', 'MODE', 'LISTGROUP', 'XOVER',
- 'XHDR', 'HEAD', 'BODY', 'NEXT', 'LAST', 'POST', 'QUIT', 'IHAVE',
- 'HELP', 'SLAVE', 'XPATH', 'XINDEX', 'XROVER', 'TAKETHIS', 'CHECK'
- ]
- def __init__(self):
- self.servingSlave = 0
- def connectionMade(self):
- self.inputHandler = None
- self.currentGroup = None
- self.currentIndex = None
- self.sendLine('200 server ready - posting allowed')
- def lineReceived(self, line):
- if self.inputHandler is not None:
- self.inputHandler(line)
- else:
- parts = line.strip().split()
- if len(parts):
- cmd, parts = parts[0].upper(), parts[1:]
- if cmd in NNTPServer.COMMANDS:
- func = getattr(self, 'do_%s' % cmd)
- try:
- func(*parts)
- except TypeError:
- self.sendLine('501 command syntax error')
- log.msg("501 command syntax error")
- log.msg("command was", line)
- log.deferr()
- except:
- self.sendLine('503 program fault - command not performed')
- log.msg("503 program fault")
- log.msg("command was", line)
- log.deferr()
- else:
- self.sendLine('500 command not recognized')
- def do_LIST(self, subcmd = '', *dummy):
- subcmd = subcmd.strip().lower()
- if subcmd == 'newsgroups':
- # XXX - this could use a real implementation, eh?
- self.sendLine('215 Descriptions in form "group description"')
- self.sendLine('.')
- elif subcmd == 'overview.fmt':
- defer = self.factory.backend.overviewRequest()
- defer.addCallbacks(self._gotOverview, self._errOverview)
- log.msg('overview')
- elif subcmd == 'subscriptions':
- defer = self.factory.backend.subscriptionRequest()
- defer.addCallbacks(self._gotSubscription, self._errSubscription)
- log.msg('subscriptions')
- elif subcmd == '':
- defer = self.factory.backend.listRequest()
- defer.addCallbacks(self._gotList, self._errList)
- else:
- self.sendLine('500 command not recognized')
- def _gotList(self, list):
- self.sendLine('215 newsgroups in form "group high low flags"')
- for i in list:
- self.sendLine('%s %d %d %s' % tuple(i))
- self.sendLine('.')
- def _errList(self, failure):
- print('LIST failed: ', failure)
- self.sendLine('503 program fault - command not performed')
- def _gotSubscription(self, parts):
- self.sendLine('215 information follows')
- for i in parts:
- self.sendLine(i)
- self.sendLine('.')
- def _errSubscription(self, failure):
- print('SUBSCRIPTIONS failed: ', failure)
- self.sendLine('503 program fault - comand not performed')
- def _gotOverview(self, parts):
- self.sendLine('215 Order of fields in overview database.')
- for i in parts:
- self.sendLine(i + ':')
- self.sendLine('.')
- def _errOverview(self, failure):
- print('LIST OVERVIEW.FMT failed: ', failure)
- self.sendLine('503 program fault - command not performed')
- def do_LISTGROUP(self, group = None):
- group = group or self.currentGroup
- if group is None:
- self.sendLine('412 Not currently in newsgroup')
- else:
- defer = self.factory.backend.listGroupRequest(group)
- defer.addCallbacks(self._gotListGroup, self._errListGroup)
- def _gotListGroup(self, result):
- (group, articles) = result
- self.currentGroup = group
- if len(articles):
- self.currentIndex = int(articles[0])
- else:
- self.currentIndex = None
- self.sendLine('211 list of article numbers follow')
- for i in articles:
- self.sendLine(str(i))
- self.sendLine('.')
- def _errListGroup(self, failure):
- print('LISTGROUP failed: ', failure)
- self.sendLine('502 no permission')
- def do_XOVER(self, range):
- if self.currentGroup is None:
- self.sendLine('412 No news group currently selected')
- else:
- l, h = parseRange(range)
- defer = self.factory.backend.xoverRequest(self.currentGroup, l, h)
- defer.addCallbacks(self._gotXOver, self._errXOver)
- def _gotXOver(self, parts):
- self.sendLine('224 Overview information follows')
- for i in parts:
- self.sendLine('\t'.join(map(str, i)))
- self.sendLine('.')
- def _errXOver(self, failure):
- print('XOVER failed: ', failure)
- self.sendLine('420 No article(s) selected')
- def xhdrWork(self, header, range):
- if self.currentGroup is None:
- self.sendLine('412 No news group currently selected')
- else:
- if range is None:
- if self.currentIndex is None:
- self.sendLine('420 No current article selected')
- return
- else:
- l = h = self.currentIndex
- else:
- # FIXME: articles may be a message-id
- l, h = parseRange(range)
- if l is h is None:
- self.sendLine('430 no such article')
- else:
- return self.factory.backend.xhdrRequest(self.currentGroup, l, h, header)
- def do_XHDR(self, header, range = None):
- d = self.xhdrWork(header, range)
- if d:
- d.addCallbacks(self._gotXHDR, self._errXHDR)
- def _gotXHDR(self, parts):
- self.sendLine('221 Header follows')
- for i in parts:
- self.sendLine('%d %s' % i)
- self.sendLine('.')
- def _errXHDR(self, failure):
- print('XHDR failed: ', failure)
- self.sendLine('502 no permission')
- def do_POST(self):
- self.inputHandler = self._doingPost
- self.message = ''
- self.sendLine('340 send article to be posted. End with <CR-LF>.<CR-LF>')
- def _doingPost(self, line):
- if line == '.':
- self.inputHandler = None
- article = self.message
- self.message = ''
- defer = self.factory.backend.postRequest(article)
- defer.addCallbacks(self._gotPost, self._errPost)
- else:
- self.message = self.message + line + '\r\n'
- def _gotPost(self, parts):
- self.sendLine('240 article posted ok')
- def _errPost(self, failure):
- print('POST failed: ', failure)
- self.sendLine('441 posting failed')
- def do_CHECK(self, id):
- d = self.factory.backend.articleExistsRequest(id)
- d.addCallbacks(self._gotCheck, self._errCheck)
- def _gotCheck(self, result):
- if result:
- self.sendLine("438 already have it, please don't send it to me")
- else:
- self.sendLine('238 no such article found, please send it to me')
- def _errCheck(self, failure):
- print('CHECK failed: ', failure)
- self.sendLine('431 try sending it again later')
- def do_TAKETHIS(self, id):
- self.inputHandler = self._doingTakeThis
- self.message = ''
- def _doingTakeThis(self, line):
- if line == '.':
- self.inputHandler = None
- article = self.message
- self.message = ''
- d = self.factory.backend.postRequest(article)
- d.addCallbacks(self._didTakeThis, self._errTakeThis)
- else:
- self.message = self.message + line + '\r\n'
- def _didTakeThis(self, result):
- self.sendLine('239 article transferred ok')
- def _errTakeThis(self, failure):
- print('TAKETHIS failed: ', failure)
- self.sendLine('439 article transfer failed')
- def do_GROUP(self, group):
- defer = self.factory.backend.groupRequest(group)
- defer.addCallbacks(self._gotGroup, self._errGroup)
- def _gotGroup(self, result):
- (name, num, high, low, flags) = result
- self.currentGroup = name
- self.currentIndex = low
- self.sendLine('211 %d %d %d %s group selected' % (num, low, high, name))
- def _errGroup(self, failure):
- print('GROUP failed: ', failure)
- self.sendLine('411 no such group')
- def articleWork(self, article, cmd, func):
- if self.currentGroup is None:
- self.sendLine('412 no newsgroup has been selected')
- else:
- if not article:
- if self.currentIndex is None:
- self.sendLine('420 no current article has been selected')
- else:
- article = self.currentIndex
- else:
- if article[0] == '<':
- return func(self.currentGroup, index = None, id = article)
- else:
- try:
- article = int(article)
- return func(self.currentGroup, article)
- except ValueError:
- self.sendLine('501 command syntax error')
- def do_ARTICLE(self, article = None):
- defer = self.articleWork(article, 'ARTICLE', self.factory.backend.articleRequest)
- if defer:
- defer.addCallbacks(self._gotArticle, self._errArticle)
- def _gotArticle(self, result):
- (index, id, article) = result
- self.currentIndex = index
- self.sendLine('220 %d %s article' % (index, id))
- s = basic.FileSender()
- d = s.beginFileTransfer(article, self.transport)
- d.addCallback(self.finishedFileTransfer)
- ##
- ## Helper for FileSender
- ##
- def finishedFileTransfer(self, lastsent):
- if lastsent != '\n':
- line = '\r\n.'
- else:
- line = '.'
- self.sendLine(line)
- ##
- def _errArticle(self, failure):
- print('ARTICLE failed: ', failure)
- self.sendLine('423 bad article number')
- def do_STAT(self, article = None):
- defer = self.articleWork(article, 'STAT', self.factory.backend.articleRequest)
- if defer:
- defer.addCallbacks(self._gotStat, self._errStat)
- def _gotStat(self, result):
- (index, id, article) = result
- self.currentIndex = index
- self.sendLine('223 %d %s article retreived - request text separately' % (index, id))
- def _errStat(self, failure):
- print('STAT failed: ', failure)
- self.sendLine('423 bad article number')
- def do_HEAD(self, article = None):
- defer = self.articleWork(article, 'HEAD', self.factory.backend.headRequest)
- if defer:
- defer.addCallbacks(self._gotHead, self._errHead)
- def _gotHead(self, result):
- (index, id, head) = result
- self.currentIndex = index
- self.sendLine('221 %d %s article retrieved' % (index, id))
- self.transport.write(head + '\r\n')
- self.sendLine('.')
- def _errHead(self, failure):
- print('HEAD failed: ', failure)
- self.sendLine('423 no such article number in this group')
- def do_BODY(self, article):
- defer = self.articleWork(article, 'BODY', self.factory.backend.bodyRequest)
- if defer:
- defer.addCallbacks(self._gotBody, self._errBody)
- def _gotBody(self, result):
- (index, id, body) = result
- self.currentIndex = index
- self.sendLine('221 %d %s article retrieved' % (index, id))
- self.lastsent = ''
- s = basic.FileSender()
- d = s.beginFileTransfer(body, self.transport)
- d.addCallback(self.finishedFileTransfer)
- def _errBody(self, failure):
- print('BODY failed: ', failure)
- self.sendLine('423 no such article number in this group')
- # NEXT and LAST are just STATs that increment currentIndex first.
- # Accordingly, use the STAT callbacks.
- def do_NEXT(self):
- i = self.currentIndex + 1
- defer = self.factory.backend.articleRequest(self.currentGroup, i)
- defer.addCallbacks(self._gotStat, self._errStat)
- def do_LAST(self):
- i = self.currentIndex - 1
- defer = self.factory.backend.articleRequest(self.currentGroup, i)
- defer.addCallbacks(self._gotStat, self._errStat)
- def do_MODE(self, cmd):
- cmd = cmd.strip().upper()
- if cmd == 'READER':
- self.servingSlave = 0
- self.sendLine('200 Hello, you can post')
- elif cmd == 'STREAM':
- self.sendLine('500 Command not understood')
- else:
- # This is not a mistake
- self.sendLine('500 Command not understood')
- def do_QUIT(self):
- self.sendLine('205 goodbye')
- self.transport.loseConnection()
- def do_HELP(self):
- self.sendLine('100 help text follows')
- self.sendLine('Read the RFC.')
- self.sendLine('.')
- def do_SLAVE(self):
- self.sendLine('202 slave status noted')
- self.servingeSlave = 1
- def do_XPATH(self, article):
- # XPATH is a silly thing to have. No client has the right to ask
- # for this piece of information from me, and so that is what I'll
- # tell them.
- self.sendLine('502 access restriction or permission denied')
- def do_XINDEX(self, article):
- # XINDEX is another silly command. The RFC suggests it be relegated
- # to the history books, and who am I to disagree?
- self.sendLine('502 access restriction or permission denied')
- def do_XROVER(self, range=None):
- """
- Handle a request for references of all messages in the currently
- selected group.
- This generates the same response a I{XHDR References} request would
- generate.
- """
- self.do_XHDR('References', range)
- def do_IHAVE(self, id):
- self.factory.backend.articleExistsRequest(id).addCallback(self._foundArticle)
- def _foundArticle(self, result):
- if result:
- self.sendLine('437 article rejected - do not try again')
- else:
- self.sendLine('335 send article to be transferred. End with <CR-LF>.<CR-LF>')
- self.inputHandler = self._handleIHAVE
- self.message = ''
- def _handleIHAVE(self, line):
- if line == '.':
- self.inputHandler = None
- self.factory.backend.postRequest(
- self.message
- ).addCallbacks(self._gotIHAVE, self._errIHAVE)
- self.message = ''
- else:
- self.message = self.message + line + '\r\n'
- def _gotIHAVE(self, result):
- self.sendLine('235 article transferred ok')
- def _errIHAVE(self, failure):
- print('IHAVE failed: ', failure)
- self.sendLine('436 transfer failed - try again later')
- class UsenetClientProtocol(NNTPClient):
- """
- A client that connects to an NNTP server and asks for articles new
- since a certain time.
- """
- def __init__(self, groups, date, storage):
- """
- Fetch all new articles from the given groups since the
- given date and dump them into the given storage. groups
- is a list of group names. date is an integer or floating
- point representing seconds since the epoch (GMT). storage is
- any object that implements the NewsStorage interface.
- """
- NNTPClient.__init__(self)
- self.groups, self.date, self.storage = groups, date, storage
- def connectionMade(self):
- NNTPClient.connectionMade(self)
- log.msg("Initiating update with remote host: " + str(self.transport.getPeer()))
- self.setStream()
- self.fetchNewNews(self.groups, self.date, '')
- def articleExists(self, exists, article):
- if exists:
- self.fetchArticle(article)
- else:
- self.count = self.count - 1
- self.disregard = self.disregard + 1
- def gotNewNews(self, news):
- self.disregard = 0
- self.count = len(news)
- log.msg("Transferring " + str(self.count) +
- " articles from remote host: " + str(self.transport.getPeer()))
- for i in news:
- self.storage.articleExistsRequest(i).addCallback(self.articleExists, i)
- def getNewNewsFailed(self, reason):
- log.msg("Updated failed (" + reason + ") with remote host: " + str(self.transport.getPeer()))
- self.quit()
- def gotArticle(self, article):
- self.storage.postRequest(article)
- self.count = self.count - 1
- if not self.count:
- log.msg("Completed update with remote host: " + str(self.transport.getPeer()))
- if self.disregard:
- log.msg("Disregarded %d articles." % (self.disregard,))
- self.factory.updateChecks(self.transport.getPeer())
- self.quit()
|