quartz_flow 0.0.1

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