2014-11-04 20:55:12 +00:00
module JamRuby
# describes what users have rights to which tracks
class JamTrackRight < ActiveRecord :: Base
2014-12-16 00:02:51 +00:00
include JamRuby :: S3ManagerMixin
2015-05-15 17:34:35 +00:00
@@log = Logging . logger [ JamTrackRight ]
2015-05-18 15:33:58 +00:00
attr_accessible :user , :jam_track , :user_id , :jam_track_id , :download_count
2015-02-25 15:41:51 +00:00
attr_accessible :user_id , :jam_track_id , as : :admin
2015-02-25 23:19:03 +00:00
attr_accessible :url_48 , :md5_48 , :length_48 , :url_44 , :md5_44 , :length_44
2015-05-18 15:33:58 +00:00
belongs_to :user , class_name : " JamRuby::User " # the owner, or purchaser of the jam_track
2014-11-04 20:55:12 +00:00
belongs_to :jam_track , class_name : " JamRuby::JamTrack "
2015-09-22 01:01:39 +00:00
belongs_to :last_mixdown , class_name : 'JamRuby::JamTrackMixdown' , foreign_key : 'last_mixdown_id' , inverse_of : :jam_track_right
2014-11-04 20:55:12 +00:00
2015-05-18 15:33:58 +00:00
validates :user , presence : true
validates :jam_track , presence : true
validates :is_test_purchase , inclusion : { in : [ true , false ] }
2015-03-20 22:26:56 +00:00
2014-12-15 00:18:04 +00:00
validate :verify_download_count
2015-01-09 17:23:20 +00:00
after_save :after_save
2014-11-04 20:55:12 +00:00
2015-05-18 15:33:58 +00:00
validates_uniqueness_of :user_id , scope : :jam_track_id
2014-12-15 01:42:38 +00:00
# Uploads the JKZ:
2015-02-25 23:19:03 +00:00
mount_uploader :url_48 , JamTrackRightUploader
mount_uploader :url_44 , JamTrackRightUploader
2014-12-16 02:30:46 +00:00
before_destroy :delete_s3_files
2015-09-22 01:01:39 +00:00
before_create :create_private_keys
2014-12-15 01:42:38 +00:00
2014-12-15 00:18:04 +00:00
MAX_JAM_TRACK_DOWNLOADS = 1000
2014-12-16 00:02:51 +00:00
2015-09-22 01:01:39 +00:00
def create_private_keys
rsa_key = OpenSSL :: PKey :: RSA . new ( 1024 )
key = rsa_key . to_pem ( )
self . private_key_44 = key
self . private_key_48 = key
end
2015-01-09 17:23:20 +00:00
def after_save
# try to catch major transitions:
# if just queue time changes, start time changes, or signed time changes, send out a notice
2015-05-01 17:09:18 +00:00
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
2015-01-09 17:23:20 +00:00
SubscriptionMessage . jam_track_signing_job_change ( self )
end
end
2014-12-16 00:02:51 +00:00
def store_dir
2015-03-09 14:44:12 +00:00
" jam_track_rights/ #{ created_at . strftime ( '%m-%d-%Y' ) } / #{ user_id } - #{ id } "
2014-12-16 00:02:51 +00:00
end
# create name of the file
2015-05-01 17:09:18 +00:00
def filename ( bitrate )
" #{ jam_track . name } - #{ bitrate == :url_48 ? '48' : '44' } .jkz "
2014-12-16 00:02:51 +00:00
end
2015-05-18 15:33:58 +00:00
2014-12-15 00:18:04 +00:00
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
2014-12-15 01:42:38 +00:00
2014-12-16 02:30:46 +00:00
def self . ready_to_clean
2015-05-11 21:52:58 +00:00
JamTrackRight . where ( " downloaded_since_sign=? AND updated_at <= ? " , true , 5 . minutes . ago ) . limit ( 1000 )
2014-12-15 01:42:38 +00:00
end
2015-05-07 13:49:15 +00:00
def finish_errored ( error_reason , error_detail , sample_rate )
2015-01-09 17:23:20 +00:00
self . last_signed_at = Time . now
2015-09-22 01:01:39 +00:00
self . queued = false
2015-01-09 17:23:20 +00:00
self . error_count = self . error_count + 1
self . error_reason = error_reason
self . error_detail = error_detail
self . should_retry = self . error_count < 5
2015-05-07 13:49:15 +00:00
if sample_rate == 48
self . signing_48 = false
else
self . signing_44 = false
end
2015-01-09 17:23:20 +00:00
if save
Notification . send_jam_track_sign_failed ( self )
else
raise " Error sending notification #{ self . errors } "
end
end
2015-02-26 23:00:01 +00:00
def finish_sign ( length , md5 , bitrate )
2014-12-22 20:23:05 +00:00
self . last_signed_at = Time . now
2015-09-22 01:01:39 +00:00
self . queued = false
2015-02-26 23:00:01 +00:00
if bitrate == 48
self . length_48 = length
self . md5_48 = md5
2015-05-01 17:09:18 +00:00
self . signed_48 = true
2015-05-07 13:49:15 +00:00
self . signing_48 = false
2015-02-26 23:00:01 +00:00
else
2015-03-09 14:44:12 +00:00
self . length_44 = length
2015-02-26 23:00:01 +00:00
self . md5_44 = md5
2015-05-01 17:09:18 +00:00
self . signed_44 = true
2015-05-07 13:49:15 +00:00
self . signing_44 = false
2015-02-26 23:00:01 +00:00
end
2015-01-09 17:23:20 +00:00
self . error_count = 0
self . error_reason = nil
self . error_detail = nil
self . should_retry = false
2015-05-05 19:36:07 +00:00
save!
2014-12-22 20:23:05 +00:00
end
2014-12-16 00:02:51 +00:00
# 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
2015-09-15 10:23:27 +00:00
def sign_url ( expiration_time = 120 , bitrate = 48 , secure = true )
2015-02-25 23:19:03 +00:00
field_name = ( bitrate == 48 ) ? " url_48 " : " url_44 "
2015-09-15 10:23:27 +00:00
s3_manager . sign_url ( self [ field_name ] , { :expires = > expiration_time , :secure = > secure } )
2014-12-16 00:02:51 +00:00
end
2014-12-16 02:30:46 +00:00
def delete_s3_files
2015-05-18 15:33:58 +00:00
remove_url_48!
remove_url_44!
2014-12-16 02:30:46 +00:00
end
2014-12-17 23:19:36 +00:00
2015-03-12 13:51:52 +00:00
2015-03-09 14:44:12 +00:00
def enqueue ( sample_rate = 48 )
2014-12-17 23:19:36 +00:00
begin
2015-09-22 01:01:39 +00:00
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 , :queued = > true )
2015-03-09 14:44:12 +00:00
Resque . enqueue ( JamTracksBuilder , self . id , sample_rate )
2015-01-09 17:23:20 +00:00
true
2014-12-17 23:19:36 +00:00
rescue Exception = > e
2015-02-26 23:00:01 +00:00
puts " e: #{ e } "
2014-12-17 23:19:36 +00:00
# implies redis is down. we don't update started_at by bailing out here
false
end
2015-01-09 17:23:20 +00:00
end
2014-12-17 23:19:36 +00:00
2015-01-09 17:23:20 +00:00
# if the job is already signed, just queued up for signing, or currently signing, then don't enqueue... otherwise fire it off
2015-03-09 14:44:12 +00:00
def enqueue_if_needed ( sample_rate = 48 )
2015-05-01 17:09:18 +00:00
state = signing_state ( sample_rate )
2015-01-09 17:23:20 +00:00
if state == 'SIGNED' || state == 'SIGNING' || state == 'QUEUED'
false
else
2015-03-09 14:44:12 +00:00
enqueue ( sample_rate )
2015-01-09 17:23:20 +00:00
true
end
end
2015-02-26 23:00:01 +00:00
2015-03-12 13:51:52 +00:00
2015-03-09 14:44:12 +00:00
# @return true if signed && file exists for the sample_rate specifed:
def ready? ( sample_rate = 48 )
if sample_rate == 48
2015-05-01 17:09:18 +00:00
self . signed_48 && self . url_48 . present? && self . url_48 . file . exists?
2015-02-26 23:00:01 +00:00
else
2015-05-01 17:09:18 +00:00
self . signed_44 && self . url_44 . present? && self . url_44 . file . exists?
2015-02-26 23:00:01 +00:00
end
end
2015-01-09 17:23:20 +00:00
# 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
2015-05-01 17:09:18 +00:00
def signing_state ( sample_rate = nil )
2015-01-09 17:23:20 +00:00
state = nil
2015-05-01 17:09:18 +00:00
# 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
2015-01-09 17:23:20 +00:00
if signed
state = 'SIGNED'
elsif signing_started_at
2015-04-16 19:45:29 +00:00
# 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
2015-01-09 17:23:20 +00:00
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
2015-02-27 17:51:45 +00:00
elsif error_count > 0
state = 'ERROR'
2015-01-09 17:23:20 +00:00
else
state = 'QUIET' # needs to be poked to go build
end
state
2014-12-17 23:19:36 +00:00
end
2015-05-01 17:09:18 +00:00
def signed? ( sample_rate )
sample_rate == 48 ? signed_48 : signed_44
end
2014-12-17 23:19:36 +00:00
def update_download_count ( count = 1 )
self . download_count = self . download_count + count
self . last_downloaded_at = Time . now
2015-04-24 19:58:31 +00:00
2015-05-01 17:09:18 +00:00
if self . signed_44 || self . signed_48
2015-04-24 19:58:31 +00:00
self . downloaded_since_sign = true
end
2014-12-17 23:19:36 +00:00
end
2015-01-05 22:53:43 +00:00
def self . list_keys ( user , jamtracks )
if jamtracks . nil?
return [ ]
end
2015-05-01 17:09:18 +00:00
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' )
2015-01-05 22:53:43 +00:00
. 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
2015-05-07 13:49:15 +00:00
2015-05-15 17:34:35 +00:00
def guard_against_fraud ( current_user , fingerprint , remote_ip )
2015-05-16 04:28:46 +00:00
2015-05-15 17:34:35 +00:00
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
2015-05-20 14:24:17 +00:00
all_fingerprint = fingerprint . delete ( :all )
running_fingerprint = fingerprint . delete ( :running )
2015-05-15 17:34:35 +00:00
if all_fingerprint . blank?
return " no all fingerprint specified "
end
if running_fingerprint . blank?
return " no running fingerprint specified "
end
2015-05-20 14:24:17 +00:00
all_fingerprint_extra = fingerprint [ all_fingerprint ]
running_fingerprint_extra = fingerprint [ running_fingerprint ]
2015-05-15 17:34:35 +00:00
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?
2015-05-18 15:33:58 +00:00
other_redeemed_freebie = JamTrackRight . where ( redeemed : true ) . where ( redeemed_and_fingerprinted : true ) . where ( 'id != ?' , id ) . where ( user_id : current_user . id ) . first
2015-05-15 17:34:35 +00:00
if other_redeemed_freebie
return " already redeemed another "
end
2015-05-20 14:24:17 +00:00
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
2015-05-15 17:34:35 +00:00
2015-05-20 14:24:17 +00:00
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 )
2015-05-15 17:34:35 +00:00
2015-05-20 14:24:17 +00:00
# record the alert
fraud = FraudAlert . create ( mf , current_user ) if mf . valid?
fraud_admin_url = fraud . admin_url if fraud
2015-05-15 17:34:35 +00:00
2015-05-18 15:33:58 +00:00
2015-05-20 14:24:17 +00:00
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
2015-05-18 15:33:58 +00:00
# try to record the other fingerprint
2015-05-20 14:24:17 +00:00
mf = MachineFingerprint . create ( running_fingerprint , current_user , MachineFingerprint :: TAKEN_ON_FRAUD_CONFLICT , MachineFingerprint :: PRINT_TYPE_ACTIVE , remote_ip , running_fingerprint_extra , self )
2015-05-18 15:33:58 +00:00
if APP_CONFIG . error_on_fraud
2015-05-20 14:24:17 +00:00
return " other user has 'all' fingerprint "
else
self . redeemed_and_fingerprinted = true
save!
return nil
2015-05-18 15:33:58 +00:00
end
2015-05-20 14:24:17 +00:00
2015-05-18 15:33:58 +00:00
end
2015-05-20 14:24:17 +00:00
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 \n Fraud 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
2015-05-15 17:34:35 +00:00
end
# we made it past all checks; let's slap on the redeemed_fingerprint
self . redeemed_and_fingerprinted = true
2015-05-20 14:24:17 +00:00
MachineFingerprint . create ( all_fingerprint , current_user , MachineFingerprint :: TAKEN_ON_SUCCESSFUL_DOWNLOAD , MachineFingerprint :: PRINT_TYPE_ALL , remote_ip , all_fingerprint_extra , self )
2015-05-15 20:02:00 +00:00
if all_fingerprint != running_fingerprint
2015-05-20 14:24:17 +00:00
MachineFingerprint . create ( running_fingerprint , current_user , MachineFingerprint :: TAKEN_ON_SUCCESSFUL_DOWNLOAD , MachineFingerprint :: PRINT_TYPE_ACTIVE , remote_ip , running_fingerprint_extra , self )
2015-05-15 20:02:00 +00:00
end
2015-05-15 17:34:35 +00:00
save!
end
nil
end
2015-05-07 13:49:15 +00:00
def self . stats
stats = { }
2015-05-20 15:17:01 +00:00
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
2015-05-07 13:49:15 +00:00
stats [ 'count' ] = result [ 'total' ] . to_i
stats [ 'signing_count' ] = result [ 'signing_count' ] . to_i
stats [ 'redeemed_count' ] = result [ 'redeem_count' ] . to_i
2015-05-28 18:52:20 +00:00
stats [ 'redeemed_and_dl_count' ] = result [ 'redeemed_and_dl_count' ] . to_i
2015-05-07 13:49:15 +00:00
stats [ 'purchased_count' ] = stats [ 'count' ] - stats [ 'redeemed_count' ]
stats
end
2014-11-04 20:55:12 +00:00
end
end