sanguinews 0.60

Sign up to get free protection for your applications and to get access to all the features.
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