quartz_flow 0.0.1

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.
data/bin/createdb.rb ADDED
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env ruby
2
+ require 'fileutils'
3
+ require 'quartz_flow/model'
4
+
5
+ DataMapper::Logger.new($stdout, :debug)
6
+
7
+ $settings = {}
8
+ def set(sym, value)
9
+ puts "Set called: #{sym}=#{value}"
10
+ $settings[sym] = value
11
+ end
12
+
13
+ require './etc/quartz'
14
+
15
+ dbPath = "#{Dir.pwd}/#{$settings[:db_file]}"
16
+ path = "sqlite://#{dbPath}"
17
+ DataMapper.setup(:default, path)
18
+
19
+ dir = File.dirname($settings[:db_file])
20
+ FileUtils.mkdir dir if dir.length > 0 && ! File.directory?(dir)
21
+
22
+
23
+ if ! File.exists?(dbPath)
24
+ puts "creating database #{path}"
25
+ DataMapper.auto_migrate!
26
+ else
27
+ puts "upgrading database #{path}"
28
+ DataMapper.auto_upgrade!
29
+ end
30
+
data/bin/quartzflow ADDED
@@ -0,0 +1,177 @@
1
+ #!/usr/bin/env ruby
2
+ require 'getoptlong'
3
+ require 'quartz_torrent'
4
+
5
+ def doSetup
6
+ home = "."
7
+
8
+ opts = GetoptLong.new(
9
+ [ '--homedir', '-d', GetoptLong::REQUIRED_ARGUMENT],
10
+ )
11
+
12
+ opts.each do |opt, arg|
13
+ if opt == '--homedir'
14
+ home = arg
15
+ end
16
+ end
17
+
18
+ if ! File.directory?(home)
19
+ puts "Error: The directory '#{home}' we will setup into doesn't exist."
20
+ exit 1
21
+ end
22
+
23
+ if ! File.owned?(home)
24
+ puts "Error: The directory '#{home}' is not owned by this user. Please change the owner."
25
+ exit 1
26
+ end
27
+
28
+ puts "Initializing new quartzflow home in #{home == "." ? "current directory" : home}"
29
+ Home.new(home).setup
30
+ end
31
+
32
+ def doHelp
33
+ if ARGV.size == 0
34
+ puts "Used to manage or start quartzflow."
35
+ puts "Usage: #{$0} [command] [options]"
36
+ puts ""
37
+ puts "If no command is specified, start is assumed."
38
+ puts "For help with a specific command, use 'help command'. The available commands are:"
39
+ puts " start Start quartzflow"
40
+ puts " setup Setup a new quartzflow home directory"
41
+ puts " adduser Add a user"
42
+ else
43
+ command = ARGV.shift
44
+ if command == "setup"
45
+ puts "Initialize a new quartzflow home directory. This command creates the necessary "
46
+ puts "subdirectories and files needed for quartzflow to run. When run without options, "
47
+ puts "the current directory is set up. The directory should be empty and owned by the "
48
+ puts "current user."
49
+ puts ""
50
+ puts "Options:"
51
+ puts " --homedir DIR, -d DIR Setup DIR instead of the current directory"
52
+ elsif command == "start"
53
+ puts "Start quartzflow. When run without options, the current directory should be a"
54
+ puts "quartzflow home directory, as created using the setup command."
55
+ puts ""
56
+ puts "Options:"
57
+ puts " --noauth, -n Disable user authentication (users won't be "
58
+ puts " prompted for a password)"
59
+ elsif command == "adduser"
60
+ puts "Add a user. This command must be run with the --login option."
61
+ puts ""
62
+ puts "Options:"
63
+ puts " --login LOGIN, -l LOGIN Login name for the user to be added"
64
+ else
65
+ puts "Unknown command"
66
+ end
67
+ end
68
+ end
69
+
70
+ def doAddUser
71
+ isLinux = RUBY_PLATFORM.downcase.include?("linux")
72
+
73
+ opts = GetoptLong.new(
74
+ [ '--login', '-l', GetoptLong::REQUIRED_ARGUMENT],
75
+ )
76
+
77
+ login = nil
78
+ opts.each do |opt, arg|
79
+ if opt == '--login'
80
+ login = arg
81
+ end
82
+ end
83
+
84
+ if ! login
85
+ puts "The --login option is required for the adduser command"
86
+ exit 1
87
+ end
88
+
89
+ system "stty -echo" if isLinux
90
+
91
+ print "Password: "
92
+ $stdout.flush
93
+ passwd1 = $stdin.gets.strip
94
+ puts
95
+ print "Password again: "
96
+ $stdout.flush
97
+ passwd2 = $stdin.gets.strip
98
+ puts
99
+
100
+ system "stty echo" if isLinux
101
+
102
+ if passwd1 != passwd2
103
+ puts "Passwords don't match"
104
+ exit 1
105
+ end
106
+
107
+ auth = Authentication.new("etc/passwd")
108
+ auth.add_account(login, passwd1)
109
+ end
110
+
111
+ def doStart
112
+ QuartzTorrent.initThread("main")
113
+
114
+ opts = GetoptLong.new(
115
+ [ '--noauth', '-n', GetoptLong::NO_ARGUMENT],
116
+ )
117
+
118
+ $useAuthentication = true
119
+ opts.each do |opt, arg|
120
+ if opt == '--noauth'
121
+ $useAuthentication = false
122
+ puts "Warning: running without user authentication. This is not secure"
123
+ end
124
+ end
125
+
126
+ require 'quartz_flow/server'
127
+
128
+ # There is an interaction with Sinatra where we can't register signal handlers until
129
+ # Sinatra has started (and thus performed it's own registration/reset of the handlers).
130
+ # The workaround is to wait for the server to start before registering.
131
+ Thread.new do
132
+ begin
133
+ puts "Starting thread"
134
+ stdout = $stdout
135
+ io = File.open('/dev/pts/0','w')
136
+ until Server.running?
137
+ sleep 1
138
+ end
139
+
140
+ if Signal.list.has_key?('USR1')
141
+ puts "Registering SIGUSR1 handler"
142
+ Signal.trap('SIGUSR1') { QuartzTorrent.logBacktraces($stdout) }
143
+ Signal.trap('SIGQUIT') { QuartzTorrent.logBacktraces($stdout) }
144
+ end
145
+ rescue
146
+ puts $!
147
+ puts $!.backtrace.join "\n"
148
+ end
149
+ end
150
+
151
+ Server.run!
152
+ end
153
+
154
+ command = :start
155
+
156
+ if ARGV.size > 0
157
+ command = ARGV.shift.to_sym
158
+ end
159
+
160
+ if command == :setup
161
+ # home is slow to load since it loads the model and calls DataMapper.finalize, so we require it only when needed
162
+ require 'quartz_flow/home'
163
+ doSetup
164
+ elsif command == :start
165
+ # home is slow to load since it loads the model and calls DataMapper.finalize, so we require it only when needed
166
+ require 'quartz_flow/home'
167
+ exit 1 if ! Home.new.validate
168
+
169
+ doStart
170
+ elsif command == :help
171
+ doHelp
172
+ elsif command == :adduser
173
+ require 'quartz_flow/authentication'
174
+ doAddUser
175
+ else
176
+ puts "Unknown command #{command}"
177
+ end
data/etc/logging.rb ADDED
@@ -0,0 +1,12 @@
1
+ # Configure logging levels. Default level for all loggers is :info.
2
+
3
+ #set "peerclient", :debug
4
+ #set "peer_manager", :debug
5
+ #set "tracker_client", :debug
6
+ #set "http_tracker_client", :debug
7
+ #set "udp_tracker_client", :debug
8
+ #set "peerclient", :debug
9
+ #set "peerclient.reactor", :debug
10
+ #set "blockstate", :debug
11
+ #set "piecemanager", :debug
12
+ #set "peerholder", :debug
data/etc/quartz.rb ADDED
@@ -0,0 +1,25 @@
1
+ # Quartzflow config file.
2
+
3
+ # IP address that the web server should bind to
4
+ set :bind, "0.0.0.0"
5
+
6
+ # TCP port that the web server should bind to
7
+ set :port, 4444
8
+
9
+ # Directory where downloaded torrent data will be stored
10
+ set :basedir, "download"
11
+
12
+ # Directory where .torrent files and .info files will be stored.
13
+ set :metadir, "meta"
14
+
15
+ # TCP port used for torrents
16
+ set :torrent_port, 9997
17
+
18
+ # SQLite database used for storing settings and state
19
+ set :db_file, "db/quartz.sqlite"
20
+
21
+ # Where to log torrent protocol messages
22
+ set :torrent_log, "log/torrent.log"
23
+
24
+ # On which day of the month should monthly usage tracking reset
25
+ set :monthly_usage_reset_day, 5
@@ -0,0 +1,88 @@
1
+ require 'digest'
2
+ require 'fileutils'
3
+ require 'quartz_flow/randstring'
4
+
5
+ class AccountInfo
6
+ def initialize(login = nil, password_hash = nil, salt = nil)
7
+ @login = login
8
+ @password_hash = password_hash
9
+ @salt = salt
10
+ end
11
+ attr_accessor :login
12
+ attr_accessor :password_hash
13
+ attr_accessor :salt
14
+ end
15
+
16
+
17
+ class Authentication
18
+ def initialize(password_file)
19
+ @password_file = password_file
20
+ @accounts = {}
21
+ load_password_file(password_file)
22
+ end
23
+
24
+ def add_account(login, unhashed_password)
25
+ if @accounts.has_key?login
26
+ raise "The account #{login} already exists"
27
+ end
28
+ raise "Password cannot be empty" if unhashed_password.nil?
29
+ add_account_internal(login, unhashed_password)
30
+ end
31
+
32
+ def del_account(login)
33
+ if ! @accounts.has_key?(login)
34
+ raise "The account #{login} does not exist"
35
+ end
36
+ del_account_internal(login)
37
+ end
38
+
39
+ # Returns true on success, false if the user cannot be authenticated
40
+ def authenticate(login, password)
41
+ # Reload the password file in case users were added/deleted
42
+ acct = @accounts[login]
43
+ return false if ! acct
44
+ hashed = hash_password(password, acct.salt)
45
+ hashed == acct.password_hash
46
+ end
47
+
48
+ private
49
+
50
+ def load_password_file(filename)
51
+ if File.exists? filename
52
+ File.open(filename, "r") do |file|
53
+ @accounts.clear
54
+ file.each_line do |line|
55
+ if line =~ /([^:]+):(.*):(.*)/
56
+ @accounts[$1] = AccountInfo.new($1,$2,$3)
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+
63
+ def add_account_internal(login, unhashed_password)
64
+ salt = RandString.make_random_string(10)
65
+ acct = AccountInfo.new(login, hash_password(unhashed_password, salt), salt)
66
+ File.open(@password_file, "a") do |file|
67
+ file.puts "#{login}:#{acct.password_hash}:#{salt}"
68
+ end
69
+ @accounts[login] = acct
70
+ end
71
+
72
+ def hash_password(pass, salt)
73
+ Digest::SHA256.hexdigest(pass + salt)
74
+ end
75
+
76
+ def del_account_internal(login)
77
+ tmpfile = "#{@password_file}.new"
78
+ File.open(tmpfile, "w") do |outfile|
79
+ File.open(@password_file, "r") do |infile|
80
+ infile.each_line do |line|
81
+ outfile.print line if line !~ /^#{login}:/
82
+ end
83
+ end
84
+ end
85
+ FileUtils.mv tmpfile, @password_file
86
+ end
87
+
88
+ end
@@ -0,0 +1,76 @@
1
+ require 'fileutils'
2
+ require 'quartz_flow/model'
3
+
4
+ # Class used to setup a new QuartzFlow home directory, and get information about it.
5
+ class Home
6
+ def initialize(dir = ".")
7
+ @dir = dir
8
+
9
+ @subdirs = [
10
+ "etc",
11
+ "log",
12
+ "download",
13
+ "meta",
14
+ "public",
15
+ "db",
16
+ "views",
17
+ ]
18
+
19
+ @installRoot = Home.determineAppRoot("quartz_flow")
20
+ end
21
+
22
+ def validate
23
+ rc = true
24
+ @subdirs.each do |subdir|
25
+ path = File.join(@dir, subdir)
26
+ if ! File.directory?(path)
27
+ puts "Error: The home directory is invalid: the subdirectory #{subdir} doesn't exist under the home directory. Was the setup command run?"
28
+ rc = false
29
+ break
30
+ end
31
+ end
32
+ rc
33
+ end
34
+
35
+ def setup
36
+ @subdirs.each do |subdir|
37
+ if File.directory?(subdir)
38
+ puts "Directory #{subdir} already exists. Skipping creation."
39
+ else
40
+ installedPath = @installRoot + File::SEPARATOR + subdir
41
+ if File.directory? installedPath
42
+ FileUtils.cp_r installedPath, @dir
43
+ puts "Copying #{subdir}"
44
+ else
45
+ FileUtils.mkdir @dir + File::SEPARATOR + subdir
46
+ puts "Creating #{subdir}"
47
+ end
48
+ end
49
+ end
50
+
51
+ # Create database
52
+ dbFile = "#{File.expand_path(@dir)}/db/quartz.sqlite"
53
+ if ! File.exists?(dbFile)
54
+ puts "Creating database file"
55
+ path = "sqlite://#{File.expand_path(@dir)}/db/quartz.sqlite"
56
+ DataMapper.setup(:default, path)
57
+ DataMapper.auto_migrate!
58
+ else
59
+ puts "Database file already exists. Running upgrade."
60
+ path = "sqlite://#{File.expand_path(@dir)}/db/quartz.sqlite"
61
+ DataMapper.setup(:default, path)
62
+ DataMapper.auto_upgrade!
63
+ end
64
+
65
+ end
66
+
67
+ def self.determineAppRoot(gemname)
68
+ # Are we running as a Gem, or from the source directory?
69
+ if Gem.loaded_specs[gemname]
70
+ Gem.loaded_specs[gemname].full_gem_path
71
+ else
72
+ "."
73
+ end
74
+ end
75
+
76
+ end
@@ -0,0 +1,247 @@
1
+ require 'quartz_torrent/magnet'
2
+
3
+ module BEncode
4
+ class DecodeError
5
+ end
6
+ end
7
+
8
+
9
+ module QuartzTorrent
10
+ class Words
11
+ def initialize
12
+ @words = []
13
+ File.open "/usr/share/dict/words", "r" do |file|
14
+ file.each_line do |l|
15
+ @words.push l.chomp
16
+ end
17
+ end
18
+ end
19
+
20
+ def randomWord
21
+ @words[rand(@words.size)]
22
+ end
23
+ end
24
+
25
+ class Metainfo
26
+
27
+ def self.createFromFile(file)
28
+ end
29
+
30
+ class FileInfo
31
+ def initialize(length = nil, path = nil)
32
+ @length = length
33
+ @path = path
34
+ end
35
+
36
+ # Relative path to the file. For a single-file torrent this is simply the name of the file. For a multi-file torrent,
37
+ # this is the directory names from the torrent and the filename separated by the file separator.
38
+ attr_accessor :path
39
+ # Length of the file.
40
+ attr_accessor :length
41
+ end
42
+
43
+ class Info
44
+ def initialize
45
+ @files = []
46
+ @name = nil
47
+ @pieceLen = 0
48
+ @pieces = []
49
+ @private = false
50
+ end
51
+
52
+ # Array of FileInfo objects
53
+ attr_accessor :files
54
+ # Suggested file or directory name
55
+ attr_accessor :name
56
+ # Length of each piece in bytes. The last piece may be shorter than this.
57
+ attr_accessor :pieceLen
58
+ # Array of SHA1 digests of all peices. These digests are in binary format.
59
+ attr_accessor :pieces
60
+ # True if no external peer source is allowed.
61
+ attr_accessor :private
62
+
63
+ # Total length of the torrent data in bytes.
64
+ def dataLength
65
+ files.reduce(0){ |memo,f| memo + f.length}
66
+ end
67
+ end
68
+ end
69
+
70
+ class TrackerPeer
71
+ def initialize(ip, port, id = nil)
72
+ @ip = ip
73
+ @port = port
74
+ @id = id
75
+ end
76
+
77
+ attr_accessor :ip
78
+ attr_accessor :port
79
+ attr_accessor :id
80
+ end
81
+
82
+ class Peer
83
+ def initialize(trackerPeer)
84
+ @trackerPeer = trackerPeer
85
+ @amChoked = true
86
+ @amInterested = false
87
+ @peerChoked = true
88
+ @peerInterested = false
89
+ @infoHash = nil
90
+ @state = :disconnected
91
+ @uploadRate = 0
92
+ @downloadRate = 0
93
+ @uploadRateDataOnly = 0
94
+ @downloadRateDataOnly = 0
95
+ @bitfield = nil
96
+ @firstEstablishTime = nil
97
+ @isUs = false
98
+ @requestedBlocks = {}
99
+ @requestedBlocksSizeLastPass = nil
100
+ @maxRequestedBlocks = 50
101
+ end
102
+
103
+ attr_accessor :trackerPeer
104
+ attr_accessor :amChoked
105
+ attr_accessor :amInterested
106
+ attr_accessor :peerChoked
107
+ attr_accessor :peerInterested
108
+ attr_accessor :infoHash
109
+ attr_accessor :firstEstablishTime
110
+ attr_accessor :maxRequestedBlocks
111
+ attr_accessor :state
112
+ attr_accessor :isUs
113
+ attr_accessor :uploadRate
114
+ attr_accessor :downloadRate
115
+ attr_accessor :uploadRateDataOnly
116
+ attr_accessor :downloadRateDataOnly
117
+ attr_accessor :bitfield
118
+ attr_accessor :requestedBlocks
119
+ attr_accessor :requestedBlocksSizeLastPass
120
+ attr_accessor :peerMsgSerializer
121
+ end
122
+
123
+ class TorrentDataDelegate
124
+ # Create a new TorrentDataDelegate. This is meant to only be called internally.
125
+ def initialize
126
+ @info = nil
127
+ @infoHash = nil
128
+ @recommendedName = nil
129
+ @downloadRate = nil
130
+ @uploadRate = nil
131
+ @downloadRateDataOnly = nil
132
+ @uploadRateDataOnly = nil
133
+ @completedBytes = nil
134
+ @peers = nil
135
+ @state = nil
136
+ @completePieceBitfield = nil
137
+ @metainfoLength = nil
138
+ @metainfoCompletedLength = nil
139
+ @paused = nil
140
+ end
141
+
142
+ # Torrent Metainfo.info struct. This is nil if the torrent has no metadata and we haven't downloaded it yet
143
+ # (i.e. a magnet link).
144
+ attr_accessor :info
145
+ attr_accessor :infoHash
146
+ # Recommended display name for this torrent.
147
+ attr_accessor :recommendedName
148
+ attr_accessor :downloadRate
149
+ attr_accessor :uploadRate
150
+ attr_accessor :downloadRateDataOnly
151
+ attr_accessor :uploadRateDataOnly
152
+ attr_accessor :completedBytes
153
+ attr_accessor :peers
154
+ # State of the torrent. This may be one of :downloading_metainfo, :error, :checking_pieces, :running, :downloading_metainfo, or :deleted.
155
+ # The :deleted state indicates that the torrent that this TorrentDataDelegate refers to is no longer being managed by the peer client.
156
+ attr_accessor :state
157
+ attr_accessor :completePieceBitfield
158
+ # Length of metainfo info in bytes. This is only set when the state is :downloading_metainfo
159
+ attr_accessor :metainfoLength
160
+ # How much of the metainfo info we have downloaded in bytes. This is only set when the state is :downloading_metainfo
161
+ attr_accessor :metainfoCompletedLength
162
+ attr_accessor :paused
163
+ end
164
+
165
+ class PeerClient
166
+ def initialize(ignored)
167
+ @words = Words.new
168
+ @torrents = {}
169
+ 7.times{ addTorrent }
170
+ Thread.new do
171
+ while true
172
+ begin
173
+ @torrents.each{ |k,t| t.downloadRate += (rand(1000) - 500)}
174
+ rescue
175
+ puts "Exception in Mock PeerClient: #{$!}"
176
+ end
177
+ sleep 2
178
+ end
179
+ end
180
+ end
181
+
182
+ def port=(p)
183
+ end
184
+
185
+ def start
186
+ end
187
+
188
+ def stop
189
+ end
190
+
191
+ def addTorrentByMetainfo(info)
192
+ end
193
+
194
+ def torrentData(infoHash = nil)
195
+ @torrents
196
+ end
197
+
198
+ def addTorrent
199
+ d = makeFakeTorrentDelegate
200
+ @torrents[d.infoHash] = d
201
+ end
202
+
203
+ private
204
+ def makeFakeTorrentDelegate
205
+ result = TorrentDataDelegate.new
206
+ name = ""
207
+ (rand(3)+1).times do
208
+ name << @words.randomWord + " "
209
+ end
210
+ result.recommendedName = name
211
+ result.infoHash = randomBinaryData 20
212
+ result.downloadRate = 100*1024
213
+ result.uploadRate = 5*1024
214
+ result.downloadRateDataOnly = 100*1024
215
+ result.uploadRateDataOnly = 5*1024
216
+ result.completedBytes = 800
217
+ result.peers = []
218
+ result.state = :running
219
+ result.paused = false
220
+
221
+ result.info = Metainfo::Info.new
222
+ result.info.files = [Metainfo::FileInfo.new(1000, "file1"), Metainfo::FileInfo.new(1000, "file2")]
223
+
224
+ 3.times do
225
+
226
+ peer = Peer.new(TrackerPeer.new("176.23.54.201", 5000))
227
+ peer.amChoked = false
228
+ peer.amInterested = false
229
+ peer.peerChoked = false
230
+ peer.peerInterested = true
231
+ peer.uploadRate = 20*1024
232
+ peer.downloadRate = 10*1024
233
+
234
+ result.peers.push peer
235
+ end
236
+
237
+ result
238
+ end
239
+
240
+ def randomBinaryData(len)
241
+ result = ""
242
+ len.times{ result << rand(256) }
243
+ result
244
+ end
245
+ end
246
+ end
247
+
@@ -0,0 +1,27 @@
1
+ require 'data_mapper'
2
+
3
+ class Setting
4
+ include DataMapper::Resource
5
+
6
+ property :id, Serial
7
+ property :name, String
8
+ property :value, String
9
+ property :scope, Enum[:global, :user, :torrent]
10
+ # For settings that are not global, owner identifies who they apply to.
11
+ # For user settings, this is the user. For torrent settings this is the torrent infohash in hex ascii
12
+ property :owner, String
13
+ end
14
+
15
+ class UsageBucket
16
+ include DataMapper::Resource
17
+
18
+ property :id, Serial
19
+ property :type, Enum[:daily, :monthly]
20
+ property :index, Integer
21
+ property :label, String
22
+ property :criteriaData, Time
23
+ property :absoluteUsage, Integer
24
+ property :value, Integer
25
+ end
26
+
27
+ DataMapper.finalize
@@ -0,0 +1,10 @@
1
+ class RandString
2
+ SALT_CHARS = %w{a b c d e f g h i j k l m n o p q r s t u v w x y z A B C D E F G H I J K L M N O P Q R S T U V W X Y Z 1 2 3 4 5 6 7 8 9 0 _ % $ @ ! " ' . , < > }
3
+
4
+ def self.make_random_string(len)
5
+ rc = ""
6
+ len.times{ rc << SALT_CHARS[rand(SALT_CHARS.size)] }
7
+ rc
8
+ end
9
+ end
10
+