def get_next(sid): line = cache.get_station(sid, "request_line") if not line: return None song = None for pos in range(0, len(line)): if not line[pos]: pass # ?!?! elif 'skip' in line[pos] and line[pos]['skip']: log.debug("request", "Passing on user %s since they're marked as skippable." % line[pos]['username']) elif not line[pos]['song_id']: log.debug("request", "Passing on user %s since they have no valid first song." % line[pos]['username']) else: entry = line.pop(pos) song = playlist.Song.load_from_id(entry['song_id'], sid) log.debug("request", "Fulfilling %s's request for %s." % (entry['username'], song.filename)) song.data['elec_request_user_id'] = entry['user_id'] song.data['elec_request_username'] = entry['username'] u = User(entry['user_id']) db.c.update("DELETE FROM r4_request_store WHERE user_id = %s AND song_id = %s", (u.id, entry['song_id'])) u.remove_from_request_line() request_count = db.c.fetch_var("SELECT COUNT(*) FROM r4_request_history WHERE user_id = %s", (u.id,)) + 1 db.c.update("DELETE FROM r4_request_store WHERE song_id = %s AND user_id = %s", (song.id, u.id)) db.c.update("INSERT INTO r4_request_history (user_id, song_id, request_wait_time, request_line_size, request_at_count, sid) " "VALUES (%s, %s, %s, %s, %s, %s)", (u.id, song.id, timestamp() - entry['line_wait_start'], len(line), request_count, sid)) db.c.update("UPDATE phpbb_users SET radio_totalrequests = %s WHERE user_id = %s", (request_count, u.id)) song.update_request_count(sid) break return song
def prepare(self): super(MainIndex, self).prepare() if not cache.get_station(self.sid, "sched_current"): raise APIException( "server_just_started", "Rainwave is Rebooting, Please Try Again in a Few Minutes", http_code=500) # self.json_payload = {} self.jsfiles = None if not self.user: self.user = User(1) self.user.ensure_api_key() if self.beta or config.get("web_developer_mode") or config.get( "developer_mode") or config.get("test_mode"): buildtools.bake_beta_css() buildtools.bake_beta_templates() self.jsfiles = [] for root, subdirs, files in os.walk( os.path.join(os.path.dirname(__file__), "../static/%s" % self.js_dir)): #pylint: disable=W0612 for f in files: if f.endswith(".js"): self.jsfiles.append( os.path.join( root[root.find("static/%s" % self.js_dir):], f))
def fill(self, target_song_length=None, skip_requests=False): # ONLY RUN _ADD_REQUESTS ONCE PER FILL if not skip_requests: self._add_requests() for i in range(len(self.songs), self._num_songs): #pylint: disable=W0612 try: if not target_song_length and len( self.songs) > 0 and 'length' in self.songs[0].data: target_song_length = self.songs[0].data['length'] log.debug( "elec_fill", "Second song in election, aligning to length %s" % target_song_length) song = self._fill_get_song(target_song_length) song.data['entry_votes'] = 0 song.data['entry_type'] = ElecSongTypes.normal song.data['elec_request_user_id'] = 0 song.data['elec_request_username'] = None self._check_song_for_conflict(song) self.add_song(song) except Exception as e: log.exception("elec_fill", "Song failed to fill in an election.", e) if len(self.songs) == 0: raise ElectionEmptyException for song in self.songs: if 'elec_request_user_id' in song.data and song.data[ 'elec_request_user_id']: log.debug( "elec_fill", "Putting user %s back in line after request fulfillment." % song.data['elec_request_username']) u = User(song.data['elec_request_user_id']) u.put_in_request_line(u.get_tuned_in_sid()) request.update_line(self.sid)
def _process_line(line, sid): new_line = [] # user_positions has user_id as a key and position as the value, this is cached for quick lookups by API requests # so users know where they are in line user_positions = {} t = int(timestamp()) albums_with_requests = [] position = 1 # For each person for row in line: add_to_line = False u = User(row['user_id']) row['song_id'] = None # If their time is up, remove them and don't add them to the new line if row['line_expiry_tune_in'] and row['line_expiry_tune_in'] <= t: log.debug("request_line", "%s: Removed user ID %s from line for tune in timeout, expiry time %s current time %s" % (sid, u.id, row['line_expiry_tune_in'], t)) u.remove_from_request_line() else: tuned_in_sid = db.c.fetch_var("SELECT sid FROM r4_listeners WHERE user_id = %s AND sid = %s AND listener_purge = FALSE", (u.id, sid)) tuned_in = True if tuned_in_sid == sid else False if tuned_in: # Get their top song ID song_id = u.get_top_request_song_id(sid) # If they have no song and their line expiry has arrived, boot 'em if not song_id and row['line_expiry_election'] and (row['line_expiry_election'] <= t): log.debug("request_line", "%s: Removed user ID %s from line for election timeout, expiry time %s current time %s" % (sid, u.id, row['line_expiry_election'], t)) u.remove_from_request_line() # Give them more chances if they still have requests # They'll get added to the line of whatever station they're tuned in to (if any!) if u.has_requests(): u.put_in_request_line(u.get_tuned_in_sid()) # If they have no song, start the expiry countdown elif not song_id and not row['line_expiry_election']: row['line_expiry_election'] = t + 900 db.c.update("UPDATE r4_request_line SET line_expiry_election = %s WHERE user_id = %s", ((t + 900), row['user_id'])) add_to_line = True # Keep 'em in line else: if song_id: albums_with_requests.append(db.c.fetch_var("SELECT album_id FROM r4_songs WHERE song_id = %s", (song_id,))) row['song_id'] = song_id add_to_line = True elif not row['line_expiry_tune_in'] or row['line_expiry_tune_in'] == 0: db.c.update("UPDATE r4_request_line SET line_expiry_tune_in = %s WHERE user_id = %s", ((t + 600), row['user_id'])) add_to_line = True else: add_to_line = True if add_to_line: new_line.append(row) user_positions[u.id] = position position = position + 1 cache.set_station(sid, "request_line", new_line, True) cache.set_station(sid, "request_user_positions", user_positions, True) db.c.update("UPDATE r4_album_sid SET album_requests_pending = NULL WHERE album_requests_pending = TRUE AND sid = %s", (sid,)) for album_id in albums_with_requests: db.c.update("UPDATE r4_album_sid SET album_requests_pending = TRUE WHERE album_id = %s AND sid = %s", (album_id, sid)) return new_line
class MainIndex(api.web.HTMLRequest): description = "Main Rainwave page." def prepare(self): super(MainIndex, self).prepare() self.json_payload = [] if not self.user: self.user = User(1) self.user.ensure_api_key(self.request.remote_ip) self.user.data['sid'] = self.sid def append(self, key, value): self.json_payload.append({key: value}) def get(self): info.attach_info_to_request(self, playlist=True, artists=True) self.append("api_info", {"time": int(time.time())}) self.set_header("Content-Type", "text/plain") self.render("index.html", request=self, site_description=self.locale.translate( "station_description_id_%s" % self.sid), revision_number=config.get("revision_number"), api_url=config.get("api_external_url_prefix"), cookie_domain=config.get("cookie_domain"))
def test_check_song_for_conflict(self): db.c.update("DELETE FROM r4_listeners") db.c.update("DELETE FROM r4_request_store") e = Election.create(1) self.assertEqual(False, e._check_song_for_conflict(self.song1)) u = User(2) u.authorize(sid=1, ip_address=None, api_key=None, bypass=True) self.assertEqual(1, u.put_in_request_line(1)) # TODO: Use proper request/user methods here instead of DB call db.c.update( "UPDATE r4_request_line SET line_top_song_id = %s, line_expiry_tune_in = %s WHERE user_id = %s", (self.song1.id, int(time.time()) + 9999, u.id)) db.c.update( "INSERT INTO r4_listeners (sid, user_id, listener_icecast_id) VALUES (1, %s, 1)", (u.id, )) db.c.update( "INSERT INTO r4_request_store (user_id, song_id, sid) VALUES (%s, %s, 1)", (u.id, self.song1.id)) request.update_cache(1) request.update_expire_times() cache.update_local_cache_for_sid(1) self.assertEqual(True, e._check_song_for_conflict(self.song1)) self.assertEqual(True, e._check_song_for_conflict(self.song5)) self.assertEqual(event.ElecSongTypes.conflict, self.song5.data['entry_type']) self.assertEqual(event.ElecSongTypes.request, self.song1.data['entry_type'])
def prepare(self): if self.path_kwargs.get("station"): self.sid = config.stream_filename_to_sid.get( self.path_kwargs["station"]) if not self.sid: self.redirect(config.get("base_site_url")) return super(MainIndex, self).prepare() if self.path_kwargs.get("station") is None: self.redirect("{}{}/".format( config.get("base_site_url"), config.station_mount_filenames[self.sid], )) return if config.get("enforce_ssl") and self.request.protocol != "https": self.redirect("{}{}/".format( config.get("base_site_url"), config.station_mount_filenames[self.sid], )) return if not config.get("developer_mode" ) and self.request.host != config.get("hostname"): self.redirect("{}{}/".format( config.get("base_site_url"), config.station_mount_filenames[self.sid], )) return if not cache.get_station(self.sid, "sched_current"): raise APIException( "server_just_started", "Rainwave is Rebooting, Please Try Again in a Few Minutes", http_code=500, ) self.jsfiles = None if not self.user: self.user = User(1) self.user.ip_address = self.request.remote_ip self.user.ensure_api_key() if (self.beta or config.get("web_developer_mode") or config.get("developer_mode") or config.get("test_mode")): buildtools.bake_beta_css() buildtools.bake_beta_templates() self.jsfiles = [] for root, _subdirs, files in os.walk( os.path.join(os.path.dirname(__file__), "../static/%s" % self.js_dir)): for f in files: if f.endswith(".js"): self.jsfiles.append( os.path.join( root[root.find("static/%s" % self.js_dir):], f))
class MainIndex(api.web.HTMLRequest): description = "Main Rainwave page." auth_required = False login_required = False sid_required = False def prepare(self): super(MainIndex, self).prepare() self.json_payload = [] if not self.user: self.user = User(1) self.user.ensure_api_key(self.request.remote_ip) self.user.data['sid'] = self.sid def append(self, key, value): self.json_payload.append({ key: value }) def get(self): info.attach_info_to_request(self, all_lists=True) self.append("api_info", { "time": int(time.time()) }) self.render("index.html", request=self, site_description=self.locale.translate("station_description_id_%s" % self.sid), revision_number=config.build_number, api_url=config.get("api_external_url_prefix"), cookie_domain=config.get("cookie_domain"), locales=api.locale.locale_names_json)
def _do_auth(self, message): try: if not "user_id" in message or not message['user_id']: self.write_message({ "wserror": { "tl_key": "missing_argument", "text": self.locale.translate("missing_argument", argument="user_id") } }) if not isinstance(message['user_id'], numbers.Number): self.write_message({ "wserror": { "tl_key": "invalid_argument", "text": self.locale.translate("invalid_argument", argument="user_id") } }) if not "key" in message or not message['key']: self.write_message({ "wserror": { "tl_key": "missing_argument", "text": self.locale.translate("missing_argument", argument="key") } }) self.user = User(message['user_id']) self.user.ip_address = self.request.remote_ip self.user.authorize(None, message['key']) if not self.user.authorized: self.write_message({ "wserror": { "tl_key": "auth_failed", "text": self.locale.translate("auth_failed") } }) self.close() return self.authorized = True self.uuid = str(uuid.uuid4()) global sessions sessions[self.sid].append(self) self.votes_by_key = self.request.remote_ip if self.user.is_anonymous() else self.user.id self.refresh_user() # no need to send the user's data to the user as that would have come with bootstrap # and will come with each synchronization of the schedule anyway self.write_message({ "wsok": True }) # since this will be the first action in any websocket interaction though, # it'd be a good time to send a station offline message. self._station_offline_check() except Exception as e: log.exception("websocket", "Exception during authentication.", e) self.close()
class Bootstrap(api.web.APIHandler): description = ( "Bootstrap a Rainwave client. Provides user info, API key, station info, relay info, and more. " "If you run a GET query to this URL, you will receive a Javascript file containing a single variable called BOOTSTRAP. While this is largely intended for the purposes of the main site, you may use this. " "If you run a POST query to this URL, you will receive a JSON object." ) phpbb_auth = True auth_required = False login_required = False sid_required = False allow_get = False def prepare(self): super(Bootstrap, self).prepare() if not self.user: self.user = User(1) self.user.ensure_api_key() # def finish(self, *args, **kwargs): # self.write_output() # super(Bootstrap, self).finish(*args, **kwargs) def get(self): self.set_header("Content-Type", "text/javascript") self.append("locales", api.locale.locale_names) self.append("cookie_domain", config.get("cookie_domain")) self.post() self.write("var BOOTSTRAP=") def post(self): info.attach_info_to_request(self, extra_list=self.get_cookie("r4_active_list")) self.append("stream_filename", config.get_station(self.sid, "stream_filename")) self.append("station_list", config.station_list) self.append("relays", config.public_relays[self.sid])
def rainwave_auth(self): user_id_present = "user_id" in self.request.arguments if self.auth_required and not user_id_present: raise APIException("missing_argument", argument="user_id", http_code=400) if user_id_present and not fieldtypes.numeric( self.get_argument("user_id")): # do not spit out the user ID back at them, that would create a potential XSS hack raise APIException("invalid_argument", argument="user_id", reason="not numeric.", http_code=400) if (self.auth_required or user_id_present) and not "key" in self.request.arguments: raise APIException("missing_argument", argument="key", http_code=400) if user_id_present: self.user = User(long(self.get_argument("user_id"))) self.user.ip_address = self.request.remote_ip self.user.authorize(self.sid, self.get_argument("key")) if not self.user.authorized: raise APIException("auth_failed", http_code=403) else: self._update_phpbb_session( self._get_phpbb_session(self.user.id))
def do_phpbb_auth(self): phpbb_cookie_name = config.get("phpbb_cookie_name") + "_" user_id = fieldtypes.integer( self.get_cookie(phpbb_cookie_name + "u", "")) if not user_id: pass else: if self._verify_phpbb_session(user_id): # update_phpbb_session is done by verify_phpbb_session if successful self.user = User(user_id) self.user.ip_address = self.request.remote_ip self.user.authorize(self.sid, None, bypass=True) return True if not self.user and self.get_cookie(phpbb_cookie_name + "k"): can_login = db.c.fetch_var( "SELECT 1 FROM phpbb_sessions_keys WHERE key_id = %s AND user_id = %s", (hashlib.md5(self.get_cookie(phpbb_cookie_name + "k")).hexdigest(), user_id)) if can_login == 1: self._update_phpbb_session( self._get_phpbb_session(user_id)) self.user = User(user_id) self.user.ip_address = self.request.remote_ip self.user.authorize(self.sid, None, bypass=True) return True return False
class MainIndex(api.web.HTMLRequest): description = "Main Rainwave page." auth_required = False login_required = False sid_required = False def prepare(self): super(MainIndex, self).prepare() self.json_payload = [] if not self.user: self.user = User(1) self.user.ensure_api_key(self.request.remote_ip) self.user.data['sid'] = self.sid def append(self, key, value): self.json_payload.append({key: value}) def get(self): info.attach_info_to_request(self, all_lists=True) self.append("api_info", {"time": int(time.time())}) self.render("index.html", request=self, site_description=self.locale.translate( "station_description_id_%s" % self.sid), revision_number=config.build_number, api_url=config.get("api_external_url_prefix"), cookie_domain=config.get("cookie_domain"), locales=api.locale.locale_names_json)
def fill(self, target_song_length = None, skip_requests = False): # ONLY RUN _ADD_REQUESTS ONCE PER FILL if not skip_requests: self._add_requests() for i in range(len(self.songs), self._num_songs): #pylint: disable=W0612 try: if not target_song_length and len(self.songs) > 0 and 'length' in self.songs[0].data: target_song_length = self.songs[0].data['length'] log.debug("elec_fill", "Second song in election, aligning to length %s" % target_song_length) song = self._fill_get_song(target_song_length) song.data['entry_votes'] = 0 song.data['entry_type'] = ElecSongTypes.normal song.data['elec_request_user_id'] = 0 song.data['elec_request_username'] = None self._check_song_for_conflict(song) self.add_song(song) except Exception as e: log.exception("elec_fill", "Song failed to fill in an election.", e) if len(self.songs) == 0: raise ElectionEmptyException for song in self.songs: if 'elec_request_user_id' in song.data and song.data['elec_request_user_id']: log.debug("elec_fill", "Putting user %s back in line after request fulfillment." % song.data['elec_request_username']) u = User(song.data['elec_request_user_id']) u.put_in_request_line(u.get_tuned_in_sid()) request.update_line(self.sid)
def prepare(self): super(MainIndex, self).prepare() self.json_payload = [] if not self.user: self.user = User(1) self.user.ensure_api_key(self.request.remote_ip) self.user.data['sid'] = self.sid
def prepare(self): super(Bootstrap, self).prepare() if not self.user: self.user = User(1) self.user.ensure_api_key() self.is_mobile = self.request.headers.get("User-Agent").lower().find( "mobile") != -1 or self.request.headers.get( "User-Agent").lower().find("android") != -1
class MainIndex(api.web.HTMLRequest): description = "Main Rainwave page." auth_required = config.has("index_requires_login") and config.get("index_requires_login") login_required = config.has("index_requires_login") and config.get("index_requires_login") sid_required = False beta = False page_template = "r4_index.html" js_dir = "js4" def set_default_headers(self): self.set_header("X-Frame-Options", "SAMEORIGIN") def prepare(self): super(MainIndex, self).prepare() self.json_payload = {} self.jsfiles = None if not self.user: self.user = User(1) self.user.ensure_api_key() if self.beta or config.get("web_developer_mode") or config.get("developer_mode") or config.get("test_mode"): buildtools.bake_beta_css() buildtools.bake_beta_templates() self.jsfiles = [] for root, subdirs, files in os.walk( os.path.join(os.path.dirname(__file__), "../static/%s" % self.js_dir) ): # pylint: disable=W0612 for f in files: if f.endswith(".js"): self.jsfiles.append(os.path.join(root[root.find("static/%s" % self.js_dir) :], f)) def append(self, key, value): self.json_payload[key] = value def get(self): self.mobile = ( self.request.headers.get("User-Agent").lower().find("mobile") != -1 or self.request.headers.get("User-Agent").lower().find("android") != -1 ) if not self.beta: info.attach_info_to_request(self, extra_list=self.get_cookie("r4_active_list")) self.append("api_info", {"time": int(timestamp())}) self.render( self.page_template, request=self, site_description=self.locale.translate("station_description_id_%s" % self.sid), revision_number=config.build_number, jsfiles=self.jsfiles, api_url=config.get("api_external_url_prefix"), cookie_domain=config.get("cookie_domain"), locales=api.locale.locale_names_json, relays=config.public_relays_json[self.sid], stream_filename=config.get_station(self.sid, "stream_filename"), station_list=config.station_list_json, apple_home_screen_icon=config.get_station(self.sid, "apple_home_screen_icon"), mobile=self.mobile, )
def prepare(self): if self.local_only and not self.request.remote_ip in config.get("api_trusted_ip_addresses"): log.info("api", "Rejected %s request from %s, untrusted address." % (self.url, self.request.remote_ip)) raise APIException("rejected", text="You are not coming from a trusted address.") if self.allow_cors: self.set_header("Access-Control-Allow-Origin", "*") self.set_header("Access-Control-Max-Age", "600") self.set_header("Access-Control-Allow-Credentials", "false") if not isinstance(self.locale, locale.RainwaveLocale): self.locale = self.get_browser_locale() self.setup_output() if 'in_order' in self.request.arguments: self._output = [] self._output_array = True else: self._output = {} self.sid = fieldtypes.integer(self.get_cookie("r4_sid", None)) hostname = self.request.headers.get('Host', None) if hostname: hostname = unicode(hostname).split(":")[0] if hostname in config.station_hostnames: self.sid = config.station_hostnames[hostname] sid_arg = fieldtypes.integer(self.get_argument("sid", None)) if sid_arg is not None: self.sid = sid_arg if self.sid is None and self.sid_required: raise APIException("missing_station_id", http_code=400) self.arg_parse() self.sid_check() if self.sid: self.set_cookie("r4_sid", str(self.sid), expires_days=365, domain=config.get("cookie_domain")) if self.phpbb_auth: self.do_phpbb_auth() else: self.rainwave_auth() if not self.user and self.auth_required: raise APIException("auth_required", http_code=403) elif not self.user and not self.auth_required: self.user = User(1) self.user.ip_address = self.request.remote_ip self.user.refresh(self.sid) if self.user and config.get("store_prefs"): self.user.save_preferences(self.request.remote_ip, self.get_cookie("r4_prefs", None)) self.permission_checks()
def do_rw_session_auth(self): rw_session_id = self.get_cookie("r4_session_id") if rw_session_id: user_id = db.c.fetch_var("SELECT user_id FROM r4_sessions WHERE session_id = %s", (rw_session_id,)) if user_id: self.user = User(user_id) self.user.ip_address = self.request.remote_ip self.user.authorize(self.sid, None, bypass=True) return True return False
def get_next(sid): line = cache.get_station(sid, "request_line") if not line: return None song = None for pos in range(0, len(line)): if not line[pos] or not line[pos]['song_id']: pass else: entry = line.pop(pos) song = playlist.Song.load_from_id(entry['song_id'], sid) song.data['elec_request_user_id'] = entry['user_id'] song.data['elec_request_username'] = entry['username'] u = User(entry['user_id']) db.c.update("DELETE FROM r4_request_store WHERE user_id = %s AND song_id = %s", (u.id, entry['song_id'])) u.remove_from_request_line() user_sid = u.get_tuned_in_sid() if u.has_requests(): u.put_in_request_line(user_sid) request_count = db.c.fetch_var("SELECT COUNT(*) FROM r4_request_history WHERE user_id = %s", (u.id,)) + 1 db.c.update("DELETE FROM r4_request_store WHERE song_id = %s AND user_id = %s", (song.id, u.id)) db.c.update("INSERT INTO r4_request_history (user_id, song_id, request_wait_time, request_line_size, request_at_count) " "VALUES (%s, %s, %s, %s, %s)", (u.id, song.id, time.time() - entry['line_wait_start'], len(line), request_count)) # Update the user's request cache u.get_requests(refresh=True) cache.set_station(sid, "request_line", line, True) break return song
def get_next(sid): entry, line = get_next_entry(sid) if not entry: return None user = User(entry['user_id']) user.data['name'] = entry['username'] song = playlist.Song.load_from_id(entry['song_id'], sid) mark_request_filled(sid, user, song, entry, line) return song
def get_next(sid): entry, line = get_next_entry(sid) if not entry: return None user = User(entry["user_id"]) user.data["name"] = entry["username"] song = playlist.Song.load_from_id(entry["song_id"], sid) mark_request_filled(sid, user, song, entry, line) return song
def get_next(sid): line = cache.get_station(sid, "request_line") if not line: return None song = None for pos in range(0, len(line)): if not line[pos]: pass # ?!?! elif not line[pos]['song_id']: log.debug( "request", "Passing on user %s since they have no valid first song." % line[pos]['username']) else: entry = line.pop(pos) song = playlist.Song.load_from_id(entry['song_id'], sid) log.debug( "request", "Fulfilling %s's request for %s." % (entry['username'], song.filename)) song.data['elec_request_user_id'] = entry['user_id'] song.data['elec_request_username'] = entry['username'] u = User(entry['user_id']) db.c.update( "DELETE FROM r4_request_store WHERE user_id = %s AND song_id = %s", (u.id, entry['song_id'])) u.remove_from_request_line() if u.has_requests(): u.put_in_request_line(u.get_tuned_in_sid()) request_count = db.c.fetch_var( "SELECT COUNT(*) FROM r4_request_history WHERE user_id = %s", (u.id, )) + 1 db.c.update( "DELETE FROM r4_request_store WHERE song_id = %s AND user_id = %s", (song.id, u.id)) db.c.update( "INSERT INTO r4_request_history (user_id, song_id, request_wait_time, request_line_size, request_at_count, sid) " "VALUES (%s, %s, %s, %s, %s, %s)", (u.id, song.id, time.time() - entry['line_wait_start'], len(line), request_count, sid)) db.c.update( "UPDATE phpbb_users SET radio_totalrequests = %s WHERE user_id = %s", (request_count, u.id)) song.update_request_count(sid) # Update the user's request cache u.get_requests(refresh=True) # If we fully update the line, the user may sneak in and get 2 requests in the same election. # This is not a good idea, so we leave it to the scheduler to issue the full cache update. cache.set_station(sid, "request_line", line, True) break return song
def get_next_ignoring_cooldowns(sid): line = db.c.fetch_all(LINE_SQL, (sid,)) if not line or len(line) == 0: return None entry = line[0] user = User(entry['user_id']) user.data['name'] = entry['username'] song = playlist.Song.load_from_id(user.get_top_request_song_id_any(sid), sid) mark_request_filled(sid, user, song, entry, line) return song
class MainIndex(api.web.HTMLRequest): description = "Main Rainwave page." auth_required = False login_required = False sid_required = False beta = False page_template = "r4_index.html" def prepare(self): super(MainIndex, self).prepare() self.json_payload = {} self.jsfiles = None if not self.user: self.user = User(1) self.user.ensure_api_key() if self.beta or config.get("web_developer_mode") or config.get( "developer_mode") or config.get("test_mode"): buildtools.bake_beta_css() self.jsfiles = [] for root, subdirs, files in os.walk( os.path.join(os.path.dirname(__file__), "../static/js4")): for f in files: self.jsfiles.append( os.path.join(root[root.find("static/js4"):], f)) def append(self, key, value): self.json_payload[key] = value def get(self): self.mobile = self.request.headers.get("User-Agent").lower().find( "mobile") != -1 or self.request.headers.get( "User-Agent").lower().find("android") != -1 info.attach_info_to_request( self, extra_list=self.get_cookie("r4_active_list")) self.append("api_info", {"time": int(time.time())}) self.render(self.page_template, request=self, site_description=self.locale.translate( "station_description_id_%s" % self.sid), revision_number=config.build_number, jsfiles=self.jsfiles, api_url=config.get("api_external_url_prefix"), cookie_domain=config.get("cookie_domain"), locales=api.locale.locale_names_json, relays=config.public_relays_json[self.sid], stream_filename=config.get_station(self.sid, "stream_filename"), station_list=config.station_list_json, mobile=self.mobile)
def test_get_request(self): db.c.update("DELETE FROM r4_listeners") db.c.update("DELETE FROM r4_request_store") u = User(2) u.authorize(1, None, None, True) u.put_in_request_line(1) # TODO: Use proper request class here instead of DB call db.c.update("INSERT INTO r4_listeners (sid, user_id, listener_icecast_id) VALUES (1, %s, 1)", (u.id,)) db.c.update("INSERT INTO r4_request_store (user_id, song_id, sid) VALUES (%s, %s, 1)", (u.id, self.song1.id,)) e = Election.create(1) req = e.get_request() self.assertNotEqual(None, req) self.assertEqual(self.song1.id, req.id)
def get_next_ignoring_cooldowns(sid): line = db.c.fetch_all(LINE_SQL, (sid, )) if not line or len(line) == 0: return None entry = line[0] user = User(entry["user_id"]) user.data["name"] = entry["username"] song = playlist.Song.load_from_id(user.get_top_request_song_id_any(sid), sid) mark_request_filled(sid, user, song, entry, line) return song
class Bootstrap(api.web.APIHandler): description = ( "Bootstrap a Rainwave client. Provides user info, API key, station info, relay info, and more. " "If you run a GET query to this URL, you will receive a Javascript file containing a single variable called BOOTSTRAP. While this is largely intended for the purposes of the main site, you may use this. " "If you run a POST query to this URL, you will receive a JSON object of the same data." ) phpbb_auth = True auth_required = False login_required = False sid_required = False allow_get = False is_mobile = False content_type = "text/javascript" def prepare(self): super(Bootstrap, self).prepare() if not self.user: self.user = User(1) self.user.ensure_api_key() self.is_mobile = ( self.request.headers.get("User-Agent").lower().find("mobile") != -1 or self.request.headers.get("User-Agent").lower().find("android") != -1) def get(self): # pylint: disable=method-hidden self.post() if self.is_mobile: self.write("window.MOBILE = true;") else: self.write("window.MOBILE = false;") self.write("var BOOTSTRAP=") def post(self): info.attach_info_to_request(self, live_voting=True) self.append("build_version", config.build_number) self.append("locale", self.locale.code) self.append("locales", api.locale.locale_names) self.append("cookie_domain", config.get("cookie_domain")) self.append("on_init", []) self.append("on_measure", []) self.append("on_draw", []) self.append("websocket_host", config.get("websocket_host")) self.append("stream_filename", config.get_station(self.sid, "stream_filename")) self.append("station_list", config.station_list) self.append("relays", config.public_relays[self.sid]) if self.is_mobile: self.append("mobile", True) else: self.append("mobile", False)
def prepare(self): super(MainIndex, self).prepare() if not cache.get_station(self.sid, "sched_current"): raise APIException( "server_just_started", "Rainwave is Rebooting, Please Try Again in a Few Minutes", http_code=500 ) # self.json_payload = {} self.jsfiles = None if not self.user: self.user = User(1) self.user.ip_address = self.request.remote_ip self.user.ensure_api_key() if self.beta or config.get("web_developer_mode") or config.get("developer_mode") or config.get("test_mode"): buildtools.bake_beta_css() buildtools.bake_beta_templates() self.jsfiles = [] for root, subdirs, files in os.walk( os.path.join(os.path.dirname(__file__), "../static/%s" % self.js_dir) ): # pylint: disable=W0612 for f in files: if f.endswith(".js"): self.jsfiles.append(os.path.join(root[root.find("static/%s" % self.js_dir) :], f))
def prepare(self): if self.local_only and not self.request.remote_ip in config.get("api_trusted_ip_addresses"): log.info("api", "Rejected %s request from %s, untrusted address." % (self.url, self.request.remote_ip)) raise APIException("rejected", text="You are not coming from a trusted address.") if self.allow_cors: self.set_header("Access-Control-Allow-Origin", "*") self.set_header("Access-Control-Max-Age", "600") self.set_header("Access-Control-Allow-Credentials", "false") if not isinstance(self.locale, locale.RainwaveLocale): self.locale = self.get_browser_locale() self.setup_output() if 'in_order' in self.request.arguments: self._output = [] self._output_array = True else: self._output = {} if not self.sid: self.sid = fieldtypes.integer(self.get_cookie("r4_sid", None)) hostname = self.request.headers.get('Host', None) if hostname: hostname = unicode(hostname).split(":")[0] if hostname in config.station_hostnames: self.sid = config.station_hostnames[hostname] sid_arg = fieldtypes.integer(self.get_argument("sid", None)) if sid_arg is not None: self.sid = sid_arg if self.sid is None and self.sid_required: raise APIException("missing_station_id", http_code=400) self.arg_parse() self.sid_check() if self.sid: self.set_cookie("r4_sid", str(self.sid), expires_days=365) if self.phpbb_auth: self.do_phpbb_auth() else: self.rainwave_auth() if not self.user and self.auth_required: raise APIException("auth_required", http_code=403) elif not self.user and not self.auth_required: self.user = User(1) self.user.ip_address = self.request.remote_ip self.user.refresh(self.sid) if self.user and config.get("store_prefs"): self.user.save_preferences(self.request.remote_ip, self.get_cookie("r4_prefs", None)) self.permission_checks()
class Bootstrap(api.web.APIHandler): description = ( "Bootstrap a Rainwave client. Provides user info, API key, station info, relay info, and more. " "If you run a GET query to this URL, you will receive a Javascript file containing a single variable called BOOTSTRAP. While this is largely intended for the purposes of the main site, you may use this. " "If you run a POST query to this URL, you will receive a JSON object of the same data." ) phpbb_auth = True auth_required = False login_required = False sid_required = False allow_get = False def prepare(self): super(Bootstrap, self).prepare() if not self.user: self.user = User(1) self.user.ensure_api_key() self.is_mobile = self.request.headers.get("User-Agent").lower().find("mobile") != -1 or self.request.headers.get("User-Agent").lower().find("android") != -1 def get(self): self.set_header("Content-Type", "text/javascript") self.post() if self.is_mobile: self.write("window.MOBILE = true;") else: self.write("window.MOBILE = false;") self.write("var BOOTSTRAP=") def post(self): info.attach_info_to_request(self, live_voting=True) self.append("build_version", config.build_number) self.append("locale", self.locale.code) self.append("locales", api.locale.locale_names) self.append("cookie_domain", config.get("cookie_domain")) self.append("on_init", []) self.append("on_measure", []) self.append("on_draw", []) self.append("websocket_host", config.get("websocket_host")) self.append("stream_filename", config.get_station(self.sid, "stream_filename")) self.append("station_list", config.station_list) self.append("relays", config.public_relays[self.sid]) if self.is_mobile: self.append("mobile", True) else: self.append("mobile", False)
def get_next(sid): line = cache.get_station(sid, "request_line") if not line: return None song = None for pos in range(0, len(line)): if not line[pos]: pass # ?!?! elif not line[pos]['song_id']: log.debug("request", "Passing on user %s since they have no valid first song." % line[pos]['username']) else: entry = line.pop(pos) song = playlist.Song.load_from_id(entry['song_id'], sid) log.debug("request", "Fulfilling %s's request for %s." % (entry['username'], song.filename)) song.data['elec_request_user_id'] = entry['user_id'] song.data['elec_request_username'] = entry['username'] u = User(entry['user_id']) db.c.update("DELETE FROM r4_request_store WHERE user_id = %s AND song_id = %s", (u.id, entry['song_id'])) u.remove_from_request_line() if u.has_requests(): u.put_in_request_line(u.get_tuned_in_sid()) request_count = db.c.fetch_var("SELECT COUNT(*) FROM r4_request_history WHERE user_id = %s", (u.id,)) + 1 db.c.update("DELETE FROM r4_request_store WHERE song_id = %s AND user_id = %s", (song.id, u.id)) db.c.update("INSERT INTO r4_request_history (user_id, song_id, request_wait_time, request_line_size, request_at_count, sid) " "VALUES (%s, %s, %s, %s, %s, %s)", (u.id, song.id, time.time() - entry['line_wait_start'], len(line), request_count, sid)) db.c.update("UPDATE phpbb_users SET radio_totalrequests = %s WHERE user_id = %s", (request_count, u.id)) song.update_request_count(sid) # If we fully update the line, the user may sneak in and get 2 requests in the same election. # This is not a good idea, so we leave it to the scheduler to issue the full cache update. cache.set_station(sid, "request_line", line, True) break return song
def prepare(self): super(MainIndex, self).prepare() self.json_payload = {} self.jsfiles = None if not self.user: self.user = User(1) self.user.ensure_api_key() if self.beta or config.get("web_developer_mode") or config.get( "developer_mode") or config.get("test_mode"): buildtools.bake_beta_css() self.jsfiles = [] for root, subdirs, files in os.walk( os.path.join(os.path.dirname(__file__), "../static/js4")): for f in files: self.jsfiles.append( os.path.join(root[root.find("static/js4"):], f))
def setup_rainwave_session_and_redirect(self, user_id, destination): session_id = str(uuid.uuid4()) db.c.update( "INSERT INTO r4_sessions (session_id, user_id) VALUES (%s, %s)", (session_id, user_id,) ) self.set_cookie("r4_session_id", session_id) if destination == "app" or destination == "rw": user = User(user_id) user.authorize(1, None, bypass=True) self.redirect("rw://%s:%[email protected]" % (user.id, user.ensure_api_key()),) elif destination == "rwpath": user = User(user_id) user.authorize(1, None, bypass=True) self.redirect("rwpath://rainwave.cc/%s/%s" % (user.id, user.ensure_api_key()),) else: self.redirect("/")
def prepare(self): # TODO: Language self.info = [] self.sid = fieldtypes.integer(self.get_cookie("r4sid", "1")) if not self.sid: self.sid = 1 if self.request.host == "game.rainwave.cc": self.sid = 1 elif self.request.host == "ocr.rainwave.cc": self.sid = 2 elif self.request.host == "covers.rainwave.cc" or self.request.host == "cover.rainwave.cc": self.sid = 3 elif self.request.host == "chiptune.rainwave.cc": self.sid = 4 elif self.request.host == "all.rainwave.cc": self.sid = 5 self.set_cookie("r4sid", str(self.sid), expires_days=365, domain=".rainwave.cc") self.user = None if not fieldtypes.integer(self.get_cookie("phpbb3_38ie8_u", "")): self.user = User(1) else: user_id = int(self.get_cookie("phpbb3_38ie8_u")) if self.get_cookie("phpbb3_38ie8_sid"): session_id = db.c_old.fetch_var("SELECT session_id FROM phpbb_sessions WHERE session_id = %s AND session_user_id = %s", (self.get_cookie("phpbb3_38ie8_sid"), user_id)) if session_id: db.c_old.update("UPDATE phpbb_sessions SET session_last_visit = %s, session_page = %s WHERE session_id = %s", (time.time(), "rainwave", session_id)) self.user = User(user_id) self.user.authorize(self.sid, None, None, True) if not self.user and self.get_cookie("phpbb3_38ie8_k"): can_login = db.c_old.fetch_var("SELECT 1 FROM phpbb_sessions_keys WHERE key_id = %s AND user_id = %s", (hashlib.md5(self.get_cookie("phpbb3_38ie8_k")).hexdigest(), user_id)) if can_login == 1: self.user = User(user_id) self.user.authorize(self.sid, None, None, True) if not self.user: self.user = User(1) self.user.ensure_api_key(self.request.remote_ip) self.user.data['sid'] = self.sid
def do_phpbb_auth(self): phpbb_cookie_name = config.get("phpbb_cookie_name") user_id = fieldtypes.integer(self.get_cookie(phpbb_cookie_name + "u", "")) if not user_id: pass else: if self._verify_phpbb_session(user_id): # update_phpbb_session is done by verify_phpbb_session if successful self.user = User(user_id) self.user.authorize(self.sid, None, None, True) return True if not self.user and self.get_cookie(phpbb_cookie_name + "k"): can_login = db.c.fetch_var("SELECT 1 FROM phpbb_sessions_keys WHERE key_id = %s AND user_id = %s", (hashlib.md5(self.get_cookie(phpbb_cookie_name + "k")).hexdigest(), user_id)) if can_login == 1: self._update_phpbb_session(self._get_phpbb_session(user_id)) self.user = User(user_id) self.user.authorize(self.sid, None, None, True) return True return False
def test_check_song_for_conflict(self): db.c.update("DELETE FROM r4_listeners") db.c.update("DELETE FROM r4_request_store") e = Election.create(1) self.assertEqual(False, e._check_song_for_conflict(self.song1)) u = User(2) u.authorize(1, None, None, True) self.assertEqual(1, u.put_in_request_line(1)) # TODO: Use proper request/user methods here instead of DB call db.c.update("UPDATE r4_request_line SET line_top_song_id = %s, line_expiry_tune_in = %s WHERE user_id = %s", (self.song1.id, time.time()+9999, u.id)) db.c.update("INSERT INTO r4_listeners (sid, user_id, listener_icecast_id) VALUES (1, %s, 1)", (u.id,)) db.c.update("INSERT INTO r4_request_store (user_id, song_id, sid) VALUES (%s, %s, 1)", (u.id, self.song1.id)) request.update_cache(1) cache.update_local_cache_for_sid(1) self.assertEqual(True, e._check_song_for_conflict(self.song1)) self.assertEqual(False, e._check_song_for_conflict(self.song5)) self.assertEqual(event.ElecSongTypes.conflict, self.song5.data['entry_type']) self.assertEqual(event.ElecSongTypes.request, self.song1.data['entry_type'])
class MainIndex(api.web.HTMLRequest): description = "Main Rainwave page." auth_required = False login_required = False sid_required = False beta = False def prepare(self): super(MainIndex, self).prepare() self.json_payload = {} self.jsfiles = None if not self.user: self.user = User(1) self.user.ensure_api_key(self.request.remote_ip) if self.beta or config.get("web_developer_mode") or config.get("developer_mode") or config.get("test_mode"): buildtools.bake_css() self.jsfiles = [] for root, subdirs, files in os.walk(os.path.join(os.path.dirname(__file__), "../static/js4")): for f in files: self.jsfiles.append(os.path.join(root[root.find("static/js4"):], f)) def append(self, key, value): self.json_payload[key] = value def get(self): info.attach_info_to_request(self, extra_list=self.get_cookie("r4_active_list")) self.append("api_info", { "time": int(time.time()) }) mobile = self.request.headers.get("User-Agent").lower().find("mobile") != -1 or self.request.headers.get("User-Agent").lower().find("android") != -1 self.render("r4_index.html", request=self, site_description=self.locale.translate("station_description_id_%s" % self.sid), revision_number=config.build_number, jsfiles=self.jsfiles, api_url=config.get("api_external_url_prefix"), cookie_domain=config.get("cookie_domain"), locales=api.locale.locale_names_json, relays=config.public_relays_json[self.sid], stream_filename=config.get_station(self.sid, "stream_filename"), station_list=config.station_list_json, mobile=mobile)
def get_next(sid): line = cache.get_station(sid, "request_line") if not line: return None song = None for pos in range(0, len(line)): if not line[pos] or not line[pos]['song_id']: pass else: entry = line.pop(pos) song = playlist.Song.load_from_id(entry['song_id'], sid) song.data['elec_request_user_id'] = entry['user_id'] song.data['elec_request_username'] = entry['username'] u = User(entry['user_id']) db.c.update( "DELETE FROM r4_request_store WHERE user_id = %s AND song_id = %s", (u.id, entry['song_id'])) u.remove_from_request_line() user_sid = u.get_tuned_in_sid() if u.has_requests(): u.put_in_request_line(user_sid) request_count = db.c.fetch_var( "SELECT COUNT(*) FROM r4_request_history WHERE user_id = %s", (u.id, )) + 1 db.c.update( "DELETE FROM r4_request_store WHERE song_id = %s AND user_id = %s", (song.id, u.id)) db.c.update( "INSERT INTO r4_request_history (user_id, song_id, request_wait_time, request_line_size, request_at_count) " "VALUES (%s, %s, %s, %s, %s)", (u.id, song.id, time.time() - entry['line_wait_start'], len(line), request_count)) # Update the user's request cache u.get_requests(refresh=True) cache.set_station(sid, "request_line", line, True) break return song
class MainIndex(api.web.HTMLRequest): description = "Main Rainwave page." def prepare(self): super(MainIndex, self).prepare() self.json_payload = [] if not self.user: self.user = User(1) self.user.ensure_api_key(self.request.remote_ip) self.user.data['sid'] = self.sid def append(self, key, value): self.json_payload.append({ key: value }) def get(self): info.attach_info_to_request(self, playlist=True, artists=True) self.append("api_info", { "time": int(time.time()) }) self.set_header("Content-Type", "text/plain") self.render("index.html", request=self, site_description=self.locale.translate("station_description_id_%s" % self.sid), revision_number=config.get("revision_number"), api_url=config.get("api_external_url_prefix"), cookie_domain=config.get("cookie_domain"))
def prepare(self): super(MainIndex, self).prepare() self.json_payload = {} self.jsfiles = None if not self.user: self.user = User(1) self.user.ensure_api_key(self.request.remote_ip) if self.beta or config.get("web_developer_mode") or config.get("developer_mode") or config.get("test_mode"): buildtools.bake_css() self.jsfiles = [] for root, subdirs, files in os.walk(os.path.join(os.path.dirname(__file__), "../static/js4")): for f in files: self.jsfiles.append(os.path.join(root[root.find("static/js4"):], f))
def rainwave_auth(self): user_id_present = "user_id" in self.request.arguments if self.auth_required and not user_id_present: raise APIException("missing_argument", argument="user_id", http_code=400) if user_id_present and not fieldtypes.numeric(self.get_argument("user_id")): # do not spit out the user ID back at them, that would create a potential XSS hack raise APIException("invalid_argument", argument="user_id", reason="not numeric.", http_code=400) if (self.auth_required or user_id_present) and not "key" in self.request.arguments: raise APIException("missing_argument", argument="key", http_code=400) if user_id_present: self.user = User(long(self.get_argument("user_id"))) self.user.authorize(self.sid, self.request.remote_ip, self.get_argument("key")) if not self.user.authorized: raise APIException("auth_failed", http_code=403) else: self._update_phpbb_session(self._get_phpbb_session(self.user.id))
def rainwave_auth(self): request_ok = True user_id_present = "user_id" in self.request.arguments if self.auth_required and not user_id_present: self.append("error", api.returns.ErrorReturn(-1000, "Missing user_id argument.")) request_ok = False if user_id_present and not fieldtypes.numeric(self.get_argument("user_id")): self.append("error", api.returns.ErrorReturn(-1000, "Invalid user ID %s.")) request_ok = False if (self.auth_required or user_id_present) and not "key" in self.request.arguments: self.append("error", api.returns.ErrorReturn(-1000, "Missing 'key' argument.")) request_ok = False self.user = None if request_ok and user_id_present: self.user = User(long(self.get_argument("user_id"))) self.user.authorize(self.sid, self.request.remote_ip, self.get_argument("key")) if not self.user.authorized: self.append("error", api.returns.ErrorReturn(403, "Authorization failed.")) request_ok = False else: self.sid = self.user.request_sid if self.auth_required and not self.user: self.append("error", api.returns.ErrorReturn(403, "Authorization required.")) request_ok = False if self.user and request_ok: if self.login_required and not user.is_anonymous(): self.append("error", api.returns.ErrorReturn(-1001, "Login required for %s." % url)) request_ok = False if self.tunein_required and not user.is_tunedin(): self.append("error", api.returns.ErrorReturn(-1001, "You must be tuned in to use %s." % url)) request_ok = False if self.admin_required and not user.is_admin(): self.append("error", api.returns.ErrorReturn(-1001, "You must be an admin to use %s." % url)) request_ok = False if self.dj_required and not user.is_dj(): self.append("error", api.returns.ErrorReturn(-1001, "You must be DJing to use %s." % url)) request_ok = False return request_ok
def prepare(self): super(MainIndex, self).prepare() self.json_payload = {} self.jsfiles = None if not self.user: self.user = User(1) self.user.ensure_api_key() if self.beta or config.get("web_developer_mode") or config.get("developer_mode") or config.get("test_mode"): buildtools.bake_beta_css() buildtools.bake_beta_templates() self.jsfiles = [] for root, subdirs, files in os.walk( os.path.join(os.path.dirname(__file__), "../static/%s" % self.js_dir) ): # pylint: disable=W0612 for f in files: if f.endswith(".js"): self.jsfiles.append(os.path.join(root[root.find("static/%s" % self.js_dir) :], f))
def prepare(self): if self.path_kwargs.get('station'): self.sid = config.stream_filename_to_sid.get(self.path_kwargs['station']) if not self.sid: self.redirect(config.get("base_site_url")) return super(MainIndex, self).prepare() if self.path_kwargs.get('station') is None: self.redirect("{}{}/".format(config.get("base_site_url"), config.station_mount_filenames[self.sid]), permanent=True) return if config.get("enforce_ssl") and self.request.protocol != "https": self.redirect("{}{}/".format(config.get("base_site_url"), config.station_mount_filenames[self.sid]), permanent=True) return if not config.get("developer_mode") and self.request.host != config.get("hostname"): self.redirect("{}{}/".format(config.get("base_site_url"), config.station_mount_filenames[self.sid]), permanent=True) return if not cache.get_station(self.sid, "sched_current"): raise APIException("server_just_started", "Rainwave is Rebooting, Please Try Again in a Few Minutes", http_code=500) # self.json_payload = {} self.jsfiles = None if not self.user: self.user = User(1) self.user.ip_address = self.request.remote_ip self.user.ensure_api_key() if self.beta or config.get("web_developer_mode") or config.get("developer_mode") or config.get("test_mode"): buildtools.bake_beta_css() buildtools.bake_beta_templates() self.jsfiles = [] for root, subdirs, files in os.walk(os.path.join(os.path.dirname(__file__), "../static/%s" % self.js_dir)): #pylint: disable=W0612 for f in files: if f.endswith(".js"): self.jsfiles.append(os.path.join(root[root.find("static/%s" % self.js_dir):], f))
def get_next(sid): # TODO: Code review line = cache.get_station(sid, "request_line") song = None for pos in range(0, len(line)): if not line['song_id']: pass else: entry = line.pop(pos) song = playlist.Song.load_from_id(entry['song_id'], sid) song.data['elec_request_user_id'] = entry['user_id'] song.data['elec_request_username'] = entry['username'] db.c.update("DELETE FROM r4_request_store WHERE user_id = %s AND song_id = %s", (u.id, request['song_id'])) u = User(entry['user_id']) u.remove_from_request_line() if u.has_requests(): u.put_in_request_line(u.get_top_request_sid()) cache.set_station(sid, "request_line", line) break return song
def do_phpbb_auth(self): phpbb_cookie_name = config.get("phpbb_cookie_name") self.user = None if not fieldtypes.integer(self.get_cookie(phpbb_cookie_name + "u", "")): self.user = User(1) else: user_id = int(self.get_cookie(phpbb_cookie_name + "u")) if self._verify_phpbb_session(user_id): # update_phpbb_session is done by verify_phpbb_session if successful self.user = User(user_id) self.user.authorize(self.sid, None, None, True) return True if not self.user and self.get_cookie(phpbb_cookie_name + "k"): can_login = db.c_old.fetch_var("SELECT 1 FROM phpbb_sessions_keys WHERE key_id = %s AND user_id = %s", (hashlib.md5(self.get_cookie(phpbb_cookie_name + "k")).hexdigest(), user_id)) if can_login == 1: self._update_phpbb_session(self._get_phpbb_session(user_id)) self.user = User(user_id) self.user.authorize(self.sid, None, None, True) return True return False
class WSHandler(tornado.websocket.WebSocketHandler): is_websocket = True local_only = False help_hidden = False locale = None sid = None uuid = None def check_origin(self, origin): if websocket_allow_from == "*": return True parsed_origin = urlparse.urlparse(origin) return parsed_origin.netloc.endswith(websocket_allow_from) def open(self, *args, **kwargs): super(WSHandler, self).open(*args, **kwargs) try: self.sid = int(args[0]) except Exception: pass if not self.sid: self.write_message({ "wserror": { "tl_key": "missing_station_id", "text": self.locale.translate("missing_station_id") } }) return if not self.sid in config.station_ids: self.write_message({ "wserror": { "tl_key": "invalid_station_id", "text": self.locale.translate("invalid_station_id") } }) return self.locale = get_browser_locale(self) self.authorized = False self.msg_times = [] self.throttled = False self.throttled_msgs = [] def rw_finish(self, *args, **kwargs): self.close() def keep_alive(self): self.write_message({ "ping": { "timestamp": timestamp() }}) def on_close(self): global sessions self.throttled_msgs = [] if self.sid: sessions[self.sid].remove(self) super(WSHandler, self).on_close() def write_message(self, obj, *args, **kwargs): message = json.dumps(obj) try: super(WSHandler, self).write_message(message, *args, **kwargs) except tornado.websocket.WebSocketClosedError: self.on_close() except tornado.websocket.WebSocketError as e: log.exception("websocket", "WebSocket Error", e) try: self.close() except Exception: self.on_close() def refresh_user(self): self.user.refresh(self.sid) # TODO: DJ permission checks def process_throttle(self): if not len(self.throttled_msgs): self.throttled = False return self.throttled_msgs.sort() # log.debug("throttle", "Throttled with %s messages" % len(self.throttled_msgs)) action = self.throttled_msgs[0]['action'] msg = None if not action in nonunique_actions: msgs = [ m for m in self.throttled_msgs if m['action'] == action ] msg = msgs.pop() for m in msgs: if "message_id" in m and fieldtypes.zero_or_greater_integer(m['message_id']): self.write_message({ "wsthrottle": { "tl_key": "websocket_throttle", "text": self.locale.translate("websocket_throttle") }, "message_id": { "message_id": fieldtypes.zero_or_greater_integer(m['message_id']), "success": False, "tl_key": "websocket_throttle" } }) self.throttled_msgs = [ m for m in self.throttled_msgs if m['action'] != action ] # log.debug("throttle", "Handling last throttled %s message." % action) else: msg = self.throttled_msgs.pop(0) # log.debug("throttle", "Handling last throttled %s message." % action) if msg: self._process_message(msg, is_throttle_process=True) tornado.ioloop.IOLoop.instance().add_timeout(datetime.timedelta(seconds=0.5), self.process_throttle) def should_vote_throttle(self): if not self.votes_by_key in votes_by: return 0 vote_limit = 3 # log.debug("vote_throttle", "%s - %s - %s" % (vote_limit, votes_by[self.votes_by_key], (timestamp() < (last_vote_by[self.votes_by_key] + vote_once_every_seconds)))) if (votes_by[self.votes_by_key] >= vote_limit) and (timestamp() < (last_vote_by[self.votes_by_key] + vote_once_every_seconds)): # log.debug("vote_throttle", (last_vote_by[self.votes_by_key] + vote_once_every_seconds) - timestamp()) return (last_vote_by[self.votes_by_key] + vote_once_every_seconds) - timestamp() return 0 def on_message(self, message_text): try: message = WSMessage() message.update(json.loads(message_text)) except: self.write_message({ "wserror": { "tl_key": "invalid_json", "text": self.locale.translate("invalid_json") } }) return if not "action" in message or not message['action']: self.write_message({ "wserror": { "tl_key": "missing_argument", "text": self.locale.translate("missing_argument", argument="action") } }) return if not self.authorized and message['action'] != "auth": self.write_message({ "wserror": { "tl_key": "auth_required", "text": self.locale.translate("auth_required") } }) return if not self.authorized and message['action'] == "auth": self._do_auth(message) return if message['action'] == "vote": if not message['entry_id']: self.write_message({ "wserror": { "tl_key": "missing_argument", "text": self.locale.translate("missing_argument", argument="entry_id") } }) elif not fieldtypes.integer(message['entry_id']): self.write_message({ "wserror": { "tl_key": "invalid_argument", "text": self.locale.translate("missing_argument", argument="entry_id", reason=fieldtypes.integer_error) } }) message['elec_id'] = rainwave.schedule.get_elec_id_for_entry(self.sid, message['entry_id']) self._process_message(message) def _process_message(self, message, is_throttle_process=False): message_id = None if "message_id" in message: message_id = fieldtypes.zero_or_greater_integer(message['message_id']) throt_t = timestamp() - 3 self.msg_times = [ t for t in self.msg_times if t > throt_t ] if not is_throttle_process: self.msg_times.append(timestamp()) # log.debug("throttle", "%s - %s" % (len(self.msg_times), message['action'])) if self.throttled: # log.debug("throttle", "Currently throttled, adding to queue.") self.throttled_msgs.append(message) return elif len(self.msg_times) >= 5: # log.debug("throttle", "Too many messages, throttling.") self.throttled = True self.throttled_msgs.append(message) tornado.ioloop.IOLoop.instance().add_timeout(datetime.timedelta(seconds=0.5), self.process_throttle) return if message['action'] == "ping": self.write_message({ "pong": { "timestamp": timestamp() } }) return if message['action'] == "pong": self.write_message({ "pongConfirm": { "timestamp": timestamp() } }) return if message['action'] == "vote": zeromq.publish({ "action": "vote_by", "by": self.votes_by_key }) if message['action'] == "check_sched_current_id": self._do_sched_check(message) return message['action'] = "/api4/%s" % message['action'] if not message['action'] in api_endpoints: self.write_message({ "wserror": { "tl_key": "websocket_404", "text": self.locale.translate("websocket_404") } }) return endpoint = api_endpoints[message['action']](websocket=True) endpoint.locale = self.locale endpoint.request = FakeRequestObject(message, self.request.cookies) endpoint.sid = message['sid'] if ('sid' in message and message['sid']) else self.sid endpoint.user = self.user #pylint: disable=W0212 try: startclock = timestamp() # TODO: this should be a part of prepare_standalone! # it's required to see if another person on the same IP address has overriden the vote # for the in-memory user here, so it requires a DB fetch. if message['action'] == "/api4/vote" and self.user.is_anonymous(): self.user.refresh(self.sid) if "message_id" in message: if message_id == None: endpoint.prepare_standalone() raise APIException("invalid_argument", argument="message_id", reason=fieldtypes.zero_or_greater_integer_error, http_code=400) endpoint.prepare_standalone(message_id) else: endpoint.prepare_standalone() endpoint.post() endpoint.append("api_info", { "exectime": timestamp() - startclock, "time": round(timestamp()) }) if endpoint.sync_across_sessions: if endpoint.return_name in endpoint._output and isinstance(endpoint._output[endpoint.return_name], dict) and not endpoint._output[endpoint.return_name]['success']: pass else: zeromq.publish({ "action": "result_sync", "sid": self.sid, "user_id": self.user.id, "data": endpoint._output, "uuid_exclusion": self.uuid }) if message['action'] == "/api4/vote" and endpoint.return_name in endpoint._output and isinstance(endpoint._output[endpoint.return_name], dict) and endpoint._output[endpoint.return_name]['success']: live_voting = rainwave.schedule.update_live_voting(self.sid) endpoint.append("live_voting", live_voting) if self.should_vote_throttle(): zeromq.publish({ "action": "delayed_live_voting", "sid": self.sid, "uuid_exclusion": self.uuid, "data": { "live_voting": live_voting } }) else: zeromq.publish({ "action": "live_voting", "sid": self.sid, "uuid_exclusion": self.uuid, "data": { "live_voting": live_voting } }) except Exception as e: endpoint.write_error(500, exc_info=sys.exc_info(), no_finish=True) log.exception("websocket", "API Exception during operation.", e) finally: self.write_message(endpoint._output) #pylint: enable=W0212 def update(self): handler = APIHandler(websocket=True) handler.locale = self.locale handler.request = FakeRequestObject({}, self.request.cookies) handler.sid = self.sid handler.user = self.user handler.return_name = "sync_result" try: startclock = timestamp() handler.prepare_standalone() if not cache.get_station(self.sid, "backend_ok"): raise APIException("station_offline") self.refresh_user() api_requests.info.attach_info_to_request(handler, live_voting=True) if self.user.is_dj(): api_requests.info.attach_dj_info_to_request(handler) handler.append("user", self.user.to_private_dict()) handler.append("api_info", { "exectime": timestamp() - startclock, "time": round(timestamp()) }) except Exception as e: if handler: handler.write_error(500, exc_info=sys.exc_info(), no_finish=True) log.exception("websocket", "Exception during update.", e) finally: if handler: self.write_message(handler._output) #pylint: disable=W0212 def update_user(self): self.write_message({ "user": self.user.to_private_dict() }) def login_mixup_warn(self): self.write_message({ "sync_result": { "tl_key": "redownload_m3u", "text": self.locale.translate("redownload_m3u") } }) def _do_auth(self, message): try: if not "user_id" in message or not message['user_id']: self.write_message({ "wserror": { "tl_key": "missing_argument", "text": self.locale.translate("missing_argument", argument="user_id") } }) if not isinstance(message['user_id'], numbers.Number): self.write_message({ "wserror": { "tl_key": "invalid_argument", "text": self.locale.translate("invalid_argument", argument="user_id") } }) if not "key" in message or not message['key']: self.write_message({ "wserror": { "tl_key": "missing_argument", "text": self.locale.translate("missing_argument", argument="key") } }) self.user = User(message['user_id']) self.user.ip_address = self.request.remote_ip self.user.authorize(None, message['key']) if not self.user.authorized: self.write_message({ "wserror": { "tl_key": "auth_failed", "text": self.locale.translate("auth_failed") } }) self.close() return self.authorized = True self.uuid = str(uuid.uuid4()) global sessions sessions[self.sid].append(self) self.votes_by_key = self.request.remote_ip if self.user.is_anonymous() else self.user.id self.refresh_user() # no need to send the user's data to the user as that would have come with bootstrap # and will come with each synchronization of the schedule anyway self.write_message({ "wsok": True }) # since this will be the first action in any websocket interaction though, # it'd be a good time to send a station offline message. self._station_offline_check() except Exception as e: log.exception("websocket", "Exception during authentication.", e) self.close() def _station_offline_check(self): if not cache.get_station(self.sid, "backend_ok"): # shamelessly fake an error. self.write_message({ "sync_result": { "tl_key": "station_offline", "text": self.locale.translate("station_offline") } }) def _do_sched_check(self, message): if not "sched_id" in message or not message['sched_id']: self.write_message({ "wserror": { "tl_key": "missing_argument", "text": self.locale.translate("missing_argument", argument="sched_id") } }) return if not isinstance(message['sched_id'], numbers.Number): self.write_message({ "wserror": { "tl_key": "invalid_argument", "text": self.locale.translate("invalid_argument", argument="sched_id") } }) self._station_offline_check() if cache.get_station(self.sid, "sched_current_dict") and (cache.get_station(self.sid, "sched_current_dict")['id'] != message['sched_id']): self.update() self.write_message({ "outdated_data_warning": { "outdated": True } })
def _process_line(line, sid): new_line = [] # user_positions has user_id as a key and position as the value, this is cached for quick lookups by API requests # so users know where they are in line user_positions = {} t = int(timestamp()) albums_with_requests = [] position = 1 user_viewable_position = 1 valid_positions = 0 # For each person for row in line: add_to_line = False u = User(row['user_id']) row['song_id'] = None # If their time is up, remove them and don't add them to the new line if row['line_expiry_tune_in'] and row['line_expiry_tune_in'] <= t: log.debug("request_line", "%s: Removed user ID %s from line for tune in timeout, expiry time %s current time %s" % (sid, u.id, row['line_expiry_tune_in'], t)) u.remove_from_request_line() else: tuned_in_sid = db.c.fetch_var("SELECT sid FROM r4_listeners WHERE user_id = %s AND sid = %s AND listener_purge = FALSE", (u.id, sid)) tuned_in = True if tuned_in_sid == sid else False if tuned_in: # Get their top song ID song_id = u.get_top_request_song_id(sid) if song_id and not row['line_has_had_valid']: row['line_has_had_valid'] = True db.c.update("UPDATE r4_request_line SET line_has_had_valid = TRUE WHERE user_id = %s", (u.id, )) if row['line_has_had_valid']: valid_positions += 1 # If they have no song and their line expiry has arrived, boot 'em if not song_id and row['line_expiry_election'] and (row['line_expiry_election'] <= t): log.debug("request_line", "%s: Removed user ID %s from line for election timeout, expiry time %s current time %s" % (sid, u.id, row['line_expiry_election'], t)) u.remove_from_request_line() # Give them more chances if they still have requests # They'll get added to the line of whatever station they're tuned in to (if any!) if u.has_requests(): u.put_in_request_line(u.get_tuned_in_sid()) # If they have no song and they're in 2nd or 1st, start the expiry countdown elif not song_id and not row['line_expiry_election'] and position <= 2: log.debug("request_line", "%s: User ID %s has no valid requests, beginning boot countdown." % (sid, u.id)) row['line_expiry_election'] = t + 900 db.c.update("UPDATE r4_request_line SET line_expiry_election = %s WHERE user_id = %s", ((t + 900), row['user_id'])) add_to_line = True # Keep 'em in line else: log.debug("request_line", "%s: User ID %s is in line." % (sid, u.id)) if song_id: albums_with_requests.append(db.c.fetch_var("SELECT album_id FROM r4_songs WHERE song_id = %s", (song_id,))) row['song'] = db.c.fetch_row("SELECT song_id AS id, song_title AS title, album_name FROM r4_songs JOIN r4_albums USING (album_id) WHERE song_id = %s", (song_id,)) else: row['song'] = None row['song_id'] = song_id add_to_line = True elif not row['line_expiry_tune_in'] or row['line_expiry_tune_in'] == 0: log.debug("request_line", "%s: User ID %s being marked as tuned out." % (sid, u.id)) db.c.update("UPDATE r4_request_line SET line_expiry_tune_in = %s WHERE user_id = %s", ((t + 600), row['user_id'])) add_to_line = True else: log.debug("request_line", "%s: User ID %s not tuned in, waiting on expiry for action." % (sid, u.id)) add_to_line = True row['skip'] = not add_to_line row['position'] = user_viewable_position new_line.append(row) user_positions[u.id] = user_viewable_position user_viewable_position = user_viewable_position + 1 if add_to_line: position = position + 1 log.debug("request_line", "Request line valid positions: %s" % valid_positions) cache.set_station(sid, 'request_valid_positions', valid_positions) cache.set_station(sid, "request_line", new_line, True) cache.set_station(sid, "request_user_positions", user_positions, True) db.c.update("UPDATE r4_album_sid SET album_requests_pending = NULL WHERE album_requests_pending = TRUE AND sid = %s", (sid,)) for album_id in albums_with_requests: db.c.update("UPDATE r4_album_sid SET album_requests_pending = TRUE WHERE album_id = %s AND sid = %s", (album_id, sid)) return new_line
class MainIndex(api.web.HTMLRequest): description = "Main Rainwave page." auth_required = config.has("index_requires_login") and config.get( "index_requires_login") login_required = config.has("index_requires_login") and config.get( "index_requires_login") sid_required = False beta = False page_template = "r5_index.html" js_dir = "js5" def set_default_headers(self): self.set_header("X-Frame-Options", "SAMEORIGIN") def prepare(self): super(MainIndex, self).prepare() if not cache.get_station(self.sid, "sched_current"): raise APIException( "server_just_started", "Rainwave is Rebooting, Please Try Again in a Few Minutes", http_code=500) # self.json_payload = {} self.jsfiles = None if not self.user: self.user = User(1) self.user.ensure_api_key() if self.beta or config.get("web_developer_mode") or config.get( "developer_mode") or config.get("test_mode"): buildtools.bake_beta_css() buildtools.bake_beta_templates() self.jsfiles = [] for root, subdirs, files in os.walk( os.path.join(os.path.dirname(__file__), "../static/%s" % self.js_dir)): #pylint: disable=W0612 for f in files: if f.endswith(".js"): self.jsfiles.append( os.path.join( root[root.find("static/%s" % self.js_dir):], f)) # def append(self, key, value): # self.json_payload[key] = value def get(self): self.mobile = self.request.headers.get("User-Agent").lower().find( "mobile") != -1 or self.request.headers.get( "User-Agent").lower().find("android") != -1 # if not self.beta: # info.attach_info_to_request(self, extra_list=self.get_cookie("r4_active_list")) # self.append("api_info", { "time": int(timestamp()) }) page_title = None if (self.sid == config.get("default_station")): page_title = self.locale.translate("page_title_on_google") else: page_title = "%s %s" % ( self.locale.translate("page_title_on_google"), self.locale.translate("station_name_%s" % self.sid)) self.render( self.page_template, request=self, site_description=self.locale.translate( "station_description_id_%s" % self.sid), revision_number=config.build_number, jsfiles=self.jsfiles, # api_url=config.get("api_external_url_prefix"), # cookie_domain=config.get("cookie_domain"), # locales=api.locale.locale_names_json, # relays=config.public_relays_json[self.sid], # stream_filename=config.get_station(self.sid, "stream_filename"), # station_list=config.station_list_json, mobile=self.mobile, station_name=page_title, dj=self.user.is_dj())
class RainwaveHandler(tornado.web.RequestHandler): # The following variables can be overridden by you. # Fields is a hash with { "form_name" => (fieldtypes.[something], True|False|None) } format, so that automatic form validation can be done for you. True/False values are for required/optional. # A True requirement means it must be present and valid # A False requirement means it is optional, but if present, must be valid # A None requirement means it is optional, and if present and invalid, will be set to None fields = {} # This URL variable is setup by the server decorator - DON'T TOUCH IT. url = False # Do we need a Rainwave auth key for this request? auth_required = True # return_name is used for documentation, can be an array. # If not inherited, return_key automatically turns into url + "_result". Useful for simple requests like rate, vote, etc. return_name = None # Validate user's tuned in status first. tunein_required = False # Validate user's logged in status first. login_required = False # User must be a DJ for the next, current, or history[0] event dj_required = False # User must have an unused DJ-able event in the future dj_preparation = False # Validate user is a station administrator. admin_required = False # Do we need a valid SID as part of the submitted form? sid_required = True # Description string for documentation. description = "Undocumented." # Error codes for documentation. return_codes = None # Restricts requests to config.get("api_trusted_ip_addresses") (presumably 127.0.0.1) local_only = False # Should the user be free to vote and rate? unlocked_listener_only = False # Do we allow GET HTTP requests to this URL? (standard is "no") allow_get = False # Use phpBB session/token auth? phpbb_auth = False # Does the user need perks (donor/beta/etc) to see this request/page? perks_required = False # hide from help, meant really only for things like redirect pages help_hidden = False # automatically add pagination to an API request. use self.get_sql_limit_string()! pagination = False # allow access to station ID 0 allow_sid_zero = False # set to allow from any source allow_cors = False # sync result across all user's websocket sessions sync_across_sessions = False def __init__(self, *args, **kwargs): if not 'websocket' in kwargs: super(RainwaveHandler, self).__init__(*args, **kwargs) self.cleaned_args = {} self.sid = None self._startclock = timestamp() self.user = None self._output = None self._output_array = False self.mobile = False def initialize(self, **kwargs): super(RainwaveHandler, self).initialize(**kwargs) if self.pagination: self.fields['per_page'] = (fieldtypes.zero_or_greater_integer, False) self.fields['page_start'] = (fieldtypes.zero_or_greater_integer, False) def set_cookie(self, name, value, *args, **kwargs): if isinstance(value, (int, long)): value = repr(value) super(RainwaveHandler, self).set_cookie(name, value, *args, **kwargs) def get_argument(self, name, default=None, **kwargs): if name in self.cleaned_args: return self.cleaned_args[name] if name in self.request.arguments: if isinstance(self.request.arguments[name], list): return self.request.arguments[name][-1].strip() return self.request.arguments[name] return default def set_argument(self, name, value): self.cleaned_args[name] = value def get_browser_locale(self, default="en_CA"): return get_browser_locale(self, default) def setup_output(self): if not self.return_name: self.return_name = self.url[self.url.rfind("/") + 1:] + "_result" else: self.return_name = self.return_name def arg_parse(self): for field, field_attribs in self.__class__.fields.iteritems(): type_cast, required = field_attribs parsed = None if required and field not in self.request.arguments: raise APIException("missing_argument", argument=field, http_code=400) elif field in self.request.arguments: parsed = type_cast(self.get_argument(field), self) if parsed == None and required != None: raise APIException( "invalid_argument", argument=field, reason="%s %s" % (field, getattr(fieldtypes, "%s_error" % type_cast.__name__)), http_code=400) self.cleaned_args[field] = parsed def sid_check(self): if self.sid is None and not self.sid_required: self.sid = config.get("default_station") if self.sid == 0 and self.allow_sid_zero: pass elif not self.sid in config.station_ids: raise APIException("invalid_station_id", http_code=400) def permission_checks(self): if (self.login_required or self.admin_required or self.dj_required) and (not self.user or self.user.is_anonymous()): raise APIException("login_required", http_code=403) if self.tunein_required and (not self.user or not self.user.is_tunedin()): raise APIException("tunein_required", http_code=403) if self.admin_required and (not self.user or not self.user.is_admin()): raise APIException("admin_required", http_code=403) if self.perks_required and (not self.user or not self.user.has_perks()): raise APIException("perks_required", http_code=403) if self.unlocked_listener_only and not self.user: raise APIException("auth_required", http_code=403) elif self.unlocked_listener_only and self.user.data[ 'lock'] and self.user.data['lock_sid'] != self.sid: raise APIException( "unlocked_only", station=config.station_id_friendly[self.user.data['lock_sid']], lock_counter=self.user.data['lock_counter'], http_code=403) is_dj = False if self.dj_required and not self.user: raise APIException("dj_required", http_code=403) if self.dj_required and not self.user.is_admin(): potential_djs = cache.get_station(self.sid, "dj_user_ids") if not potential_djs or not self.user.id in potential_djs: raise APIException("dj_required", http_code=403) is_dj = True self.user.data['dj'] = True elif self.dj_required and self.user.is_admin(): is_dj = True self.user.data['dj'] = True if self.dj_preparation and not is_dj and not self.user.is_admin(): if not db.c.fetch_var( "SELECT COUNT(*) FROM r4_schedule WHERE sched_used = 0 AND sched_dj_user_id = %s", (self.user.id, )): raise APIException("dj_required", http_code=403) # Called by Tornado, allows us to setup our request as we wish. User handling, form validation, etc. take place here. def prepare(self): if self.local_only and not self.request.remote_ip in config.get( "api_trusted_ip_addresses"): log.info( "api", "Rejected %s request from %s, untrusted address." % (self.url, self.request.remote_ip)) raise APIException( "rejected", text="You are not coming from a trusted address.") if self.allow_cors: self.set_header("Access-Control-Allow-Origin", "*") self.set_header("Access-Control-Max-Age", "600") self.set_header("Access-Control-Allow-Credentials", "false") if not isinstance(self.locale, locale.RainwaveLocale): self.locale = self.get_browser_locale() self.setup_output() if 'in_order' in self.request.arguments: self._output = [] self._output_array = True else: self._output = {} self.sid = fieldtypes.integer(self.get_cookie("r4_sid", None)) hostname = self.request.headers.get('Host', None) if hostname: hostname = unicode(hostname).split(":")[0] if hostname in config.station_hostnames: self.sid = config.station_hostnames[hostname] sid_arg = fieldtypes.integer(self.get_argument("sid", None)) if sid_arg is not None: self.sid = sid_arg if self.sid is None and self.sid_required: raise APIException("missing_station_id", http_code=400) self.arg_parse() self.sid_check() if self.sid: self.set_cookie("r4_sid", str(self.sid), expires_days=365, domain=config.get("cookie_domain")) if self.phpbb_auth: self.do_phpbb_auth() else: self.rainwave_auth() if not self.user and self.auth_required: raise APIException("auth_required", http_code=403) elif not self.user and not self.auth_required: self.user = User(1) self.user.ip_address = self.request.remote_ip self.user.refresh(self.sid) if self.user and config.get("store_prefs"): self.user.save_preferences(self.request.remote_ip, self.get_cookie("r4_prefs", None)) self.permission_checks() # works without touching cookies or headers, primarily used for websocket requests def prepare_standalone(self, message_id=None): self._output = {} if message_id != None: self.append("message_id", {"message_id": message_id}) self.setup_output() self.arg_parse() self.sid_check() self.permission_checks() def do_phpbb_auth(self): phpbb_cookie_name = config.get("phpbb_cookie_name") + "_" user_id = fieldtypes.integer( self.get_cookie(phpbb_cookie_name + "u", "")) if not user_id: pass else: if self._verify_phpbb_session(user_id): # update_phpbb_session is done by verify_phpbb_session if successful self.user = User(user_id) self.user.ip_address = self.request.remote_ip self.user.authorize(self.sid, None, bypass=True) return True if not self.user and self.get_cookie(phpbb_cookie_name + "k"): can_login = db.c.fetch_var( "SELECT 1 FROM phpbb_sessions_keys WHERE key_id = %s AND user_id = %s", (hashlib.md5(self.get_cookie(phpbb_cookie_name + "k")).hexdigest(), user_id)) if can_login == 1: self._update_phpbb_session( self._get_phpbb_session(user_id)) self.user = User(user_id) self.user.ip_address = self.request.remote_ip self.user.authorize(self.sid, None, bypass=True) return True return False def _verify_phpbb_session(self, user_id=None): # TODO: Do we want to enhance this with IP checking and other bits and pieces like phpBB does? if not user_id and not self.user: return None if not user_id: user_id = self.user.id cookie_session = self.get_cookie( config.get("phpbb_cookie_name") + "_sid") if cookie_session: if cookie_session == db.c.fetch_var( "SELECT session_id FROM phpbb_sessions WHERE session_user_id = %s AND session_id = %s", (user_id, cookie_session)): self._update_phpbb_session(cookie_session) return cookie_session return None def _get_phpbb_session(self, user_id=None): return db.c.fetch_var( "SELECT session_id FROM phpbb_sessions WHERE session_user_id = %s ORDER BY session_last_visit DESC LIMIT 1", (user_id, )) def _update_phpbb_session(self, session_id): db.c.update( "UPDATE phpbb_sessions SET session_last_visit = %s, session_page = %s WHERE session_id = %s", (int(timestamp()), "rainwave", session_id)) def rainwave_auth(self): user_id_present = "user_id" in self.request.arguments if self.auth_required and not user_id_present: raise APIException("missing_argument", argument="user_id", http_code=400) if user_id_present and not fieldtypes.numeric( self.get_argument("user_id")): # do not spit out the user ID back at them, that would create a potential XSS hack raise APIException("invalid_argument", argument="user_id", reason="not numeric.", http_code=400) if (self.auth_required or user_id_present) and not "key" in self.request.arguments: raise APIException("missing_argument", argument="key", http_code=400) if user_id_present: self.user = User(long(self.get_argument("user_id"))) self.user.ip_address = self.request.remote_ip self.user.authorize(self.sid, self.get_argument("key")) if not self.user.authorized: raise APIException("auth_failed", http_code=403) else: self._update_phpbb_session( self._get_phpbb_session(self.user.id)) # Handles adding dictionaries for JSON output # Will return a "code" if it exists in the hash passed in, if not, returns True def append(self, key, dct): if dct == None: return if self._output_array: self._output.append({key: dct}) else: self._output[key] = dct if isinstance(dct, dict) and "code" in dct: return dct["code"] return True def append_standard(self, tl_key, text=None, success=True, return_name=None, **kwargs): if not text: text = self.locale.translate(tl_key, **kwargs) kwargs.update({"success": success, "tl_key": tl_key, "text": text}) if return_name: self.append(return_name, kwargs) else: self.append(self.return_name, kwargs) def write_error(self, status_code, **kwargs): if kwargs.has_key("exc_info"): exc = kwargs['exc_info'][1] if isinstance(exc, APIException): exc.localize(self.locale) log.debug("exception", exc.reason) def get_sql_limit_string(self): if not self.pagination: return "" limit = "" if self.get_argument("per_page") != None: if not self.get_argument("per_page"): limit = "LIMIT ALL" else: limit = "LIMIT %s" % self.get_argument("per_page") else: limit = "LIMIT 100" if self.get_argument("page_start"): limit += " OFFSET %s" % self.get_argument("page_start") return limit
class RainwaveHandler(tornado.web.RequestHandler): # The following variables can be overridden by you. # Fields is a hash with { "form_name" => (fieldtypes.[something], True|False|None } format, so that automatic form validation can be done for you. True/False values are for required/optional. # A True requirement means it must be present and valid # A False requirement means it is optional, but if present, must be valid # A None requirement means it is optional, and if present and invalid, will be set to None fields = {} # This URL variable is setup by the server decorator - DON'T TOUCH IT. url = False # Do we need a Rainwave auth key for this request? auth_required = True # return_name is used for documentation, can be an array. # If not inherited, return_key automatically turns into url + "_result". Useful for simple requests like rate, vote, etc. return_name = None # Validate user's tuned in status first. tunein_required = False # Validate user's logged in status first. login_required = False # Validate user is a station administrator. admin_required = False # Validate user is currently DJing. dj_required = False # Do we need a valid SID as part of the submitted form? sid_required = True # Description string for documentation. description = "Undocumented." # Error codes for documentation. return_codes = None # Restricts requests to config.get("api_trusted_ip_addresses") (presumably 127.0.0.1) local_only = False # Should the user be free to vote and rate? unlocked_listener_only = False # Do we allow GET HTTP requests to this URL? (standard is "no") allow_get = False # Use phpBB session/token auth? phpbb_auth = False # Does the user need perks (donor/beta/etc) to see this request/page? perks_required = False # hide from help, meant really only for things like redirect pages help_hidden = False # automatically add pagination to an API request. use self.get_sql_limit_string()! pagination = False def __init__(self, *args, **kwargs): super(RainwaveHandler, self).__init__(*args, **kwargs) self.cleaned_args = {} self.cookie_prefs = {} self.sid = None self._startclock = time.time() self.user = None self._output = None self._output_array = False self.mobile = False def initialize(self, **kwargs): super(RainwaveHandler, self).initialize(**kwargs) if self.pagination: self.fields['per_page'] = (fieldtypes.zero_or_greater_integer, False) self.fields['page_start'] = (fieldtypes.zero_or_greater_integer, False) def set_cookie(self, name, value, *args, **kwargs): if isinstance(value, (int, long)): value = repr(value) super(RainwaveHandler, self).set_cookie(name, value, *args, **kwargs) def get_argument(self, name, *args, **kwargs): if name in self.cleaned_args: return self.cleaned_args[name] return super(RainwaveHandler, self).get_argument(name, *args, **kwargs) def set_argument(self, name, value): self.cleaned_args[name] = value def get_browser_locale(self, default="en_CA"): """Determines the user's locale from ``Accept-Language`` header. Copied from Tornado, adapted slightly. See http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.4 """ if "rw_lang" in self.cookies: if locale.RainwaveLocale.exists(self.cookies['rw_lang'].value): return locale.RainwaveLocale.get(self.cookies['rw_lang'].value) if "Accept-Language" in self.request.headers: languages = self.request.headers["Accept-Language"].split(",") locales = [] for language in languages: parts = language.strip().split(";") if len(parts) > 1 and parts[1].startswith("q="): try: score = float(parts[1][2:]) except (ValueError, TypeError): score = 0.0 else: score = 1.0 locales.append((parts[0], score)) if locales: locales.sort(key=lambda pair: pair[1], reverse=True) codes = [l[0] for l in locales] return locale.RainwaveLocale.get_closest(codes) return locale.RainwaveLocale.get(default) # Called by Tornado, allows us to setup our request as we wish. User handling, form validation, etc. take place here. def prepare(self): if self.local_only and not self.request.remote_ip in config.get( "api_trusted_ip_addresses"): log.info( "api", "Rejected %s request from %s, untrusted address." % (self.url, self.request.remote_ip)) raise APIException( "rejected", text="You are not coming from a trusted address.") if not isinstance(self.locale, locale.RainwaveLocale): self.locale = self.get_browser_locale() if not self.return_name: self.return_name = self.url[self.url.rfind("/") + 1:] + "_result" else: self.return_name = self.return_name if self.admin_required or self.dj_required: self.login_required = True if 'in_order' in self.request.arguments: self._output = [] self._output_array = True else: self._output = {} self.sid = fieldtypes.integer(self.get_cookie("r4_sid", None)) hostname = self.request.headers.get('Host', None) if hostname: hostname = unicode(hostname).split(":")[0] if hostname in config.station_hostnames: self.sid = config.station_hostnames[hostname] self.sid = fieldtypes.integer(self.get_argument("sid", None)) or self.sid if self.sid and not self.sid in config.station_ids: self.sid = None if not self.sid and self.sid_required: raise APIException("missing_station_id", http_code=400) for field, field_attribs in self.__class__.fields.iteritems(): type_cast, required = field_attribs if required and field not in self.request.arguments: raise APIException("missing_argument", argument=field, http_code=400) elif not required and field not in self.request.arguments: self.cleaned_args[field] = None else: parsed = type_cast(self.get_argument(field), self) if parsed == None and required != None: raise APIException( "invalid_argument", argument=field, reason="%s %s" % (field, getattr(fieldtypes, "%s_error" % type_cast.__name__)), http_code=400) else: self.cleaned_args[field] = parsed if not self.sid and not self.sid_required: self.sid = 5 if not self.sid in config.station_ids: raise APIException("invalid_station_id", http_code=400) self.set_cookie("r4_sid", str(self.sid), expires_days=365, domain=config.get("cookie_domain")) if self.phpbb_auth: self.do_phpbb_auth() else: self.rainwave_auth() if not self.user and self.auth_required: raise APIException("auth_required", http_code=403) elif not self.user and not self.auth_required: self.user = User(1) self.user.ip_address = self.request.remote_ip self.user.refresh(self.sid) if self.login_required and (not self.user or self.user.is_anonymous()): raise APIException("login_required", http_code=403) if self.tunein_required and (not self.user or not self.user.is_tunedin()): raise APIException("tunein_required", http_code=403) if self.admin_required and (not self.user or not self.user.is_admin()): raise APIException("admin_required", http_code=403) if self.dj_required and (not self.user or not self.user.is_dj()): raise APIException("dj_required", http_code=403) if self.perks_required and (not self.user or not self.user.has_perks()): raise APIException("perks_required", http_code=403) if self.unlocked_listener_only and not self.user: raise APIException("auth_required", http_code=403) elif self.unlocked_listener_only and self.user.data[ 'lock'] and self.user.data['lock_sid'] != self.sid: raise APIException( "unlocked_only", station=config.station_id_friendly[self.user.data['lock_sid']], lock_counter=self.user.data['lock_counter'], http_code=403) def do_phpbb_auth(self): phpbb_cookie_name = config.get("phpbb_cookie_name") user_id = fieldtypes.integer( self.get_cookie(phpbb_cookie_name + "u", "")) if not user_id: pass else: if self._verify_phpbb_session(user_id): # update_phpbb_session is done by verify_phpbb_session if successful self.user = User(user_id) self.user.ip_address = self.request.remote_ip self.user.authorize(self.sid, None, bypass=True) return True if not self.user and self.get_cookie(phpbb_cookie_name + "k"): can_login = db.c.fetch_var( "SELECT 1 FROM phpbb_sessions_keys WHERE key_id = %s AND user_id = %s", (hashlib.md5(self.get_cookie(phpbb_cookie_name + "k")).hexdigest(), user_id)) if can_login == 1: self._update_phpbb_session( self._get_phpbb_session(user_id)) self.user = User(user_id) self.user.ip_address = self.request.remote_ip self.user.authorize(self.sid, None, bypass=True) return True return False def _verify_phpbb_session(self, user_id=None): # TODO: Do we want to enhance this with IP checking and other bits and pieces like phpBB does? if not user_id and not self.user: return None if not user_id: user_id = self.user.id cookie_session = self.get_cookie( config.get("phpbb_cookie_name") + "sid") if cookie_session: if cookie_session == db.c.fetch_var( "SELECT session_id FROM phpbb_sessions WHERE session_user_id = %s AND session_id = %s", (user_id, cookie_session)): self._update_phpbb_session(cookie_session) return cookie_session return None def _get_phpbb_session(self, user_id=None): return db.c.fetch_var( "SELECT session_id FROM phpbb_sessions WHERE session_user_id = %s ORDER BY session_last_visit DESC LIMIT 1", (user_id, )) def _update_phpbb_session(self, session_id): db.c.update( "UPDATE phpbb_sessions SET session_last_visit = %s, session_page = %s WHERE session_id = %s", (int(time.time()), "rainwave", session_id)) def rainwave_auth(self): user_id_present = "user_id" in self.request.arguments if self.auth_required and not user_id_present: raise APIException("missing_argument", argument="user_id", http_code=400) if user_id_present and not fieldtypes.numeric( self.get_argument("user_id")): # do not spit out the user ID back at them, that would create a potential XSS hack raise APIException("invalid_argument", argument="user_id", reason="not numeric.", http_code=400) if (self.auth_required or user_id_present) and not "key" in self.request.arguments: raise APIException("missing_argument", argument="key", http_code=400) if user_id_present: self.user = User(long(self.get_argument("user_id"))) self.user.ip_address = self.request.remote_ip self.user.authorize(self.sid, self.get_argument("key")) if not self.user.authorized: raise APIException("auth_failed", http_code=403) else: self._update_phpbb_session( self._get_phpbb_session(self.user.id)) # Handles adding dictionaries for JSON output # Will return a "code" if it exists in the hash passed in, if not, returns True def append(self, key, dct): if dct == None: return if self._output_array: self._output.append({key: dct}) else: self._output[key] = dct if "code" in dct: return dct["code"] return True def append_standard(self, tl_key, text=None, success=True, return_name=None, **kwargs): if not text: text = self.locale.translate(tl_key, **kwargs) kwargs.update({"success": success, "tl_key": tl_key, "text": text}) if return_name: self.append(return_name, kwargs) else: self.append(self.return_name, kwargs) def write_error(self, status_code, **kwargs): if kwargs.has_key("exc_info"): exc = kwargs['exc_info'][1] if isinstance(exc, APIException): exc.localize(self.locale) log.debug("exception", exc.reason) def get_sql_limit_string(self): if not self.pagination: return "" limit = "" if self.get_argument("per_page") != None: if self.get_argument("per_page") == "0": limit = "LIMIT ALL" else: limit = "LIMIT %s" % self.get_argument("per_page") else: limit = "LIMIT 100" if self.get_argument("page_start"): limit += " OFFSET %s" % self.get_argument("page_start") return limit
def prepare(self): super(Bootstrap, self).prepare() if not self.user: self.user = User(1) self.user.ensure_api_key()
def update_line(sid): # Get everyone in the line line = db.c.fetch_all( "SELECT username, user_id, line_expiry_tune_in, line_expiry_election, line_wait_start FROM r4_request_line JOIN phpbb_users USING (user_id) WHERE r4_request_line.sid = %s AND radio_requests_paused = FALSE ORDER BY line_wait_start", (sid, )) new_line = [] # user_positions has user_id as a key and position as the value, this is cached for quick lookups by API requests # so users know where they are in line user_positions = {} t = int(time.time()) albums_with_requests = [] position = 1 # For each person for row in line: add_to_line = False u = User(row['user_id']) row['song_id'] = None # If their time is up, remove them and don't add them to the new line if row['line_expiry_tune_in'] and row['line_expiry_tune_in'] <= t: log.debug( "request_line", "%s: Removed user ID %s from line for tune in timeout, expiry time %s current time %s" % (sid, u.id, row['line_expiry_tune_in'], t)) u.remove_from_request_line() else: tuned_in_sid = db.c.fetch_var( "SELECT sid FROM r4_listeners WHERE user_id = %s AND sid = %s AND listener_purge = FALSE", (u.id, sid)) tuned_in = True if tuned_in_sid == sid else False if tuned_in: # Get their top song ID song_id = u.get_top_request_song_id(sid) # If they have no song and their line expiry has arrived, boot 'em if not song_id and row['line_expiry_election'] and ( row['line_expiry_election'] <= t): log.debug( "request_line", "%s: Removed user ID %s from line for election timeout, expiry time %s current time %s" % (sid, u.id, row['line_expiry_election'], t)) u.remove_from_request_line() # Give them more chances if they still have requests # They'll get added to the line of whatever station they're tuned in to (if any!) if u.has_requests(): u.put_in_request_line(u.get_tuned_in_sid()) # If they have no song, start the expiry countdown elif not song_id and not row['line_expiry_election']: row['line_expiry_election'] = t + 900 db.c.update( "UPDATE r4_request_line SET line_expiry_election = %s WHERE user_id = %s", ((t + 900), row['user_id'])) add_to_line = True # Keep 'em in line else: if song_id: albums_with_requests.append( db.c.fetch_var( "SELECT album_id FROM r4_songs WHERE song_id = %s", (song_id, ))) row['song_id'] = song_id add_to_line = True elif not row['line_expiry_tune_in'] or row[ 'line_expiry_tune_in'] == 0: db.c.update( "UPDATE r4_request_line SET line_expiry_tune_in = %s WHERE user_id = %s", ((t + 900), row['user_id'])) add_to_line = True else: add_to_line = True if add_to_line: new_line.append(row) user_positions[u.id] = position position = position + 1 cache.set_station(sid, "request_line", new_line, True) cache.set_station(sid, "request_user_positions", user_positions, True) db.c.update( "UPDATE r4_album_sid SET album_requests_pending = NULL WHERE album_requests_pending = TRUE AND sid = %s", (sid, )) for album_id in albums_with_requests: db.c.update( "UPDATE r4_album_sid SET album_requests_pending = TRUE WHERE album_id = %s AND sid = %s", (album_id, sid)) return new_line
def _process_line(line, sid): new_line = [] # user_positions has user_id as a key and position as the value, this is cached for quick lookups by API requests # so users know where they are in line user_positions = {} t = int(timestamp()) albums_with_requests = [] position = 1 user_viewable_position = 1 valid_positions = 0 # For each person for row in line: add_to_line = False u = User(row["user_id"]) row["song_id"] = None # If their time is up, remove them and don't add them to the new line if row["line_expiry_tune_in"] and row["line_expiry_tune_in"] <= t: log.debug( "request_line", "%s: Removed user ID %s from line for tune in timeout, expiry time %s current time %s" % (sid, u.id, row["line_expiry_tune_in"], t), ) u.remove_from_request_line() else: tuned_in_sid = db.c.fetch_var( "SELECT sid FROM r4_listeners WHERE user_id = %s AND sid = %s AND listener_purge = FALSE", (u.id, sid), ) tuned_in = True if tuned_in_sid == sid else False if tuned_in: # Get their top song ID song_id = u.get_top_request_song_id(sid) if song_id and not row["line_has_had_valid"]: row["line_has_had_valid"] = True db.c.update( "UPDATE r4_request_line SET line_has_had_valid = TRUE WHERE user_id = %s", (u.id, ), ) if row["line_has_had_valid"]: valid_positions += 1 # If they have no song and their line expiry has arrived, boot 'em if (not song_id and row["line_expiry_election"] and (row["line_expiry_election"] <= t)): log.debug( "request_line", "%s: Removed user ID %s from line for election timeout, expiry time %s current time %s" % (sid, u.id, row["line_expiry_election"], t), ) u.remove_from_request_line() # Give them more chances if they still have requests # They'll get added to the line of whatever station they're tuned in to (if any!) if u.has_requests(): u.put_in_request_line(u.get_tuned_in_sid()) # If they have no song and they're in 2nd or 1st, start the expiry countdown elif not song_id and not row[ "line_expiry_election"] and position <= 2: log.debug( "request_line", "%s: User ID %s has no valid requests, beginning boot countdown." % (sid, u.id), ) row["line_expiry_election"] = t + 900 db.c.update( "UPDATE r4_request_line SET line_expiry_election = %s WHERE user_id = %s", ((t + 900), row["user_id"]), ) add_to_line = True # Keep 'em in line else: log.debug("request_line", "%s: User ID %s is in line." % (sid, u.id)) if song_id: albums_with_requests.append( db.c.fetch_var( "SELECT album_id FROM r4_songs WHERE song_id = %s", (song_id, ), )) row["song"] = db.c.fetch_row( "SELECT song_id AS id, song_title AS title, album_name FROM r4_songs JOIN r4_albums USING (album_id) WHERE song_id = %s", (song_id, ), ) else: row["song"] = None row["song_id"] = song_id add_to_line = True elif not row["line_expiry_tune_in"] or row[ "line_expiry_tune_in"] == 0: log.debug( "request_line", "%s: User ID %s being marked as tuned out." % (sid, u.id), ) db.c.update( "UPDATE r4_request_line SET line_expiry_tune_in = %s WHERE user_id = %s", ((t + 600), row["user_id"]), ) add_to_line = True else: log.debug( "request_line", "%s: User ID %s not tuned in, waiting on expiry for action." % (sid, u.id), ) add_to_line = True row["skip"] = not add_to_line row["position"] = user_viewable_position new_line.append(row) user_positions[u.id] = user_viewable_position user_viewable_position = user_viewable_position + 1 if add_to_line: position = position + 1 log.debug("request_line", "Request line valid positions: %s" % valid_positions) cache.set_station(sid, "request_valid_positions", valid_positions) cache.set_station(sid, "request_line", new_line, True) cache.set_station(sid, "request_user_positions", user_positions, True) db.c.update( "UPDATE r4_album_sid SET album_requests_pending = NULL WHERE album_requests_pending = TRUE AND sid = %s", (sid, ), ) for album_id in albums_with_requests: db.c.update( "UPDATE r4_album_sid SET album_requests_pending = TRUE WHERE album_id = %s AND sid = %s", (album_id, sid), ) return new_line