def recv_connect(self, version=None, support=None, session=None): """DDP connect handler.""" del session # Meteor doesn't even use this! if self.connection is not None: raise MeteorError( 400, 'Session already established.', self.connection.connection_id, ) elif None in (version, support) or version not in self.versions: self.reply('failed', version=self.versions[0]) elif version not in support: raise MeteorError(400, 'Client version/support mismatch.') else: from dddp.models import Connection cur = connection.cursor() cur.execute('SELECT pg_backend_pid()') (backend_pid,) = cur.fetchone() this.version = version this.support = support self.connection = Connection.objects.create( server_addr='%d:%s' % ( backend_pid, self.ws.handler.socket.getsockname(), ), remote_addr=self.remote_addr, version=version, ) self.pgworker.connections[self.connection.pk] = self atexit.register(self.on_close, 'Shutting down.') self.reply('connected', session=self.connection.connection_id)
def get_username(self, user): """Retrieve username from user selector.""" if isinstance(user, basestring): return user elif isinstance(user, dict) and len(user) == 1: [(key, val)] = user.items() if key == 'username' or (key == self.user_model.USERNAME_FIELD): # username provided directly return val elif key in ('email', 'emails.address'): email_field = getattr(self.user_model, 'EMAIL_FIELD', 'email') if self.user_model.USERNAME_FIELD == email_field: return val # email is username # find username by email return self.user_model.objects.values_list( self.user_model.USERNAME_FIELD, flat=True, ).get(**{email_field: val}) elif key in ('id', 'pk'): # find username by primary key (ID) return self.user_model.objects.values_list( self.user_model.USERNAME_FIELD, flat=True, ).get( pk=val, ) else: raise MeteorError(400, 'Invalid user lookup: %r' % key) else: raise MeteorError(400, 'Invalid user expression: %r' % user)
def validate_kwargs(func, kwargs): """Validate arguments to be supplied to func.""" func_name = func.__name__ argspec = inspect.getargspec(func) all_args = argspec.args[:] defaults = list(argspec.defaults or []) # ignore implicit 'self' argument if inspect.ismethod(func) and all_args[:1] == ['self']: all_args[:1] = [] # don't require arguments that have defaults if defaults: required = all_args[:-len(defaults)] else: required = all_args[:] # translate 'foo_' to avoid reserved names like 'id' trans = { arg: arg.endswith('_') and arg[:-1] or arg for arg in all_args } for key in list(kwargs): key_adj = '%s_' % key if key_adj in all_args: kwargs[key_adj] = kwargs.pop(key) # figure out what we're missing supplied = sorted(kwargs) missing = [ trans.get(arg, arg) for arg in required if arg not in supplied ] if missing: raise MeteorError( 400, func.err, 'Missing required arguments to %s: %s' % ( func_name, ' '.join(missing), ), ) # figure out what is extra extra = [ arg for arg in supplied if arg not in all_args ] if extra: raise MeteorError( 400, func.err, 'Unknown arguments to %s: %s' % (func_name, ' '.join(extra)), )
def method(self, method, params, id_): """Invoke a method.""" try: handler = self.api_path_map()[method] except KeyError: raise MeteorError(404, 'Method not found', method) try: inspect.getcallargs(handler, *params) except TypeError as err: raise MeteorError(400, '%s' % err) result = handler(*params) msg = {'msg': 'result', 'id': id_} if result is not None: msg['result'] = result this.send(msg)
def dispatch(self, msg, kwargs): """Dispatch msg to appropriate recv_foo handler.""" # enforce calling 'connect' first if self.connection is None and msg != 'connect': self.reply('error', reason='Must connect first') return if msg == 'method': if ( 'method' not in kwargs ) or ( 'id' not in kwargs ): self.reply( 'error', error=400, reason='Malformed method invocation', ) return # lookup method handler try: handler = getattr(self, 'recv_%s' % msg) except (AttributeError, UnicodeEncodeError): raise MeteorError(404, 'Method not found') # validate handler arguments validate_kwargs(handler, kwargs) # dispatch to handler handler(**kwargs)
def get_password(password): """Return password in plain-text from string/dict.""" if isinstance(password, basestring): # regular Django authentication - plaintext password... but you're # using HTTPS (SSL) anyway so it's protected anyway, right? return password else: # Meteor is trying to be smart by doing client side hashing of the # password so that passwords are "...not sent in plain text over # the wire". This behaviour doesn't make HTTP any more secure - # it just gives a false sense of security as replay attacks and # code-injection are both still viable attack vectors for the # malicious MITM. Also as no salt is used with hashing, the # passwords are vulnerable to rainbow-table lookups anyway. # # If you're doing security, do it right from the very outset. For # web services that means using SSL and not relying on half-baked # security concepts put together by people with no security # background. # # We protest loudly to anyone who cares to listen in the server # logs until upstream developers see the light and drop the # password hashing mis-feature. raise MeteorError( 426, "Outmoded password hashing: " "https://github.com/meteor/meteor/issues/4363", upgrade='meteor add tysonclugg:accounts-secure', )
def auth_failed(**credentials): """Consistent fail so we don't provide attackers with valuable info.""" if credentials: user_login_failed.send_robust( sender=__name__, credentials=auth._clean_credentials(credentials), ) raise MeteorError(403, 'Authentication failed.')
def check_secure(): """Check request, return False if using SSL or local connection.""" if this.request.is_secure(): return True # using SSL elif this.request.META['REMOTE_ADDR'] in [ 'localhost', '127.0.0.1', ]: return True # localhost raise MeteorError(403, 'Authentication refused without SSL.')
def do_sub(self, id_, name, silent, *params): """Subscribe the current thread to the specified publication.""" try: pub = self.get_pub_by_name(name) except KeyError: if not silent: raise MeteorError(404, 'Subscription not found') return sub, created = Subscription.objects.get_or_create( connection_id=this.ws.connection.pk, sub_id=id_, user_id=getattr(this, 'user_id', None), defaults={ 'publication': pub.name, 'params_ejson': ejson.dumps(params), }, ) this.subs.setdefault(sub.publication, set()).add(sub.pk) if not created: if not silent: this.send({'msg': 'ready', 'subs': [id_]}) return # re-read from DB so we can get transaction ID (xmin) sub = Subscription.objects.extra(**XMIN).get(pk=sub.pk) for col, qs in self.sub_unique_objects( sub, params, pub, xmin__lte=sub.xmin, ): sub.collections.create( model_name=model_name(qs.model), collection_name=col.name, ) if isinstance(col.model._meta.pk, AleaIdField): meteor_ids = None elif len([ field for field in col.model._meta.local_fields if (isinstance(field, AleaIdField)) and ( field.unique) and (not field.null) ]) == 1: meteor_ids = None else: meteor_ids = get_meteor_ids( qs.model, qs.values_list('pk', flat=True), ) for obj in qs.select_related(): payload = col.obj_change_as_msg(obj, ADDED, meteor_ids) this.send(payload) if not silent: this.send({'msg': 'ready', 'subs': [id_]})
def update(self, selector, update, options=None): """Update user data.""" # we're ignoring the `options` argument at this time del options user = get_object( self.model, selector['_id'], pk=this.user_id, ) profile_update = self.deserialize_profile( update['$set'], key_prefix='profile.', pop=True, ) if len(update['$set']) != 0: raise MeteorError(400, 'Invalid update fields: %r') for key, val in profile_update.items(): setattr(user, key, val) user.save()
def deserialize_profile(profile, key_prefix='', pop=False): """De-serialize user profile fields into concrete model fields.""" result = {} if pop: getter = profile.pop else: getter = profile.get def prefixed(name): """Return name prefixed by `key_prefix`.""" return '%s%s' % (key_prefix, name) for key in profile.keys(): val = getter(key) if key == prefixed('name'): result['full_name'] = val else: raise MeteorError(400, 'Bad profile key: %r' % key) return result