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.
- checksums.yaml +7 -0
- data/bin/sanguinews +4 -0
- data/ext/yencoded/extconf.rb +3 -0
- data/ext/yencoded/yencoded.c +89 -0
- data/ext/yencoded/yencoded.h +6 -0
- data/lib/sanguinews.rb +415 -0
- data/lib/sanguinews/file_to_upload.rb +109 -0
- data/lib/sanguinews/nntp.rb +896 -0
- data/lib/sanguinews/nntp_msg.rb +82 -0
- data/lib/sanguinews/thread-pool.rb +150 -0
- data/lib/sanguinews/version.rb +3 -0
- metadata +153 -0
@@ -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
|