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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 8fa336b8808a65611854b554245a288b4b05f303
4
+ data.tar.gz: 0c4309243350764bae4669b537f65d4ed9b79e12
5
+ SHA512:
6
+ metadata.gz: b231b17b59ab469a3d64294c6f3befc97abf0083ebfac5fce887e74511844591708f879de799b09dc75348f202d6bfd5aa1bb05bde52b8970960214922f6abba
7
+ data.tar.gz: e551da80f3d6f1641c4e47b46636b34ffcfd196fbd9f111c6fab804823df3112418669f38ecd5e3c2c34e4f8a25407f7616d0383d4146cfa4f281f4ab07a8c7f
data/bin/sanguinews ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+ require 'sanguinews'
3
+
4
+ Sanguinews.run!
@@ -0,0 +1,3 @@
1
+ require 'mkmf'
2
+ extension_name = 'yencoded'
3
+ create_makefile('sanguinews/' + extension_name)
@@ -0,0 +1,89 @@
1
+ //////////////////////////////////////////////////////////////////////////
2
+ // Yencoded - C code to yencode bindary data for sanguinews
3
+ // Copyright (c) 2013-2014, 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
+ #include <ruby.h>
19
+ #include <yencoded.h>
20
+
21
+ void Init_yencoded() {
22
+ Yencoded = rb_define_module("Yencoded");
23
+ YencodedData = rb_define_module_under(Yencoded, "Data");
24
+ rb_define_singleton_method(YencodedData, "yenc", method_yencoded_data_yenc, 2);
25
+ }
26
+
27
+ VALUE method_yencoded_data_yenc(VALUE self, VALUE data,VALUE length) {
28
+ char *bindata;
29
+ long datalen;
30
+ int i=0;
31
+ int linelen=128;
32
+ long restlen;
33
+ long destlen;
34
+ unsigned char c;
35
+ unsigned char *output;
36
+ unsigned char *start;
37
+ VALUE result;
38
+
39
+ // convert ruby variables to c variables
40
+ bindata = StringValuePtr(data);
41
+ datalen = FIX2LONG(length);
42
+
43
+ restlen = datalen; //restlen is our byte processing counter
44
+ destlen = restlen; //will be needing this for memory allocation
45
+ output = (unsigned char*)malloc(2 * destlen * sizeof(char));
46
+ start = output; //starting address will be stored here
47
+ while (restlen>0) {
48
+ c=(unsigned char) *bindata; //get byte
49
+ c=c+42; //add 42 as per yenc specs
50
+ bindata++; restlen--;
51
+ switch(c) { //special characters
52
+ case 0:
53
+ case 10:
54
+ case 13:
55
+ case 61:
56
+ destlen++; i++; //we need more memory than expected
57
+ *output=61; output++; //add escape char to output
58
+ c = c+64;
59
+ break;
60
+ case 9: //escape tab and space if the are first or last on the line
61
+ case 32:
62
+ if ((i==0)||(i==linelen-1)) {
63
+ destlen++; i++; //we need more memory than expected
64
+ *output=61; output++; //add escape char to output
65
+ c = c+64;
66
+ }
67
+ break;
68
+ case 46: //escape dot if it's in a first column
69
+ if (i==0) {
70
+ destlen++; i++; //we need more memory than expected
71
+ *output=61; output++; //add escape char to output
72
+ c = c+64;
73
+ }
74
+ break;
75
+ }
76
+ *output=c; output++; i++;
77
+ if ((i>=linelen)||(restlen==0)){
78
+ destlen++; destlen++;
79
+ *output=13; output++; //according to yenc specs we must use windows style line breaks
80
+ *output=10; output++;
81
+ i=0;
82
+ }
83
+ }
84
+ *output=0; //NULL termination is required
85
+ destlen++;
86
+ result=rb_str_new2((char*)start);
87
+ free(start);
88
+ return result;
89
+ }
@@ -0,0 +1,6 @@
1
+ VALUE Yencoded = Qnil;
2
+ VALUE YencodedData = Qnil;
3
+
4
+ void Init_yencoded();
5
+ VALUE method_yencoded_data_yenc(VALUE self, VALUE data, VALUE length);
6
+
data/lib/sanguinews.rb ADDED
@@ -0,0 +1,415 @@
1
+ ########################################################################
2
+ # sanguinews - usenet command line binary poster written in ruby
3
+ # Copyright (c) 2013, Tadeus Dobrovolskij
4
+ # This program 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 program 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 program; if not, write to the Free Software Foundation, Inc.,
16
+ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17
+ ########################################################################
18
+
19
+ require 'rubygems'
20
+ require 'bundler/setup'
21
+ require 'optparse'
22
+ require 'monitor'
23
+ require 'date'
24
+ require 'tempfile'
25
+ # Following non-standard gems are needed
26
+ require 'parseconfig'
27
+ require 'speedometer'
28
+ # Our library
29
+ require_relative 'sanguinews/thread-pool'
30
+ require_relative 'sanguinews/nntp'
31
+ require_relative 'sanguinews/nntp_msg'
32
+ require_relative 'sanguinews/file_to_upload'
33
+ require_relative 'sanguinews/yencoded'
34
+ require_relative 'sanguinews/version'
35
+
36
+ module Sanguinews
37
+ module_function
38
+ # Method returns yenc encoded string and crc32 value
39
+ def yencode(file, length, queue)
40
+ i = 1
41
+ until file.eof?
42
+ bindata = file.read(length)
43
+ # We can't take all memory, so we wait
44
+ queue.synchronize do
45
+ @cond.wait_while do
46
+ queue.length > @threads * 3
47
+ end
48
+ end
49
+ data = {}
50
+ final_data = []
51
+ len = bindata.length
52
+ data[:yenc] = Yencoded::Data.yenc(bindata, len)
53
+ data[:crc32] = Zlib.crc32(bindata, 0).to_s(16)
54
+ data[:length] = len
55
+ data[:chunk] = i
56
+ data[:file] = file
57
+ final_data[0] = form_message(data)
58
+ final_data[1] = file
59
+ queue.push(final_data)
60
+ i += 1
61
+ end
62
+ end
63
+
64
+ def form_message(data)
65
+ message = data[:yenc]
66
+ length = data[:length]
67
+ pcrc32 = data[:crc32]
68
+ file = data[:file]
69
+ chunk = data[:chunk]
70
+ crc32 = file.crc32
71
+ fsize = file.size
72
+ chunks = file.chunks
73
+ basename = file.name
74
+ # usenet works with ASCII
75
+ subject="#{@prefix}#{file.dir_prefix}\"#{basename}\" yEnc (#{chunk}/#{chunks})"
76
+ msg = NntpMsg.new(@from, @groups, subject)
77
+ msg.poster = "sanguinews v#{Sanguinews::VERSION} (ruby #{RUBY_VERSION}) - https://github.com/tdobrovolskij/sanguinews"
78
+ msg.xna = @xna
79
+ msg.message = message.force_encoding('ASCII-8BIT')
80
+ msg.yenc_body(chunk, chunks, crc32, pcrc32, length, fsize, basename)
81
+ msg = msg.return_self
82
+ { message: msg, filename: basename, chunk: chunk, length: length }
83
+ end
84
+
85
+ def connect(x)
86
+ begin
87
+ nntp = Net::NNTP.start(@server, @port, @username, @password, @mode)
88
+ rescue
89
+ @s.log([$!, $@], stderr: true) if @debug
90
+ if @verbose
91
+ parse_error($!.to_s)
92
+ @s.log("Connection nr. #{x} has failed. Reconnecting...\n", stderr: true)
93
+ end
94
+ sleep @delay
95
+ retry
96
+ end
97
+ return nntp
98
+ end
99
+
100
+ def parse_config(config)
101
+ config = ParseConfig.new(config)
102
+ config.get_params()
103
+ @username = config['username']
104
+ @password = config['password']
105
+ @from = config['from']
106
+ @server = config['server']
107
+ @port = config['port']
108
+ @threads = config['connections'].to_i
109
+ @length = config['article_size'].to_i
110
+ @delay = config['reconnect_delay'].to_i
111
+ @groups = config['groups']
112
+ @prefix = config['prefix']
113
+ ssl = config['ssl']
114
+ if ssl == 'yes'
115
+ @mode = :tls
116
+ else
117
+ @mode = :original
118
+ end
119
+ config['xna'] == 'yes' ? @xna = true : @xna = false
120
+ config['nzb'] == 'yes' ? @nzb = true : @nzb = false
121
+ config['header_check'] == 'yes' ? @header_check = true : @header_check = false
122
+ config['debug'] == 'yes' ? @debug = true : @debug = false
123
+ end
124
+
125
+ def get_msgid(response)
126
+ msgid = ''
127
+ response.each do |r|
128
+ msgid = r.sub(/>.*/, '').tr("<", '') if r.end_with?('Article posted')
129
+ end
130
+ return msgid
131
+ end
132
+
133
+ def parse_options(args)
134
+ # version and legal info presented to user
135
+ banner = []
136
+ banner << ""
137
+ banner << "sanguinews v#{Sanguinews::VERSION}. Copyright (c) 2013-2014 Tadeus Dobrovolskij."
138
+ banner << "Comes with ABSOLUTELY NO WARRANTY. Distributed under GPL v2 license(http://www.gnu.org/licenses/gpl-2.0.txt)."
139
+ banner << "sanguinews is a simple nntp(usenet) binary poster. It supports multithreading and SSL. More info in README."
140
+ banner << ""
141
+ # option parser
142
+ options = {}
143
+ options[:filemode] = false
144
+ options[:files] = []
145
+
146
+ opt_parser = OptionParser.new do |opt|
147
+ opt.banner = "Usage: #{$0} [OPTIONS] [DIRECTORY] | -f FILE1..[FILEX]"
148
+ opt.separator ""
149
+ opt.separator "Options"
150
+
151
+ opt.on("-c", "--config CONFIG", "use different config file") do |cfg|
152
+ options[:config] = cfg
153
+ end
154
+ opt.on("-C", "--check", "check headers while uploading; slow but reliable") do
155
+ options[:header_check] = true
156
+ end
157
+ opt.on("-f", "--file FILE", "upload FILE, treat all additional parameters as files") do |file|
158
+ options[:filemode] = true
159
+ options[:files] << file
160
+ end
161
+ opt.on("-g", "--groups GROUP_LIST", "use these groups(comma separated) for upload") do |group_list|
162
+ options[:groups] = group_list
163
+ end
164
+ opt.on("-h", "--help", "help") do
165
+ banner.each do |msg|
166
+ puts msg
167
+ end
168
+ puts opt_parser
169
+ puts
170
+ exit
171
+ end
172
+ opt.on("-p", "--password PASSWORD", "use PASSWORD as your password(overwrites config file)") do |password|
173
+ options[:password] = password
174
+ end
175
+ opt.on("-u", "--user USERNAME", "use USERNAME as your username(overwrites config file)") do |username|
176
+ options[:username] = username
177
+ end
178
+ opt.on("-v", "--verbose", "be verbose?") do
179
+ options[:verbose] = true
180
+ end
181
+ end
182
+
183
+ begin
184
+ opt_parser.parse!(args)
185
+ rescue OptionParser::InvalidOption, OptionParser::MissingArgument
186
+ puts opt_parser
187
+ exit 1
188
+ end
189
+
190
+ options[:directory] = args[0] unless options[:filemode]
191
+
192
+ # in file mode treat every additional parameter as a file
193
+ if !args.empty? && options[:filemode]
194
+ args.each do |file|
195
+ options[:files] << file.to_s
196
+ end
197
+ end
198
+
199
+ # exit when no file list is provided
200
+ if options[:directory].nil? && options[:files].empty?
201
+ puts "You need to specify something to upload!"
202
+ puts opt_parser
203
+ exit 1
204
+ end
205
+
206
+ return options
207
+ end
208
+
209
+ def parse_error(msg, **info)
210
+ if info[:file].nil? || info[:chunk].nil?
211
+ fileinfo = ''
212
+ else
213
+ fileinfo = '(' + info[:file] + ' / Chunk: ' + info[:chunk].to_s + ')'
214
+ end
215
+
216
+ case
217
+ when /\A411/ === msg
218
+ @s.log("Invalid newsgroup specified.", stderr: true)
219
+ when /\A430/ === msg
220
+ @s.log("No such article. Maybe server is lagging...#{fileinfo}", stderr: true)
221
+ when /\A(4\d{2}\s)?437/ === msg
222
+ @s.log("Article rejected by server. Maybe it's too big.#{fileinfo}", stderr: true)
223
+ when /\A440/ === msg
224
+ @s.log("Posting not allowed.", stderr: true)
225
+ when /\A441/ === msg
226
+ @s.log("Posting failed for some reason.#{fileinfo}", stderr: true)
227
+ when /\A450/ === msg
228
+ @s.log("Not authorized.", stderr: true)
229
+ when /\A452/ === msg
230
+ @s.log("Wrong username and/or password.", stderr: true)
231
+ when /\A500/ === msg
232
+ @s.log("Command not recognized.", stderr: true)
233
+ when /\A501/ === msg
234
+ @s.log("Command syntax error.", stderr: true)
235
+ when /\A502/ === msg
236
+ @s.log("Access denied.", stderr: true)
237
+ end
238
+ end
239
+
240
+ def process_and_upload(queue, nntp_pool, info_lock, informed)
241
+ stuff = queue.pop
242
+ queue.synchronize do
243
+ @cond.signal
244
+ end
245
+ nntp = nntp_pool.pop
246
+
247
+ data = stuff[0]
248
+ file = stuff[1]
249
+ msg = data[:message]
250
+ chunk = data[:chunk]
251
+ basename = data[:filename]
252
+ length = data[:length]
253
+ full_size = msg.length
254
+ info_lock.synchronize do
255
+ if !informed[basename.to_sym]
256
+ @s.log("Uploading #{basename}\n")
257
+ @s.log(file.subject + "\n")
258
+ @s.log("Chunks: #{file.chunks}\n", stderr: true) if @verbose
259
+ informed[basename.to_sym] = true
260
+ end
261
+ end
262
+
263
+ @s.start
264
+ x = 1
265
+ begin
266
+ response = nntp.post msg
267
+ msgid = get_msgid(response)
268
+ if @header_check
269
+ sleep x
270
+ nntp.stat("<#{msgid}>")
271
+ end
272
+ rescue
273
+ @s.log([$!, $@], stderr: true) if @debug
274
+ if @verbose
275
+ parse_error($!.to_s, file: basename, chunk: chunk)
276
+ @s.log("Upload of chunk #{chunk} from file #{basename} unsuccessful. Retrying...\n", stderr: true)
277
+ end
278
+ sleep @delay
279
+ x += 4
280
+ retry
281
+ end
282
+
283
+ if @verbose
284
+ @s.log("Uploaded chunk Nr:#{chunk}\n", stderr: true)
285
+ end
286
+
287
+ @s.done(length)
288
+ @s.uploaded += full_size
289
+ if @nzb
290
+ file.write_segment_info(length, chunk, msgid)
291
+ end
292
+ nntp_pool.push(nntp)
293
+ end
294
+
295
+ def run!
296
+ # Parse options in config file
297
+ config = "~/.sanguinews.conf"
298
+ config = File.expand_path(config)
299
+ # variable to store if config was parsed
300
+ saw_config = false
301
+ if File.exist?(config)
302
+ saw_config = true
303
+ parse_config(config)
304
+ end
305
+
306
+ options = parse_options(ARGV)
307
+
308
+ optconfig = options[:config]
309
+ optconfig = '' if optconfig.nil?
310
+ if !File.exist?(optconfig) && !saw_config
311
+ puts "No config information specified. Aborting..."
312
+ exit
313
+ end
314
+ parse_config(optconfig) if File.exist?(optconfig)
315
+
316
+ options[:verbose] ? @verbose = true : @verbose = false
317
+ @header_check = true unless options[:header_check].nil?
318
+ filemode = options[:filemode]
319
+
320
+ @username = options[:username] unless options[:username].nil?
321
+ @password = options[:password] unless options[:password].nil?
322
+ @groups = options[:groups] unless options[:groups].nil?
323
+ directory = options[:directory] unless filemode
324
+ files = options[:files]
325
+
326
+ # skip hidden files
327
+ if !filemode
328
+ directory = directory + "/" unless directory.end_with?('/')
329
+ Dir.foreach(directory) do |item|
330
+ next if item.start_with?('.')
331
+ files << directory+item
332
+ end
333
+ end
334
+
335
+ # "max" is needed only in dirmode
336
+ max = files.length
337
+ c = 1
338
+
339
+ unprocessed = 0
340
+ info_lock=Mutex.new
341
+ messages = Queue.new
342
+ messages.extend(MonitorMixin)
343
+ @cond = messages.new_cond
344
+ files_to_process = []
345
+ @s = Speedometer.new(units: "KB", progressbar: true)
346
+ @s.uploaded = 0
347
+
348
+ pool = Queue.new
349
+ Thread.new {
350
+ @threads.times do |x|
351
+ nntp = connect(x)
352
+ pool.push(nntp)
353
+ end
354
+ }
355
+
356
+ p = Pool.new(@threads)
357
+ informed = {}
358
+
359
+ files.each do |file|
360
+ next if !File.file?(file)
361
+
362
+ informed[file.to_sym] = false
363
+ file = FileToUpload.new(
364
+ name: file, chunk_length: @length,
365
+ prefix: @prefix, current: c, last: max, filemode: filemode,
366
+ from: @from, groups: @groups, nzb: @nzb
367
+ )
368
+ @s.to_upload += file.size
369
+
370
+ info_lock.synchronize do
371
+ unprocessed += file.chunks
372
+ end
373
+
374
+ files_to_process << file
375
+ c += 1
376
+ end
377
+
378
+ # let's give a little bit higher priority for file processing thread
379
+ @t = Thread.new {
380
+ files_to_process.each do |file|
381
+ @s.log("Calculating CRC32 value for #{file.name}\n", stderr: true) if @verbose
382
+ file.file_crc32
383
+ @s.log("Encoding #{file.name}\n")
384
+ yencode(file, @length, messages)
385
+ end
386
+ }
387
+ @t.priority += 2
388
+
389
+ until unprocessed == 0
390
+ p.schedule do
391
+ process_and_upload(messages, pool, info_lock, informed)
392
+ end
393
+ unprocessed -= 1
394
+ end
395
+
396
+ p.shutdown
397
+
398
+ until pool.empty?
399
+ nntp = pool.pop
400
+ nntp.finish
401
+ end
402
+
403
+ @s.stop
404
+ puts
405
+
406
+ files_to_process.each do |file|
407
+ if files_to_process.last == file
408
+ last = true
409
+ else
410
+ last = false
411
+ end
412
+ file.close(last)
413
+ end
414
+ end
415
+ end