sanguinews 0.60

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,109 @@
1
+ ########################################################################
2
+ # FileToUpload - File class' extension specifically for sanguinews
3
+ # Copyright (c) 2013, Tadeus Dobrovolskij
4
+ # This library is free software; you can redistribute it and/or modify
5
+ # it under the terms of the GNU General Public License as published by
6
+ # the Free Software Foundation; either version 2 of the License, or
7
+ # (at your option) any later version.
8
+ #
9
+ # This library is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU General Public License along
15
+ # with this library; if not, write to the Free Software Foundation, Inc.,
16
+ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17
+ ########################################################################
18
+ require 'zlib'
19
+ require 'nzb'
20
+ require 'vmstat'
21
+
22
+ module Sanguinews
23
+ class FileToUpload < File
24
+ attr_accessor :name, :chunks, :subject
25
+ attr_reader :crc32, :nzb, :dir_prefix, :cname
26
+
27
+ @@max_mem = nil
28
+
29
+ def initialize(var)
30
+ @dir_prefix = ''
31
+
32
+ var[:mode] = "rb" if var[:mode].nil?
33
+
34
+ super(var[:name], var[:mode])
35
+ @filemode = var[:filemode]
36
+ @name = File.basename(var[:name])
37
+ chunk_amount(var[:chunk_length])
38
+ common_name(var)
39
+ max_mem
40
+ if var[:nzb]
41
+ @from = var[:from]
42
+ @groups = var[:groups]
43
+ nzb_init
44
+ end
45
+ return @name
46
+ end
47
+
48
+ def close(last=false)
49
+ if @nzb
50
+ @nzb.write_file_header(@from, @subject, @groups)
51
+ @nzb.write_segments
52
+ @nzb.write_file_footer
53
+ @nzb.write_footer if @filemode || last
54
+ end
55
+ super()
56
+ end
57
+
58
+ def write_segment_info(length, chunk, msgid)
59
+ @nzb.save_segment(length, chunk, msgid) if @nzb
60
+ end
61
+
62
+ def file_crc32
63
+ @crc32 ||= begin
64
+ fcrc32 = nil
65
+ until self.eof?
66
+ f = self.read(@@max_mem)
67
+ crc32 = Zlib.crc32(f, 0)
68
+ if fcrc32.nil?
69
+ fcrc32 = crc32
70
+ else
71
+ fcrc32 = Zlib.crc32_combine(fcrc32, crc32, f.size)
72
+ end
73
+ end
74
+ self.rewind
75
+ fcrc32.to_s(16)
76
+ end
77
+ end
78
+
79
+ private
80
+
81
+ def chunk_amount(chunk_length)
82
+ chunks = self.size.to_f / chunk_length
83
+ @chunks = chunks.ceil
84
+ end
85
+
86
+ def common_name(var)
87
+ if var[:filemode]
88
+ @cname = File.basename(var[:name])
89
+ else
90
+ @cname = File.basename(File.dirname(var[:name]))
91
+ @dir_prefix = @cname + " [#{var[:current]}/#{var[:last]}] - "
92
+ end
93
+ @subject = "#{var[:prefix]}#{@dir_prefix}#{@name} yEnc (1/#{@chunks})"
94
+ end
95
+
96
+ def nzb_init
97
+ @nzb = Nzb.new(@cname, "sanguinews_")
98
+ @nzb.write_header
99
+ end
100
+
101
+ def max_mem
102
+ if @@max_mem.nil?
103
+ memory = Vmstat.memory
104
+ @@max_mem = (memory[:free] * memory[:pagesize] * 0.1).floor
105
+ end
106
+ @@max_mem
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,896 @@
1
+ # = nntp.rb
2
+ #
3
+ # NNTP Client Library
4
+ #
5
+ # This program is free software; you can redistribute it and/or modify it
6
+ # under the terms of the GNU Lesser General Public License as published by
7
+ # the Free Software Foundation; either version 2.1 of the License, or (at
8
+ # your option) any later version.
9
+ #
10
+ # This program is distributed in the hope that it will be useful, but WITHOUT
11
+ # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12
+ # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
13
+ # more details.
14
+ #
15
+ # You should have received a copy of the GNU Lesser General Public License
16
+ # along with this program; if not, write to the Free Software Foundation,
17
+ # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
18
+ #
19
+ # See Net::NNTP for detailed documentation.
20
+ #
21
+ # ==Download
22
+ #
23
+ # (http://rubyforge.org/projects/nntp)
24
+ #
25
+ # == Copyright
26
+ #
27
+ # Copyright (C) 2004-2007 by Dr Balwinder Singh Dheeman. Distributed under
28
+ # the GNU GPL (http://www.gnu.org/licenses/gpl.html). See the files "COPYING"
29
+ # and, or "Copyright" , supplied with all distributions for additional
30
+ # information.
31
+ #
32
+ # == Authors
33
+ #
34
+ # Balwinder Singh Dheeman <bsd.SANSPAM@rubyforge.org> (http://cto.homelinux.net/~bsd)
35
+ # Albert Vernon <aevernon.SANSPAM@rubyforge.org>
36
+ # Bob Schafer <rschafer.SANSPAM@rubyforge.org>
37
+ # Mark Triggs <mark.SANSPAM@dishevelled.net>
38
+
39
+ require 'net/protocol'
40
+ require 'digest/md5'
41
+ require 'openssl'
42
+ module Net #:nodoc:
43
+
44
+ # Module mixed in to all NNTP error classes
45
+ module NNTPError
46
+ # This *class* is module for some reason.
47
+ # In ruby 1.9.x, this module becomes a class.
48
+ end
49
+
50
+ # Represents an NNTP authentication error.
51
+ class NNTPAuthenticationError < ProtoAuthError
52
+ include NNTPError
53
+ end
54
+
55
+ # Represents NNTP error code 420 or 450, a temporary error.
56
+ class NNTPServerBusy < ProtoServerError
57
+ include NNTPError
58
+ end
59
+
60
+ # Represents NNTP error code 440, posting not permitted.
61
+ class NNTPPostingNotAllowed < ProtoServerError
62
+ include NNTPError
63
+ end
64
+
65
+ # Represents an NNTP command syntax error (error code 500)
66
+ class NNTPSyntaxError < ProtoSyntaxError
67
+ include NNTPError
68
+ end
69
+
70
+ # Represents a fatal NNTP error (error code 5xx, except for 500)
71
+ class NNTPFatalError < ProtoFatalError
72
+ include NNTPError
73
+ end
74
+
75
+ # Unexpected reply code returned from server.
76
+ class NNTPUnknownError < ProtoUnknownError
77
+ include NNTPError
78
+ end
79
+
80
+ # Error in NNTP response data.
81
+ class NNTPDataError
82
+ include NNTPError
83
+ end
84
+
85
+ # = Net::NNTP
86
+ #
87
+ # == What is This Library?
88
+ #
89
+ # This library provides functionality to retrieve and, or post Usenet news
90
+ # articles via NNTP, the Network News Transfer Protocol. The Usenet is a
91
+ # world-wide distributed discussion system. It consists of a set of
92
+ # "newsgroups" with names that are classified hierarchically by topic.
93
+ # "articles" or "messages" are "posted" to these newsgroups by people on
94
+ # computers with the appropriate software -- these articles are then
95
+ # broadcast to other interconnected NNTP servers via a wide variety of
96
+ # networks. For details of NNTP itself, see [RFC977]
97
+ # (http://www.ietf.org/rfc/rfc977.txt).
98
+ #
99
+ # == What is This Library NOT?
100
+ #
101
+ # This library does NOT provide functions to compose Usenet news. You
102
+ # must create and, or format them yourself as per guidelines per
103
+ # Standard for Interchange of Usenet messages, see [RFC850], [RFC2047]
104
+ # and a fews other RFC's (http://www.ietf.org/rfc/rfc850.txt),
105
+ # (http://www.ietf.org/rfc/rfc2047.txt).
106
+ #
107
+ # FYI: the official documentation on Usenet news extentions is: [RFC2980]
108
+ # (http://www.ietf.org/rfc/rfc2980.txt).
109
+ #
110
+ # == Examples
111
+ #
112
+ # === Posting Messages
113
+ #
114
+ # You must open a connection to an NNTP server before posting messages.
115
+ # The first argument is the address of your NNTP server, and the second
116
+ # argument is the port number. Using NNTP.start with a block is the simplest
117
+ # way to do this. This way, the NNTP connection is closed automatically
118
+ # after the block is executed.
119
+ #
120
+ # require 'rubygems'
121
+ # require 'nntp'
122
+ # Net::NNTP.start('your.nntp.server', 119) do |nntp|
123
+ # # Use the NNTP object nntp only in this block.
124
+ # end
125
+ #
126
+ # Replace 'your.nntp.server' with your NNTP server. Normally your system
127
+ # manager or internet provider supplies a server for you.
128
+ #
129
+ # Then you can post messages.
130
+ #
131
+ # require 'date'
132
+ # date = DateTime.now().strftime(fmt='%a, %d %b %Y %T %z')
133
+ #
134
+ # msgstr = <<END_OF_MESSAGE
135
+ # From: Your Name <your@mail.address>
136
+ # Newsgroups: news.group.one, news.group.two ...
137
+ # Subject: test message
138
+ # Date: #{date}
139
+ #
140
+ # This is a test message.
141
+ # END_OF_MESSAGE
142
+ #
143
+ # require 'rubygems'
144
+ # require 'nntp'
145
+ # Net::NNTP.start('your.nntp.server', 119) do |nntp|
146
+ # nntp.post msgstr
147
+ # end
148
+ #
149
+ # *NOTE*: The NNTP message headers such as +Date:+, +Message-ID:+ and, or
150
+ # +Path:+ if ommited, may also be generated and added by your Usenet news
151
+ # server; better you verify the behavior of your news server.
152
+ #
153
+ # === Closing the Session
154
+ #
155
+ # You MUST close the NNTP session after posting messages, by calling the
156
+ # Net::NNTP#finish method:
157
+ #
158
+ # # using NNTP#finish
159
+ # nntp = Net::NNTP.start('your.nntp.server', 119)
160
+ # nntp.post msgstr
161
+ # nntp.finish
162
+ #
163
+ # You can also use the block form of NNTP.start/NNTP#start. This closes
164
+ # the NNTP session automatically:
165
+ #
166
+ # # using block form of NNTP.start
167
+ # Net::NNTP.start('your.nntp.server', 119) do |nntp|
168
+ # nntp.post msgstr
169
+ # end
170
+ #
171
+ # I strongly recommend this scheme. This form is simpler and more robust.
172
+ #
173
+ # === NNTP Authentication
174
+ #
175
+ # The Net::NNTP class may support various authentication schemes depending
176
+ # on your news server's reponse to CAPABILITIES command. To use NNTP
177
+ # authentication, pass extra arguments to NNTP.start/NNTP#start.
178
+ #
179
+ # See NNTP Extension for Authentication:
180
+ # (http://www.ietf.org/internet-drafts/draft-ietf-nntpext-authinfo-07.txt)
181
+ #
182
+ # Net::NNTP.start('your.nntp.server', 119,
183
+ # 'YourAccountName', 'YourPassword', :method)
184
+ #
185
+ # Where +:method+ can be one of the 'gassapi', 'digest_md5',
186
+ # 'cram_md5', 'starttls', 'external', 'plain', 'generic', 'simple' or
187
+ # 'original'; the later and, or unencrypted ones are less secure!
188
+ #
189
+ # In the case of method +:generic+ argumnents should be passed to a format
190
+ # string as follows:
191
+ #
192
+ # Net::NNTP.start('your.nntp.server', 119,
193
+ # "format", *arguments, :generic)
194
+ #
195
+ # *NOTE*: The Authentication mechanism will fallback to a lesser secure
196
+ # scheme, if your Usenet server does not supports method opted by you,
197
+ # except for the +:generic+ option.
198
+ #
199
+ class NNTP
200
+
201
+ # The default NNTP port, port 119.
202
+ def NNTP.default_port
203
+ 119
204
+ end
205
+
206
+ # Creates a new Net::NNTP object.
207
+ #
208
+ # +address+ is the hostname or ip address of your NNTP server. +port+ is
209
+ # the port to connect to; it defaults to port 119.
210
+ #
211
+ # This method does not opens any TCP connection. You can use NNTP.start
212
+ # instead of NNTP.new if you want to do everything at once. Otherwise,
213
+ # follow NNTP.new with optional changes to +:open_timeout+,
214
+ # +:read_timeout+ and, or +NNTP#set_debug_output+ and then NNTP#start.
215
+ #
216
+ def initialize(address, port = nil)
217
+ @address = address
218
+ @port = (port || NNTP.default_port)
219
+ @socket = nil
220
+ @started = false
221
+ @open_timeout = 30
222
+ @read_timeout = 60
223
+ @error_occured = false
224
+ @debug_output = nil
225
+ end
226
+
227
+ # Provide human-readable stringification of class state.
228
+ def inspect #:nodoc:
229
+ "#<#{self.class} #{@address}:#{@port} started=#{@started}>"
230
+ end
231
+
232
+ # The address of the NNTP server to connect to.
233
+ attr_reader :address
234
+
235
+ # The port number of the NNTP server to connect to.
236
+ attr_reader :port
237
+
238
+ # Seconds to wait while attempting to open a connection. If the
239
+ # connection cannot be opened within this time, a TimeoutError is raised.
240
+ attr_accessor :open_timeout
241
+
242
+ # Seconds to wait while reading one block (by one read(2) call). If the
243
+ # read(2) call does not complete within this time, a TimeoutError is
244
+ # raised.
245
+ attr_reader :read_timeout
246
+
247
+ # Set the number of seconds to wait until timing-out a read(2) call.
248
+ def read_timeout=(sec)
249
+ @socket.read_timeout = sec if @socket
250
+ @read_timeout = sec
251
+ end
252
+
253
+ # Set an output stream for debug logging. You must call this before
254
+ # #start.
255
+ #
256
+ # === Example
257
+ #
258
+ # nntp = Net::NNTP.new(addr, port)
259
+ # nntp.set_debug_output $stderr
260
+ # nntp.start do |nntp|
261
+ # ....
262
+ # end
263
+ #
264
+ # *WARNING*: This method causes serious security holes. Use this method
265
+ # for only debugging.
266
+ #
267
+ def set_debug_output(arg)
268
+ @debug_output = arg
269
+ end
270
+
271
+ #
272
+ # NNTP session control
273
+ #
274
+
275
+ # Creates a new Net::NNTP object and connects to the server.
276
+ #
277
+ # This method is equivalent to:
278
+ #
279
+ # Net::NNTP.new(address, port).start(account, password, :method)
280
+ #
281
+ # === Example
282
+ #
283
+ # Net::NNTP.start('your.nntp.server') do |nntp|
284
+ # nntp.post msgstr
285
+ # end
286
+ #
287
+ # === Block Usage
288
+ #
289
+ # If called with a block, the newly-opened Net::NNTP object is yielded to
290
+ # the block, and automatically closed when the block finishes. If called
291
+ # without a block, the newly-opened Net::NNTP object is returned to the
292
+ # caller, and it is the caller's responsibility to close it when
293
+ # finished.
294
+ #
295
+ # === Parameters
296
+ #
297
+ # +address+ is the hostname or ip address of your nntp server.
298
+ #
299
+ # +port+ is the port to connect to; it defaults to port 119.
300
+ #
301
+ # The remaining arguments are used for NNTP authentication, if required
302
+ # or desired. +user+ is the account name, +secret+ is your password or
303
+ # other authentication token, and +method+ is the authentication
304
+ # type; defaults to 'original'. Please read the discussion of NNTP
305
+ # Authentication in the overview notes above.
306
+ #
307
+ # === Errors
308
+ #
309
+ # This method may raise:
310
+ #
311
+ # * Net::NNTPAuthenticationError
312
+ # * Net::NNTPFatalError
313
+ # * Net::NNTPServerBusy
314
+ # * Net::NNTPSyntaxError
315
+ # * Net::NNTPUnknownError
316
+ # * IOError
317
+ # * TimeoutError
318
+ #
319
+ def NNTP.start(address, port = nil,
320
+ user = nil, secret = nil, method = nil,
321
+ &block) # :yield: nntp
322
+ new(address, port).start(user, secret, method, &block)
323
+ end
324
+
325
+ # +true+ if the NNTP session has been started.
326
+ def started?
327
+ @started
328
+ end
329
+
330
+ # Opens a TCP connection and starts the NNTP session.
331
+ #
332
+ # === Parameters
333
+ #
334
+ # If both of +user+ and +secret+ are given, NNTP authentication will be
335
+ # attempted using the AUTH command. The +method+ specifies the type of
336
+ # authentication to attempt; it must be one of :original, :simple,
337
+ # :generic, :plain, :starttls, :external, :cram_md5, :digest_md5 and, or
338
+ # :gassapi may be used. See the discussion of NNTP Authentication in the
339
+ # overview notes.
340
+ #
341
+ #
342
+ # === Block Usage
343
+ #
344
+ # When this methods is called with a block, the newly-started NNTP object
345
+ # is yielded to the block, and automatically closed after the block call
346
+ # finishes. Otherwise, it is the caller's responsibility to close the
347
+ # session when finished.
348
+ #
349
+ # === Example
350
+ #
351
+ # This is very similar to the class method NNTP.start.
352
+ #
353
+ # require 'rubygems'
354
+ # require 'nntp'
355
+ # nntp = Net::NNTP.new('nntp.news.server', 119)
356
+ # nntp.start(account, password, method) do |nntp|
357
+ # nntp.post msgstr
358
+ # end
359
+ #
360
+ # The primary use of this method (as opposed to NNTP.start) is probably
361
+ # to set debugging (#set_debug_output), which must be done before the
362
+ # session is started.
363
+ #
364
+ # === Errors
365
+ #
366
+ # If session has already been started, an IOError will be raised.
367
+ #
368
+ # This method may raise:
369
+ #
370
+ # * Net::NNTPAuthenticationError
371
+ # * Net::NNTPFatalError
372
+ # * Net::NNTPServerBusy
373
+ # * Net::NNTPSyntaxError
374
+ # * Net::NNTPUnknownError
375
+ # * IOError
376
+ # * TimeoutError
377
+ #
378
+ def start(user = nil, secret = nil, method = nil) # :yield: nntp
379
+ if block_given?
380
+ begin
381
+ do_start(user, secret, method)
382
+ return yield(self)
383
+ ensure
384
+ do_finish
385
+ end
386
+ else
387
+ do_start(user, secret, method)
388
+ return self
389
+ end
390
+ end
391
+
392
+ def do_start(user, secret, method) #:nodoc:
393
+ raise IOError, 'NNTP session already started' if @started
394
+ check_auth_args user, secret, method if user or secret
395
+
396
+ if InternetMessageIO.respond_to?(:old_open)
397
+ @socket = InternetMessageIO.old_open(@address, @port, @open_timeout,
398
+ @read_timeout, @debug_output)
399
+
400
+ else
401
+ socket = timeout(@open_timeout) { TCPSocket.open(@address, @port) }
402
+ @socket = InternetMessageIO.new(socket)
403
+ @socket.read_timeout = @read_timeout
404
+ @socket.debug_output = @debug_output
405
+ # Use OpenSSL to wrap socket
406
+ # Introduced by: Tadeus Dobrovolskij
407
+ if method == :tls
408
+ ssl_context = OpenSSL::SSL::SSLContext.new
409
+ ssl = OpenSSL::SSL::SSLSocket.new socket, ssl_context
410
+ ssl.sync_close = true
411
+ ssl.connect
412
+ @socket = InternetMessageIO.new(ssl)
413
+ @socket.read_timeout = @read_timeout
414
+ @socket.debug_output = @debug_output
415
+ end
416
+ end
417
+
418
+ check_response(critical { recv_response() })
419
+
420
+ mode_reader_success = false
421
+ tried_authenticating = false
422
+ until mode_reader_success
423
+ begin
424
+ mode_reader
425
+ mode_reader_success = true
426
+ rescue NNTPAuthenticationError
427
+ if tried_authenticating
428
+ raise
429
+ end
430
+ rescue ProtocolError
431
+ raise
432
+ end
433
+ authenticate user, secret, method if user
434
+ tried_authenticating = true
435
+ end
436
+
437
+ @started = true
438
+ ensure
439
+ @socket.close if not @started and @socket and not @socket.closed?
440
+ end
441
+ private :do_start
442
+
443
+ # Finishes the NNTP session and closes TCP connection. Raises IOError if
444
+ # not started.
445
+ def finish
446
+ raise IOError, 'not yet started' unless started?
447
+ do_finish
448
+ end
449
+
450
+ def do_finish #:nodoc:
451
+ quit if @socket and not @socket.closed? and not @error_occured
452
+ ensure
453
+ @started = false
454
+ @error_occured = false
455
+ @socket.close if @socket and not @socket.closed?
456
+ @socket = nil
457
+ end
458
+ private :do_finish
459
+
460
+ public
461
+
462
+ # POST
463
+ #
464
+ # Posts +msgstr+ as a message. Single CR ("\r") and LF ("\n") found in
465
+ # the +msgstr+, are converted into the CR LF pair. You cannot post a
466
+ # binary message with this method. +msgstr+ _should include both the
467
+ # message headers and body_. All non US-ASCII, binary and, or multi-part
468
+ # messages should be submitted in an encoded form as per MIME standards.
469
+ #
470
+ # === Example
471
+ #
472
+ # Net::NNTP.start('nntp.example.com') do |nntp|
473
+ # nntp.post msgstr
474
+ # end
475
+ #
476
+ # === Errors
477
+ #
478
+ # This method may raise:
479
+ #
480
+ # * Net::NNTPFatalError
481
+ # * Net::NNTPPostingNotAllowed
482
+ # * Net::NNTPServerBusy
483
+ # * Net::NNTPSyntaxError
484
+ # * Net::NNTPUnknownError
485
+ # * IOError
486
+ # * TimeoutError
487
+ #
488
+ def post(msgstr)
489
+ stat = post0 {
490
+ @socket.write_message msgstr
491
+ }
492
+ return stat[0..3], stat[4..-1].chop
493
+ end
494
+
495
+ # Opens a message writer stream and gives it to the block. The stream is
496
+ # valid only in the block, and has these methods:
497
+ #
498
+ # puts(str = ''):: outputs STR and CR LF.
499
+ # print(str):: outputs STR.
500
+ # printf(fmt, *args):: outputs sprintf(fmt,*args).
501
+ # write(str):: outputs STR and returns the length of written bytes.
502
+ # <<(str):: outputs STR and returns self.
503
+ #
504
+ # If a single CR ("\r") or LF ("\n") is found in the message, it is
505
+ # converted to the CR LF pair. You cannot post a binary message with
506
+ # this method.
507
+ #
508
+ # === Parameters
509
+ #
510
+ # Block
511
+ #
512
+ # === Example
513
+ #
514
+ # Net::NNTP.start('nntp.example.com', 119) do |nntp|
515
+ # nntp.open_message_stream do |f|
516
+ # f.puts 'From: from@example.com'
517
+ # f.puts 'Newsgroups: news.group.one, news.group.two ...'
518
+ # f.puts 'Subject: test message'
519
+ # f.puts
520
+ # f.puts 'This is a test message.'
521
+ # end
522
+ # end
523
+ #
524
+ # === Errors
525
+ #
526
+ # This method may raise:
527
+ #
528
+ # * Net::NNTPFatalError
529
+ # * Net::NNTPPostingNotAllowed
530
+ # * Net::NNTPServerBusy
531
+ # * Net::NNTPSyntaxError
532
+ # * Net::NNTPUnknownError
533
+ # * IOError
534
+ # * TimeoutError
535
+ #
536
+ def open_message_stream(&block) # :yield: stream
537
+ post0 { @socket.write_message_by_block(&block) }
538
+ end
539
+
540
+ # ARTICLE [<Message-ID>|<Number>]
541
+ def article(id_num = nil)
542
+ stat, text = longcmd("ARTICLE #{id_num}".strip)
543
+ return stat[0..2], text
544
+ end
545
+
546
+ # BODY [<Message-ID>|<Number>]
547
+ def body(id_num = nil)
548
+ stat, text = longcmd("BODY #{id_num}".strip)
549
+ return stat[0..2], text
550
+ end
551
+
552
+ # IO_BODY <output IO object> [<Message-ID>|<Number>]
553
+ def io_body (io_output, id_num = nil)
554
+ stat = io_longcmd(io_output, "BODY #{id_num}".strip)
555
+ return stat[0..2], io_output
556
+ end
557
+
558
+ # DATE
559
+ def date
560
+ text = []
561
+ stat = shortcmd("DATE")
562
+ text << stat[4...12]
563
+ text << stat[12...18]
564
+ raise NNTPDataError, stat, caller unless text[0].length == 8 and text[1].length == 6
565
+ return stat[0..2], text
566
+ end
567
+
568
+ # GROUP <Newsgroup>
569
+ def group(ng)
570
+ stat = shortcmd("GROUP %s", ng)
571
+ return stat[0..2], stat[4..-1].chop
572
+ end
573
+
574
+ # HEAD [<Message-ID>|<Number>]
575
+ def head(id_num = nil)
576
+ stat, text = longcmd("HEAD #{id_num}".strip)
577
+ return stat[0..2], text
578
+ end
579
+
580
+ # HELP
581
+ def help
582
+ stat, text = longcmd('HELP')
583
+ text.each_with_index do |line, index|
584
+ text[index] = line.gsub(/\A\s+/, '')
585
+ end
586
+ return stat[0..2], text
587
+ end
588
+
589
+ # LAST
590
+ def last
591
+ stat = shortcmd('LAST')
592
+ return stat[0..2], stat[4..-1].chop
593
+ end
594
+
595
+ # LIST [ACTIVE|NEWSGROUPS] [<Wildmat>]]:br:
596
+ # LIST [ACTIVE.TIMES|EXTENSIONS|SUBSCRIPTIONS|OVERVIEW.FMT]
597
+ def list(opts = nil)
598
+ stat, text = longcmd("LIST #{opts}".strip)
599
+ return stat[0..2], text
600
+ end
601
+
602
+ # LISTGROUP <Newsgroup>
603
+ def listgroup(ng)
604
+ stat, text = longcmd("LISTGROUP #{ng}".strip)
605
+ return stat[0..2], text
606
+ end
607
+
608
+ # MODE READER
609
+ def mode_reader
610
+ stat = shortcmd('MODE READER')
611
+ return stat[0..2], stat[4..-1].chop
612
+ end
613
+ private :mode_reader #:nodoc:
614
+
615
+ # NEWGROUPS <yymmdd> <hhmmss> [GMT]
616
+ def newgroups(date, time, tzone = nil)
617
+ stat, text = longcmd("NEWGROUPS #{date} #{time} #{tzone}".strip)
618
+ return stat[0..2], text
619
+ end
620
+
621
+ # NEXT
622
+ def next
623
+ stat = shortcmd('NEXT')
624
+ return stat[0..2], stat[4..-1].chop
625
+ end
626
+
627
+ # OVER <Range> # e.g first[-[last]]
628
+ def over(range)
629
+ stat, text = longcmd("OVER #{range}".strip)
630
+ return stat[0..2], text
631
+ end
632
+
633
+ # QUIT
634
+ def quit
635
+ stat = shortcmd('QUIT')
636
+ end
637
+ private :quit #:nodoc:
638
+
639
+ # SLAVE
640
+ def slave
641
+ stat = shortcmd('SLAVE')
642
+ return stat[0..2], stat[4..-1].chop
643
+ end
644
+
645
+ # STAT [<Message-ID>|<Number>]
646
+ def stat(id_num = nil)
647
+ stat = shortcmd("STAT #{id_num}".strip)
648
+ return stat[0..2], stat[4..-1].chop
649
+ end
650
+
651
+ # XHDR <Header> <Message-ID>|<Range> # e.g first[-[last]]
652
+ def xhdr(header, id_range)
653
+ stat, text = longcmd("XHDR #{header} #{id_range}".strip)
654
+ return stat[0..2], text
655
+ end
656
+
657
+ # XOVER <Range> # e.g first[-[last]]
658
+ def xover(range)
659
+ stat, text = longcmd("XOVER #{range}".strip)
660
+ return stat[0..2], text
661
+ end
662
+
663
+ private
664
+
665
+ #
666
+ # row level library
667
+ #
668
+
669
+ def post0
670
+ raise IOError, 'closed session' unless @socket
671
+ stat = critical {
672
+ check_response(get_response('POST'), true)
673
+ yield
674
+ recv_response()
675
+ }
676
+ check_response(stat)
677
+ end
678
+
679
+ #
680
+ # auth
681
+ #
682
+
683
+ def check_auth_args(user, secret, method)
684
+ raise ArgumentError, 'both user and secret are required'\
685
+ unless user and secret
686
+ authmeth = "auth_#{method || 'original'}"
687
+ raise ArgumentError, "wrong auth type #{method}"\
688
+ unless respond_to?(authmeth, true)
689
+ end
690
+
691
+ def authenticate(user, secret, method)
692
+ methods = %w(original simple generic plain tls starttls external cram_md5 digest_md5 gassapi)
693
+ method = "#{method || 'original'}"
694
+ authmeth = methods.index(method)
695
+ begin
696
+ __send__("auth_#{method}", user, secret)
697
+ rescue NNTPAuthenticationError
698
+ if authmeth and authmeth > 0
699
+ authmeth -= 1 # fallback
700
+ method = methods[authmeth]
701
+ @error_occured = false
702
+ retry
703
+ else
704
+ raise
705
+ end
706
+ end
707
+ end
708
+
709
+ # AUTHINFO USER username
710
+ # AUTHINFO PASS password
711
+ def auth_original(user, secret)
712
+ stat = critical {
713
+ check_response(get_response("AUTHINFO USER %s", user), true)
714
+ check_response(get_response("AUTHINFO PASS %s", secret), true)
715
+ }
716
+ raise NNTPAuthenticationError, stat unless /\A2../ === stat
717
+ end
718
+
719
+ # AUTHINFO SIMPLE
720
+ # username password
721
+ def auth_simple(user, secret)
722
+ stat = critical {
723
+ check_response(get_response('AUTHINFO SIMPLE'), true)
724
+ check_response(get_response('%s %s', user, secret), true)
725
+ }
726
+ raise NNTPAuthenticationError, stat unless /\A2../ === stat
727
+ end
728
+
729
+ # AUTHINFO GENERIC authenticator arguments ...
730
+ #
731
+ # The authentication protocols are not inculeded in RFC2980,
732
+ # see [RFC1731] (http://www.ieft.org/rfc/rfc1731.txt).
733
+ def auth_generic(fmt, *args)
734
+ stat = critical {
735
+ cmd = 'AUTHINFO GENERIC ' + sprintf(fmt, *args)
736
+ check_response(get_response(cmd), true)
737
+ }
738
+ raise NNTPAuthenticationError, stat unless /\A2../ === stat
739
+ end
740
+
741
+ # AUTHINFO SASL PLAIN
742
+ def auth_plain(user, secret)
743
+ stat = critical {
744
+ check_response(get_response('AUTHINFO SASL PLAIN %s',
745
+ base64_encode("\0#{user}\0#{secret}")), true)
746
+ }
747
+ raise NNTPAuthenticationError, stat unless /\A2../ === stat
748
+ end
749
+
750
+ # ORIGINAL OVER ENCRYPTED CONNECTION
751
+ # Introduced by: Tadeus Dobrovolskij
752
+ # AUTHINFO USER username
753
+ # AUTHINFO PASS password
754
+ def auth_tls(user, secret)
755
+ stat = critical {
756
+ check_response(get_response("AUTHINFO USER %s", user), true)
757
+ check_response(get_response("AUTHINFO PASS %s", secret), true)
758
+ }
759
+ raise NNTPAuthenticationError, stat unless /\A2../ === stat
760
+ end
761
+
762
+ # STARTTLS
763
+ def auth_starttls(user, secret)
764
+ stat = critical {
765
+ check_response(get_response('STARTTLS'), true)
766
+ ### FIXME:
767
+ }
768
+ raise NNTPAuthenticationError, 'not implemented as yet!'
769
+ end
770
+
771
+ # AUTHINFO SASL EXTERNAL =
772
+ def auth_external(user, secret)
773
+ stat = critical {
774
+ check_response(get_response('AUTHINFO SASL EXTERNAL ='), true)
775
+ ### FIXME:
776
+ }
777
+ raise NNTPAuthenticationError, 'not implemented as yet!'
778
+ end
779
+
780
+ # AUTHINFO SASL CRAM-MD5 [RFC2195]
781
+ def auth_cram_md5(user, secret)
782
+ stat = nil
783
+ critical {
784
+ stat = check_response(get_response('AUTHINFO SASL CRAM-MD5'), true)
785
+ challenge = stat.split(/ /)[1].unpack('m')[0]
786
+ secret = Digest::MD5.digest(secret) if secret.size > 64
787
+
788
+ isecret = secret + "\0" * (64 - secret.size)
789
+ osecret = isecret.dup
790
+ 0.upto(63) do |i|
791
+ isecret[i] ^= 0x36
792
+ osecret[i] ^= 0x5c
793
+ end
794
+ tmp = Digest::MD5.digest(isecret + challenge)
795
+ tmp = Digest::MD5.hexdigest(osecret + tmp)
796
+
797
+ stat = get_response(base64_encode(user + ' ' + tmp))
798
+ }
799
+ raise NNTPAuthenticationError, stat unless /\A2../ === stat
800
+ end
801
+
802
+ # AUTHINFO SASL DIGEST-MD5
803
+ def auth_digest_md5(user, secret)
804
+ stat = critical {
805
+ check_response(get_response('AUTHINFO SASL DIGEST-MD5'), true)
806
+ ### FIXME:
807
+ }
808
+ raise NNTPAuthenticationError, 'not implemented as yet!'
809
+ end
810
+
811
+ # AUTHINFO SASL GASSAPI
812
+ def auth_gassapi(user, secret)
813
+ stat = critical {
814
+ check_response(get_response('AUTHINFO SASL GASSAPI'), true)
815
+ ### FIXME:
816
+ }
817
+ raise NNTPAuthenticationError, 'not implemented as yet!'
818
+ end
819
+
820
+ def base64_encode(str)
821
+ # expects "str" may not become too long
822
+ [str].pack('m').gsub(/\s+/, '')
823
+ end
824
+
825
+ def longcmd(fmt, *args)
826
+ text = []
827
+ stat = io_longcmd(text, fmt, *args)
828
+ return stat, text.map { |line| line.chomp! }
829
+ end
830
+
831
+ def io_longcmd(target, fmt, *args)
832
+ if stat = shortcmd(fmt, *args)
833
+ while true
834
+ line = @socket.readline
835
+ break if line =~ /^\.\s*$/ # done
836
+ line = line[1..-1] if line.to_s[0...2] == '..'
837
+ target << line + $/
838
+ end
839
+ end
840
+
841
+ return stat, target
842
+ end
843
+
844
+ def shortcmd(fmt, *args)
845
+ stat = critical {
846
+ @socket.writeline sprintf(fmt, *args)
847
+ recv_response()
848
+ }
849
+ check_response(stat)
850
+ end
851
+
852
+ def get_response(fmt, *args)
853
+ @socket.writeline sprintf(fmt, *args)
854
+ recv_response()
855
+ end
856
+
857
+ def recv_response
858
+ stat = ''
859
+ while true
860
+ line = @socket.readline
861
+ stat << line << "\n"
862
+ break unless line[3] == ?- # "210-PIPELINING"
863
+ end
864
+ stat
865
+ end
866
+
867
+ def check_response(stat, allow_continue = false)
868
+ return stat if /\A1/ === stat # 1xx info msg
869
+ return stat if /\A2/ === stat # 2xx cmd k
870
+ return stat if allow_continue and /\A[35]/ === stat # 3xx cmd k, snd rst
871
+ exception = case stat
872
+ when /\A440/ then NNTPPostingNotAllowed # 4xx cmd k, bt nt prfmd
873
+ when /\A48/ then NNTPAuthenticationError
874
+ when /\A4/ then NNTPServerBusy
875
+ when /\A50/ then NNTPSyntaxError # 5xx cmd ncrrct
876
+ when /\A55/ then NNTPFatalError
877
+ else
878
+ NNTPUnknownError
879
+ end
880
+ raise exception, stat
881
+ end
882
+
883
+ def critical(&block)
884
+ return '200 dummy reply code' if @error_occured
885
+ begin
886
+ return yield()
887
+ rescue Exception
888
+ @error_occured = true
889
+ raise
890
+ end
891
+ end
892
+
893
+ end # class_NNTP
894
+
895
+ NNTPSession = NNTP
896
+ end # module_Net