353 lines
14 KiB
Ruby
353 lines
14 KiB
Ruby
module JamRuby
|
|
|
|
# describes what users have rights to which tracks
|
|
class JamTrackRight < ActiveRecord::Base
|
|
include JamRuby::S3ManagerMixin
|
|
|
|
@@log = Logging.logger[JamTrackRight]
|
|
|
|
attr_accessible :user, :jam_track, :user_id, :jam_track_id, :download_count
|
|
attr_accessible :user_id, :jam_track_id, as: :admin
|
|
attr_accessible :url_48, :md5_48, :length_48, :url_44, :md5_44, :length_44
|
|
belongs_to :user, class_name: "JamRuby::User" # the owner, or purchaser of the jam_track
|
|
belongs_to :jam_track, class_name: "JamRuby::JamTrack"
|
|
|
|
validates :user, presence: true
|
|
validates :jam_track, presence: true
|
|
validates :is_test_purchase, inclusion: {in: [true, false]}
|
|
|
|
validate :verify_download_count
|
|
after_save :after_save
|
|
|
|
validates_uniqueness_of :user_id, scope: :jam_track_id
|
|
|
|
# Uploads the JKZ:
|
|
mount_uploader :url_48, JamTrackRightUploader
|
|
mount_uploader :url_44, JamTrackRightUploader
|
|
before_destroy :delete_s3_files
|
|
|
|
MAX_JAM_TRACK_DOWNLOADS = 1000
|
|
|
|
def after_save
|
|
# try to catch major transitions:
|
|
|
|
# if just queue time changes, start time changes, or signed time changes, send out a notice
|
|
if signing_queued_at_was != signing_queued_at || signing_started_at_48_was != signing_started_at_48 || signing_started_at_44_was != signing_started_at_44 || last_signed_at_was != last_signed_at || current_packaging_step != current_packaging_step_was || packaging_steps != packaging_steps_was
|
|
SubscriptionMessage.jam_track_signing_job_change(self)
|
|
end
|
|
end
|
|
|
|
def store_dir
|
|
"jam_track_rights/#{created_at.strftime('%m-%d-%Y')}/#{user_id}-#{id}"
|
|
end
|
|
|
|
# create name of the file
|
|
def filename(bitrate)
|
|
"#{jam_track.name}-#{bitrate == :url_48 ? '48' : '44'}.jkz"
|
|
end
|
|
|
|
def verify_download_count
|
|
if (self.download_count < 0 || self.download_count > MAX_JAM_TRACK_DOWNLOADS) && !@current_user.admin
|
|
errors.add(:download_count, "must be less than or equal to #{MAX_JAM_TRACK_DOWNLOADS}")
|
|
end
|
|
end
|
|
|
|
def self.ready_to_clean
|
|
JamTrackRight.where("downloaded_since_sign=? AND updated_at <= ?", true, 5.minutes.ago).limit(1000)
|
|
end
|
|
|
|
def finish_errored(error_reason, error_detail, sample_rate)
|
|
self.last_signed_at = Time.now
|
|
self.error_count = self.error_count + 1
|
|
self.error_reason = error_reason
|
|
self.error_detail = error_detail
|
|
self.should_retry = self.error_count < 5
|
|
if sample_rate == 48
|
|
self.signing_48 = false
|
|
else
|
|
self.signing_44 = false
|
|
end
|
|
|
|
if save
|
|
Notification.send_jam_track_sign_failed(self)
|
|
else
|
|
raise "Error sending notification #{self.errors}"
|
|
end
|
|
end
|
|
|
|
def finish_sign(length, md5, bitrate)
|
|
self.last_signed_at = Time.now
|
|
if bitrate==48
|
|
self.length_48 = length
|
|
self.md5_48 = md5
|
|
self.signed_48 = true
|
|
self.signing_48 = false
|
|
else
|
|
self.length_44 = length
|
|
self.md5_44 = md5
|
|
self.signed_44 = true
|
|
self.signing_44 = false
|
|
end
|
|
self.error_count = 0
|
|
self.error_reason = nil
|
|
self.error_detail = nil
|
|
self.should_retry = false
|
|
save!
|
|
end
|
|
|
|
# creates a short-lived URL that has access to the object.
|
|
# the idea is that this is used when a user who has the rights to this tries to download this JamTrack
|
|
# we would verify their rights (can_download?), and generates a URL in response to the click so that they can download
|
|
# but the url is short lived enough so that it wouldn't be easily shared
|
|
def sign_url(expiration_time = 120, bitrate=48)
|
|
field_name = (bitrate==48) ? "url_48" : "url_44"
|
|
s3_manager.sign_url(self[field_name], {:expires => expiration_time, :secure => true})
|
|
end
|
|
|
|
def delete_s3_files
|
|
remove_url_48!
|
|
remove_url_44!
|
|
end
|
|
|
|
|
|
def enqueue(sample_rate=48)
|
|
begin
|
|
JamTrackRight.where(:id => self.id).update_all(:signing_queued_at => Time.now, :signing_started_at_44 => nil, :signing_started_at_48 => nil, :last_signed_at => nil)
|
|
Resque.enqueue(JamTracksBuilder, self.id, sample_rate)
|
|
true
|
|
rescue Exception => e
|
|
puts "e: #{e}"
|
|
# implies redis is down. we don't update started_at by bailing out here
|
|
false
|
|
end
|
|
end
|
|
|
|
# if the job is already signed, just queued up for signing, or currently signing, then don't enqueue... otherwise fire it off
|
|
def enqueue_if_needed(sample_rate=48)
|
|
state = signing_state(sample_rate)
|
|
if state == 'SIGNED' || state == 'SIGNING' || state == 'QUEUED'
|
|
false
|
|
else
|
|
enqueue(sample_rate)
|
|
true
|
|
end
|
|
end
|
|
|
|
|
|
# @return true if signed && file exists for the sample_rate specifed:
|
|
def ready?(sample_rate=48)
|
|
if sample_rate==48
|
|
self.signed_48 && self.url_48.present? && self.url_48.file.exists?
|
|
else
|
|
self.signed_44 && self.url_44.present? && self.url_44.file.exists?
|
|
end
|
|
end
|
|
|
|
# returns easy to digest state field
|
|
# SIGNED - the package is ready to be downloaded
|
|
# ERROR - the package was built unsuccessfully
|
|
# SIGNING_TIMEOUT - the package was kicked off to be signed, but it seems to have hung
|
|
# SIGNING - the package is currently signing
|
|
# QUEUED_TIMEOUT - the package signing job (JamTrackBuilder) was queued, but never executed
|
|
# QUEUED - the package is queued to sign
|
|
# QUIET - the jam_track_right exists, but no job has been kicked off; a job needs to be enqueued
|
|
def signing_state(sample_rate = nil)
|
|
state = nil
|
|
|
|
# if the caller did not specified sample rate, we will determine what signing state to check by looking at the most recent signing attempt
|
|
if sample_rate.nil?
|
|
# determine what package is being signed by checking the most recent signing_started at
|
|
time_48 = signing_started_at_48.to_i
|
|
time_44 = signing_started_at_44.to_i
|
|
sample_rate = time_48 > time_44 ? 48 : 44
|
|
end
|
|
|
|
signed = sample_rate == 48 ? signed_48 : signed_44
|
|
signing_started_at = sample_rate == 48 ? signing_started_at_48 : signing_started_at_44
|
|
|
|
if signed
|
|
state = 'SIGNED'
|
|
elsif signing_started_at
|
|
# the maximum amount of time the packaging job can take is 10 seconds * num steps. For a 10 track song, this will be 110 seconds. It's a bit long.
|
|
signing_job_run_max_time = packaging_steps * 10
|
|
if Time.now - signing_started_at > signing_job_run_max_time
|
|
state = 'SIGNING_TIMEOUT'
|
|
elsif Time.now - last_step_at > APP_CONFIG.signing_step_max_time
|
|
state = 'SIGNING_TIMEOUT'
|
|
else
|
|
state = 'SIGNING'
|
|
end
|
|
elsif signing_queued_at
|
|
if Time.now - signing_queued_at > APP_CONFIG.signing_job_queue_max_time
|
|
state = 'QUEUED_TIMEOUT'
|
|
else
|
|
state = 'QUEUED'
|
|
end
|
|
elsif error_count > 0
|
|
state = 'ERROR'
|
|
else
|
|
state = 'QUIET' # needs to be poked to go build
|
|
end
|
|
state
|
|
end
|
|
|
|
def signed?(sample_rate)
|
|
sample_rate == 48 ? signed_48 : signed_44
|
|
end
|
|
|
|
def update_download_count(count=1)
|
|
self.download_count = self.download_count + count
|
|
self.last_downloaded_at = Time.now
|
|
|
|
if self.signed_44 || self.signed_48
|
|
self.downloaded_since_sign = true
|
|
end
|
|
end
|
|
|
|
def self.list_keys(user, jamtracks)
|
|
if jamtracks.nil?
|
|
return []
|
|
end
|
|
|
|
JamTrack.select('jam_tracks.id, jam_track_rights.private_key_44 AS private_key_44, jam_track_rights.private_key_48 AS private_key_48, jam_track_rights.id AS jam_track_right_id')
|
|
.joins("LEFT OUTER JOIN jam_track_rights ON jam_tracks.id = jam_track_rights.jam_track_id AND jam_track_rights.user_id = '#{user.id}'")
|
|
.where('jam_tracks.id IN (?)', jamtracks)
|
|
end
|
|
|
|
def guard_against_fraud(current_user, fingerprint, remote_ip)
|
|
|
|
if current_user.blank?
|
|
return "no user specified"
|
|
end
|
|
|
|
# admin's get to skip fraud check
|
|
if current_user.admin
|
|
return nil
|
|
end
|
|
|
|
if fingerprint.nil? || fingerprint.empty?
|
|
return "no fingerprint specified"
|
|
end
|
|
|
|
all_fingerprint = fingerprint.delete(:all)
|
|
running_fingerprint = fingerprint.delete(:running)
|
|
|
|
if all_fingerprint.blank?
|
|
return "no all fingerprint specified"
|
|
end
|
|
|
|
if running_fingerprint.blank?
|
|
return "no running fingerprint specified"
|
|
end
|
|
|
|
all_fingerprint_extra = fingerprint[all_fingerprint]
|
|
running_fingerprint_extra = fingerprint[running_fingerprint]
|
|
|
|
if redeemed && !redeemed_and_fingerprinted
|
|
# if this is a free JamTrack, we need to check for fraud or accidental misuse
|
|
|
|
# first of all, does this user have any other JamTracks aside from this one that have already been redeemed it and are marked free?
|
|
other_redeemed_freebie = JamTrackRight.where(redeemed: true).where(redeemed_and_fingerprinted: true).where('id != ?', id).where(user_id: current_user.id).first
|
|
|
|
if other_redeemed_freebie
|
|
return "already redeemed another"
|
|
end
|
|
|
|
if FingerprintWhitelist.select('id').find_by_fingerprint(all_fingerprint)
|
|
# we can short circuit out of the rest of the check, since this is a known bad fingerprint
|
|
@@log.debug("ignoring 'all' hash found in whitelist")
|
|
else
|
|
# can we find a jam track that belongs to someone else with the same fingerprint
|
|
conflict = MachineFingerprint.select('count(id) as count').where('user_id != ?', current_user.id).where(fingerprint: all_fingerprint).where(remote_ip: remote_ip).where('created_at > ?', APP_CONFIG.expire_fingerprint_days.days.ago).first
|
|
conflict_count = conflict['count'].to_i
|
|
|
|
if conflict_count >= APP_CONFIG.found_conflict_count
|
|
mf = MachineFingerprint.create(all_fingerprint, current_user, MachineFingerprint::TAKEN_ON_FRAUD_CONFLICT, MachineFingerprint::PRINT_TYPE_ACTIVE, remote_ip, all_fingerprint_extra, self)
|
|
|
|
# record the alert
|
|
fraud = FraudAlert.create(mf, current_user) if mf.valid?
|
|
fraud_admin_url = fraud.admin_url if fraud
|
|
|
|
|
|
AdminMailer.alerts(subject: "'All' fingerprint collision by #{current_user.name}",
|
|
body: "Current User: #{current_user.admin_url}\n\n Fraud Alert: #{fraud_admin_url}").deliver
|
|
|
|
# try to record the other fingerprint
|
|
mf = MachineFingerprint.create(running_fingerprint, current_user, MachineFingerprint::TAKEN_ON_FRAUD_CONFLICT, MachineFingerprint::PRINT_TYPE_ACTIVE, remote_ip, running_fingerprint_extra, self)
|
|
|
|
if APP_CONFIG.error_on_fraud
|
|
return "other user has 'all' fingerprint"
|
|
else
|
|
self.redeemed_and_fingerprinted = true
|
|
save!
|
|
return nil
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
if all_fingerprint != running_fingerprint
|
|
if FingerprintWhitelist.select('id').find_by_fingerprint(running_fingerprint)
|
|
# we can short circuit out of the rest of the check, since this is a known bad fingerprint
|
|
@@log.debug("ignoring 'running' hash found in whitelist")
|
|
else
|
|
|
|
conflict = MachineFingerprint.select('count(id) as count').where('user_id != ?', current_user.id).where(fingerprint: running_fingerprint).where(remote_ip: remote_ip).where('created_at > ?', APP_CONFIG.expire_fingerprint_days.days.ago).first
|
|
conflict_count = conflict['count'].to_i
|
|
if conflict_count >= APP_CONFIG.found_conflict_count
|
|
mf = MachineFingerprint.create(running_fingerprint, current_user, MachineFingerprint::TAKEN_ON_FRAUD_CONFLICT, MachineFingerprint::PRINT_TYPE_ACTIVE, remote_ip, running_fingerprint_extra, self)
|
|
|
|
# record the alert
|
|
fraud = FraudAlert.create(mf, current_user) if mf.valid?
|
|
fraud_admin_url = fraud.admin_url if fraud
|
|
AdminMailer.alerts(subject: "'Running' fingerprint collision by #{current_user.name}",
|
|
body: "Current User: #{current_user.admin_url}\n\nFraud Alert: #{fraud_admin_url}").deliver\
|
|
|
|
# try to record the other fingerprint
|
|
mf = MachineFingerprint.create(all_fingerprint, current_user, MachineFingerprint::TAKEN_ON_FRAUD_CONFLICT, MachineFingerprint::PRINT_TYPE_ALL, remote_ip, all_fingerprint_extra, self)
|
|
|
|
|
|
if APP_CONFIG.error_on_fraud
|
|
return "other user has 'running' fingerprint"
|
|
else
|
|
self.redeemed_and_fingerprinted = true
|
|
save!
|
|
return nil
|
|
end
|
|
end
|
|
end
|
|
|
|
end
|
|
|
|
# we made it past all checks; let's slap on the redeemed_fingerprint
|
|
self.redeemed_and_fingerprinted = true
|
|
|
|
MachineFingerprint.create(all_fingerprint, current_user, MachineFingerprint::TAKEN_ON_SUCCESSFUL_DOWNLOAD, MachineFingerprint::PRINT_TYPE_ALL, remote_ip, all_fingerprint_extra, self)
|
|
if all_fingerprint != running_fingerprint
|
|
MachineFingerprint.create(running_fingerprint, current_user, MachineFingerprint::TAKEN_ON_SUCCESSFUL_DOWNLOAD, MachineFingerprint::PRINT_TYPE_ACTIVE, remote_ip, running_fingerprint_extra, self)
|
|
end
|
|
|
|
save!
|
|
end
|
|
|
|
|
|
nil
|
|
end
|
|
|
|
def self.stats
|
|
stats = {}
|
|
|
|
result = JamTrackRight.select('count(id) as total, count(CASE WHEN signing_44 THEN 1 ELSE NULL END) + count(CASE WHEN signing_48 THEN 1 ELSE NULL END) as signing_count, count(CASE WHEN redeemed THEN 1 ELSE NULL END) as redeem_count, count(last_downloaded_at) as redeemed_and_dl_count').where(is_test_purchase: false).first
|
|
|
|
stats['count'] = result['total'].to_i
|
|
stats['signing_count'] = result['signing_count'].to_i
|
|
stats['redeemed_count'] = result['redeem_count'].to_i
|
|
stats['redeemed_and_dl_count'] = result['redeemed_and_dl_count'].to_i
|
|
stats['purchased_count'] = stats['count'] - stats['redeemed_count']
|
|
stats
|
|
end
|
|
end
|
|
end
|