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