class WebLogController(RedditController): on_validation_error = staticmethod(abort_with_error) @csrf_exempt @validate( VRatelimit(rate_user=False, rate_ip=True, prefix='rate_weblog_'), level=VOneOf('level', ('error', )), logs=VValidatedJSON( 'logs', VValidatedJSON.ArrayOf( VValidatedJSON.PartialObject({ 'msg': VPrintable('msg', max_length=256), 'url': VPrintable('url', max_length=256), 'tag': VPrintable('tag', max_length=32), }))), ) def POST_message(self, level, logs): # Whitelist tags to keep the frontend from creating too many keys in statsd valid_frontend_log_tags = { 'unknown', 'jquery-migrate-bad-html', } # prevent simple CSRF by requiring a custom header if not request.headers.get('X-Loggit'): abort(403) uid = c.user._id if c.user_is_loggedin else '-' # only accept a maximum of 3 entries per request for log in logs[:3]: if 'msg' not in log or 'url' not in log: continue tag = 'unknown' if log.get('tag') in valid_frontend_log_tags: tag = log['tag'] g.stats.simple_event('frontend.error.' + tag) g.log.warning('[web frontend] %s: %s | U: %s FP: %s UA: %s', level, log['msg'], uid, log['url'], request.user_agent) VRatelimit.ratelimit(rate_user=False, rate_ip=True, prefix="rate_weblog_", seconds=10)
class WebLogController(RedditController): on_validation_error = staticmethod(abort_with_error) @csrf_exempt @validate( VRatelimit(rate_user=False, rate_ip=True, prefix='rate_weblog_'), level=VOneOf('level', ('error', )), logs=VValidatedJSON( 'logs', VValidatedJSON.ArrayOf( VValidatedJSON.PartialObject({ 'msg': VPrintable('msg', max_length=256), 'url': VPrintable('url', max_length=256), 'tag': VPrintable('tag', max_length=32), }))), ) def POST_message(self, level, logs): # Whitelist tags to keep the frontend from creating too many keys in statsd valid_frontend_log_tags = { 'unknown', 'jquery-migrate-bad-html', } # prevent simple CSRF by requiring a custom header if not request.headers.get('X-Loggit'): abort(403) uid = c.user._id if c.user_is_loggedin else '-' # only accept a maximum of 3 entries per request for log in logs[:3]: if 'msg' not in log or 'url' not in log: continue tag = 'unknown' if log.get('tag') in valid_frontend_log_tags: tag = log['tag'] g.stats.simple_event('frontend.error.' + tag) g.log.warning('[web frontend] %s: %s | U: %s FP: %s UA: %s', level, log['msg'], uid, log['url'], request.user_agent) VRatelimit.ratelimit(rate_user=False, rate_ip=True, prefix="rate_weblog_", seconds=10) def OPTIONS_report_cache_poisoning(self): """Send CORS headers for cache poisoning reports.""" if "Origin" not in request.headers: return origin = request.headers["Origin"] parsed_origin = UrlParser(origin) if not is_subdomain(parsed_origin.hostname, g.domain): return response.headers["Access-Control-Allow-Origin"] = origin response.headers["Access-Control-Allow-Methods"] = "POST" response.headers["Access-Control-Allow-Headers"] = \ "Authorization, X-Loggit, " response.headers["Access-Control-Allow-Credentials"] = "false" response.headers['Access-Control-Expose-Headers'] = \ self.COMMON_REDDIT_HEADERS @csrf_exempt @validate( VRatelimit(rate_user=False, rate_ip=True, prefix='rate_poison_'), report_mac=VPrintable('report_mac', 255), poisoner_name=VPrintable('poisoner_name', 255), poisoner_id=VInt('poisoner_id'), poisoner_canary=VPrintable('poisoner_canary', 2, min_length=2), victim_canary=VPrintable('victim_canary', 2, min_length=2), render_time=VInt('render_time'), route_name=VPrintable('route_name', 255), url=VPrintable('url', 2048), # To differentiate between web and mweb in the future source=VOneOf('source', ('web', 'mweb')), cache_policy=VOneOf( 'cache_policy', ('loggedin_www', 'loggedin_www_new', 'loggedin_mweb')), # JSON-encoded response headers from when our script re-requested # the poisoned page resp_headers=nop('resp_headers'), ) def POST_report_cache_poisoning( self, report_mac, poisoner_name, poisoner_id, poisoner_canary, victim_canary, render_time, route_name, url, source, cache_policy, resp_headers, ): """Report an instance of cache poisoning and its details""" self.OPTIONS_report_cache_poisoning() if c.errors: abort(400) # prevent simple CSRF by requiring a custom header if not request.headers.get('X-Loggit'): abort(403) # Eh? Why are you reporting this if the canaries are the same? if poisoner_canary == victim_canary: abort(400) expected_mac = make_poisoning_report_mac( poisoner_canary=poisoner_canary, poisoner_name=poisoner_name, poisoner_id=poisoner_id, cache_policy=cache_policy, source=source, route_name=route_name, ) if not constant_time_compare(report_mac, expected_mac): abort(403) if resp_headers: try: resp_headers = json.loads(resp_headers) # Verify this is a JSON map of `header_name => [value, ...]` if not isinstance(resp_headers, dict): abort(400) for hdr_name, hdr_vals in resp_headers.iteritems(): if not isinstance(hdr_name, basestring): abort(400) if not all(isinstance(h, basestring) for h in hdr_vals): abort(400) except ValueError: abort(400) if not resp_headers: resp_headers = {} poison_info = dict( poisoner_name=poisoner_name, poisoner_id=str(poisoner_id), # Convert the JS timestamp to a standard one render_time=render_time * 1000, route_name=route_name, url=url, source=source, cache_policy=cache_policy, resp_headers=resp_headers, ) # For immediate feedback when tracking the effects of caching changes g.stats.simple_event("cache.poisoning.%s.%s" % (source, cache_policy)) # For longer-term diagnosing of caching issues g.events.cache_poisoning_event(poison_info, request=request, context=c) VRatelimit.ratelimit(rate_ip=True, prefix="rate_poison_", seconds=10) return self.api_wrapper({})
class APIv1UserController(OAuth2OnlyController): @require_oauth2_scope("identity") @validate( VUser(), ) @api_doc(api_section.account) def GET_me(self): """Returns the identity of the user currently authenticated via OAuth.""" resp = IdentityJsonTemplate().data(c.oauth_user) return self.api_wrapper(resp) @require_oauth2_scope("identity") @validate( VUser(), fields=VList( "fields", choices=PREFS_JSON_SPEC.spec.keys(), error=errors.errors.NON_PREFERENCE, ), ) @api_doc(api_section.account, uri='/api/v1/me/prefs') def GET_prefs(self, fields): """Return the preference settings of the logged in user""" resp = PrefsJsonTemplate(fields).data(c.oauth_user) return self.api_wrapper(resp) @require_oauth2_scope("read") @validate( user=VAccountByName('username'), ) @api_doc( section=api_section.users, uri='/api/v1/user/{username}/trophies', ) def GET_usertrophies(self, user): """Return a list of trophies for the a given user.""" return self.api_wrapper(get_usertrophies(user)) @require_oauth2_scope("identity") @validate( VUser(), ) @api_doc( section=api_section.account, uri='/api/v1/me/trophies', ) def GET_trophies(self): """Return a list of trophies for the current user.""" return self.api_wrapper(get_usertrophies(c.oauth_user)) @require_oauth2_scope("mysubreddits") @validate( VUser(), ) @api_doc( section=api_section.account, uri='/api/v1/me/karma', ) def GET_karma(self): """Return a breakdown of subreddit karma.""" karmas = c.oauth_user.all_karmas(include_old=False) resp = KarmaListJsonTemplate().render(karmas) return self.api_wrapper(resp.finalize()) PREFS_JSON_VALIDATOR = VValidatedJSON("json", PREFS_JSON_SPEC, body=True) @require_oauth2_scope("account") @validate( VUser(), validated_prefs=PREFS_JSON_VALIDATOR, ) @api_doc(api_section.account, json_model=PREFS_JSON_VALIDATOR, uri='/api/v1/me/prefs') def PATCH_prefs(self, validated_prefs): user_prefs = c.user.preferences() for short_name, new_value in validated_prefs.iteritems(): pref_name = "pref_" + short_name user_prefs[pref_name] = new_value vprefs.filter_prefs(user_prefs, c.user) vprefs.set_prefs(c.user, user_prefs) c.user._commit() return self.api_wrapper(PrefsJsonTemplate().data(c.user)) FRIEND_JSON_SPEC = VValidatedJSON.PartialObject({ "name": VAccountByName("name"), "note": VLength("note", 300), }) FRIEND_JSON_VALIDATOR = VValidatedJSON("json", spec=FRIEND_JSON_SPEC, body=True) @require_oauth2_scope('subscribe') @validate( VUser(), friend=VAccountByName('username'), notes_json=FRIEND_JSON_VALIDATOR, ) @api_doc(api_section.users, json_model=FRIEND_JSON_VALIDATOR, uri='/api/v1/me/friends/{username}') def PUT_friends(self, friend, notes_json): """Create or update a "friend" relationship. This operation is idempotent. It can be used to add a new friend, or update an existing friend (e.g., add/change the note on that friend) """ err = None if 'name' in notes_json and notes_json['name'] != friend: # The 'name' in the JSON is optional, but if present, must # match the username from the URL err = errors.RedditError('BAD_USERNAME', fields='name') if 'note' in notes_json and not c.user.gold: err = errors.RedditError('GOLD_REQUIRED', fields='note') if err: self.on_validation_error(err) # See if the target is already an existing friend. # If not, create the friend relationship. friend_rel = Account.get_friend(c.user, friend) rel_exists = bool(friend_rel) if not friend_rel: friend_rel = c.user.add_friend(friend) response.status = 201 if 'note' in notes_json: note = notes_json['note'] or '' if not rel_exists: # If this is a newly created friend relationship, # the cache needs to be updated before a note can # be applied c.user.friend_rels_cache(_update=True) c.user.add_friend_note(friend, note) rel_view = FriendTableItem(friend_rel) return self.api_wrapper(FriendTableItemJsonTemplate().data(rel_view)) @require_oauth2_scope('mysubreddits') @validate( VUser(), friend_rel=VFriendOfMine('username'), ) @api_doc(api_section.users, uri='/api/v1/me/friends/{username}') def GET_friends(self, friend_rel): """Get information about a specific 'friend', such as notes.""" rel_view = FriendTableItem(friend_rel) return self.api_wrapper(FriendTableItemJsonTemplate().data(rel_view)) @require_oauth2_scope('subscribe') @validate( VUser(), friend_rel=VFriendOfMine('username'), ) @api_doc(api_section.users, uri='/api/v1/me/friends/{username}') def DELETE_friends(self, friend_rel): """Stop being friends with a user.""" c.user.remove_friend(friend_rel._thing2) if c.user.gold: c.user.friend_rels_cache(_update=True) response.status = 204
from r2.lib.validator import ( validate, VAccountByName, VFriendOfMine, VLength, VList, VValidatedJSON, VUser, ) from r2.models import Account, Trophy import r2.lib.errors as errors import r2.lib.validator.preferences as vprefs PREFS_JSON_SPEC = VValidatedJSON.PartialObject({ k[len("pref_"):]: v for k, v in vprefs.PREFS_VALIDATORS.iteritems() if k in Account._preference_attrs }) class APIv1UserController(OAuth2OnlyController): @require_oauth2_scope("identity") @validate( VUser(), ) @api_doc(api_section.account) def GET_me(self): """Returns the identity of the user currently authenticated via OAuth.""" resp = IdentityJsonTemplate().data(c.oauth_user) return self.api_wrapper(resp) @require_oauth2_scope("identity")
VSubredditName('name', allow_language_srs=True), }) MAX_DESC = 10000 MAX_DISP_NAME = 50 WRITABLE_MULTI_FIELDS = ('visibility', 'description_md', 'display_name', 'key_color', 'weighting_scheme') multi_json_spec = VValidatedJSON.PartialObject({ 'description_md': VMarkdownLength('description_md', max_length=MAX_DESC, empty_error=None), 'display_name': VLength('display_name', max_length=MAX_DISP_NAME), 'icon_name': VOneOf('icon_name', g.multi_icons + ("", None)), 'key_color': VColor('key_color'), 'visibility': VOneOf('visibility', ('private', 'public', 'hidden')), 'weighting_scheme': VOneOf('weighting_scheme', ('classic', 'fresh')), 'subreddits': VValidatedJSON.ArrayOf(multi_sr_data_json_spec), }) multi_description_json_spec = VValidatedJSON.Object({ 'body_md': VMarkdownLength('body_md', max_length=MAX_DESC, empty_error=None), }) class MultiApiController(RedditController):
from r2.lib.pages import FriendTableItem from r2.lib.validator import ( validate, VAccountByName, VFriendOfMine, VLength, VList, VValidatedJSON, VUser, ) from r2.models import Account, Trophy import r2.lib.errors as errors import r2.lib.validator.preferences as vprefs PREFS_JSON_SPEC = VValidatedJSON.PartialObject( {k[len("pref_"):]: v for k, v in vprefs.PREFS_VALIDATORS.iteritems()}) class APIv1UserController(OAuth2OnlyController): @require_oauth2_scope("identity") @validate( VUser(), ) @api_doc(api_section.account) def GET_me(self): """Returns the identity of the user currently authenticated via OAuth.""" resp = IdentityJsonTemplate().data(c.oauth_user) return self.api_wrapper(resp) @require_oauth2_scope("identity") @validate(
class WebLogController(RedditController): on_validation_error = staticmethod(abort_with_error) @csrf_exempt @validate( VRatelimit(rate_user=False, rate_ip=True, prefix='rate_weblog_'), level=VOneOf('level', ('error', )), logs=VValidatedJSON( 'logs', VValidatedJSON.ArrayOf( VValidatedJSON.PartialObject({ 'msg': VPrintable('msg', max_length=256), 'url': VPrintable('url', max_length=256), 'tag': VPrintable('tag', max_length=32), }))), ) def POST_message(self, level, logs): # Whitelist tags to keep the frontend from creating too many keys in statsd valid_frontend_log_tags = { 'unknown', 'jquery-migrate-bad-html', } # prevent simple CSRF by requiring a custom header if not request.headers.get('X-Loggit'): abort(403) uid = c.user._id if c.user_is_loggedin else '-' # only accept a maximum of 3 entries per request for log in logs[:3]: if 'msg' not in log or 'url' not in log: continue tag = 'unknown' if log.get('tag') in valid_frontend_log_tags: tag = log['tag'] g.stats.simple_event('frontend.error.' + tag) g.log.warning('[web frontend] %s: %s | U: %s FP: %s UA: %s', level, log['msg'], uid, log['url'], request.user_agent) VRatelimit.ratelimit(rate_user=False, rate_ip=True, prefix="rate_weblog_", seconds=10) @csrf_exempt @validate( # set default to invalid number so we can ignore it later. dns_timing=VFloat('dnsTiming', min=0, num_default=-1), tcp_timing=VFloat('tcpTiming', min=0, num_default=-1), request_timing=VFloat('requestTiming', min=0, num_default=-1), response_timing=VFloat('responseTiming', min=0, num_default=-1), dom_loading_timing=VFloat('domLoadingTiming', min=0, num_default=-1), dom_interactive_timing=VFloat('domInteractiveTiming', min=0, num_default=-1), dom_content_loaded_timing=VFloat('domContentLoadedTiming', min=0, num_default=-1), action_name=VPrintable('actionName', max_length=256), verification=VPrintable('verification', max_length=256), ) def POST_timings(self, action_name, verification, **kwargs): lookup = { 'dns_timing': 'dns', 'tcp_timing': 'tcp', 'request_timing': 'request', 'response_timing': 'response', 'dom_loading_timing': 'dom_loading', 'dom_interactive_timing': 'dom_interactive', 'dom_content_loaded_timing': 'dom_content_loaded', } if not (action_name and verification): abort(422) expected_mac = hmac.new(g.secrets["action_name"], action_name, hashlib.sha1).hexdigest() if not constant_time_compare(verification, expected_mac): abort(422) # action_name comes in the format 'controller.METHOD_action' stat_tpl = 'service_time.web.{}.frontend'.format(action_name) stat_aggregate = 'service_time.web.frontend' for key, name in lookup.iteritems(): val = kwargs[key] if val >= 0: g.stats.simple_timing(stat_tpl + '.' + name, val) g.stats.simple_timing(stat_aggregate + '.' + name, val) abort(204)