From 2d92bbb79f663b476bfdb525578428e3520d8d4f Mon Sep 17 00:00:00 2001 From: Denis Washington Date: Mon, 9 Jul 2012 19:04:01 +0200 Subject: [PATCH 1/6] Implement temporary subscriptions Clients can now send a Pub-Sub IQ and set 'pubsub#expire'='presence' as described in http://xmpp.org/extensions/xep-0060.html#impl-tempsub --- postgres.sql | 1 + src/local/model_postgres.coffee | 9 +++++++++ src/local/operations.coffee | 10 ++++++++++ src/router.coffee | 5 +++++ src/xmpp/pubsub_server.coffee | 31 +++++++++++++++++++++++++++++++ 5 files changed, 56 insertions(+) diff --git a/postgres.sql b/postgres.sql index 9495950..e2d01a0 100644 --- a/postgres.sql +++ b/postgres.sql @@ -15,6 +15,7 @@ CREATE TABLE subscriptions (node TEXT REFERENCES nodes (node), listener TEXT, subscription TEXT, updated TIMESTAMP, + temporary BOOLEAN DEFAULT FALSE, PRIMARY KEY (node, "user")); CREATE INDEX subscriptions_updated ON subscriptions (updated); CREATE TABLE affiliations (node TEXT REFERENCES nodes (node), diff --git a/src/local/model_postgres.coffee b/src/local/model_postgres.coffee index 8cd67e9..2849612 100644 --- a/src/local/model_postgres.coffee +++ b/src/local/model_postgres.coffee @@ -305,6 +305,10 @@ class Transaction ], (err) -> cb err + setSubscriptionTemporary: (node, user, temporary, cb) -> + logger.debug "setSubscriptionTemporary #{node} #{user} #{temporary}" + @db.query "UPDATE subscriptions SET temporary=$1 WHERE node=$2 AND \"user\"=$3", [ temporary, node, user ], cb + getSubscribers: (node, cb) -> unless node return cb(new Error("No node")) @@ -382,6 +386,11 @@ class Transaction @db.query "DELETE FROM subscriptions WHERE \"user\"=$1", [user], (err) -> cb err + clearTemporarySubscriptions: (user, cb) -> + logger.debug "clearTemporarySubscriptions #{user}" + @db.query "DELETE FROM subscriptions WHERE \"user\"=$1 AND temporary=TRUE", [user], (err) -> + cb err + ## # Affiliation management ## diff --git a/src/local/operations.coffee b/src/local/operations.coffee index 2d1913c..a4a2650 100644 --- a/src/local/operations.coffee +++ b/src/local/operations.coffee @@ -620,6 +620,10 @@ class Unsubscribe extends PrivilegedOperation affiliation: @actorAffiliation }] +class ModifySubscriptionOptions extends PrivilegedOperation + privilegedTransaction: (t, cb) -> + if @req.temporary? + t.setSubscriptionTemporary @req.node, @req.actor, @req.temporary, cb class RetrieveItems extends PrivilegedOperation run: (cb) -> @@ -1006,6 +1010,10 @@ class RemoveUser extends ModelOperation t.clearUserSubscriptions @req.actor, (err) => cb err, subscriptions +class RemoveTemporarySubscriptions extends ModelOperation + transaction: (t, cb) -> + t.clearTemporarySubscriptions @req.actor, (err) => + cb err class AuthorizeSubscriber extends PrivilegedOperation requiredAffiliation: => @@ -1348,6 +1356,7 @@ OPERATIONS = 'publish-node-items': Publish 'subscribe-node': Subscribe 'unsubscribe-node': Unsubscribe + 'modify-subscription-options': ModifySubscriptionOptions 'retrieve-node-items': RetrieveItems 'retract-node-items': RetractItems 'retrieve-user-subscriptions': RetrieveUserSubscriptions @@ -1359,6 +1368,7 @@ OPERATIONS = 'manage-node-affiliations': ManageNodeAffiliations 'manage-node-configuration': ManageNodeConfiguration 'remove-user': RemoveUser + 'remove-temporary-subscriptions': RemoveTemporarySubscriptions 'confirm-subscriber-authorization': AuthorizeSubscriber 'replay-archive': ReplayArchive 'push-inbox': PushInbox diff --git a/src/router.coffee b/src/router.coffee index 833610d..2dfc26e 100644 --- a/src/router.coffee +++ b/src/router.coffee @@ -292,3 +292,8 @@ class exports.Router actor: user sender: user runLocally req, -> + else + req = + operation: 'remove-temporary-subscriptions' + actor: user + @runLocally req, -> diff --git a/src/xmpp/pubsub_server.coffee b/src/xmpp/pubsub_server.coffee index 5cb1ece..c3fdedb 100644 --- a/src/xmpp/pubsub_server.coffee +++ b/src/xmpp/pubsub_server.coffee @@ -360,6 +360,36 @@ class PubsubUnsubscribeRequest extends PubsubRequest writes: true +# +# +# +# +# ... +class PubsubOptionsRequest extends PubsubRequest + constructor: (stanza) -> + super + + @optionsEl = @pubsubEl?.getChild("options") + @node = @optionsEl?.attrs.node + @formEl = @optionsEl?.getChild("x") + if @formEl + for field in @formEl.getChildren("field") + if field.attrs.var == 'pubsub#expire' + @temporary = field.getChild("value")?.getText() is 'presence' + + matches: () -> + super && + @iq.attrs.type is 'set' && + @node && + @formEl + + operation: 'modify-subscription-options' + + writes: true + # Date: Tue, 10 Jul 2012 19:53:48 +0200 Subject: [PATCH 2/6] Don't return temporarily subscribed users on / --- src/local/model_postgres.coffee | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/local/model_postgres.coffee b/src/local/model_postgres.coffee index 2849612..67d2cf5 100644 --- a/src/local/model_postgres.coffee +++ b/src/local/model_postgres.coffee @@ -315,7 +315,7 @@ class Transaction db = @db async.waterfall [(cb2) -> - db.query "SELECT \"user\", subscription FROM subscriptions WHERE node=$1 ORDER BY updated DESC", [ node ], cb2 + db.query "SELECT \"user\", subscription FROM subscriptions WHERE node=$1 AND temporary=FALSE ORDER BY updated DESC", [ node ], cb2 , (res, cb2) -> subscribers = for row in res.rows { user: row.user, subscription: row.subscription } @@ -327,7 +327,7 @@ class Transaction # Not only by users but also by listeners. # @param cb {Function} cb(Error, { user, node, subscription }) getSubscriptions: (actor, cb) -> - @db.query "SELECT \"user\", node, subscription FROM subscriptions WHERE \"user\"=$1 OR listener=$1 ORDER BY updated DESC", [ actor ], (err, res) -> + @db.query "SELECT \"user\", node, subscription FROM subscriptions WHERE temporary=FALSE AND (\"user\"=$1 OR listener=$1) ORDER BY updated DESC", [ actor ], (err, res) -> cb err, res?.rows getPending: (node, cb) -> @@ -457,7 +457,7 @@ class Transaction getAffiliated: (node, cb) -> db = @db async.waterfall [(cb2) -> - db.query "SELECT \"user\", affiliation FROM affiliations WHERE node=$1 ORDER BY updated DESC", [ node ], cb2 + db.query "SELECT affiliations.user, affiliation FROM (affiliations JOIN subscriptions ON affiliations.user = subscriptions.user AND affiliations.node = subscriptions.node AND temporary=FALSE) WHERE affiliations.node=$1 ORDER BY affiliations.updated DESC", [ node ], cb2 , (res, cb2) -> affiliations = for row in res.rows { user: row.user, affiliation: row.affiliation } From db5d86760c7b90a4d0bca6bfef283a0c7d15bb7a Mon Sep 17 00:00:00 2001 From: Denis Washington Date: Tue, 10 Jul 2012 20:07:53 +0200 Subject: [PATCH 3/6] Let override temporary subscriptions with persistent ones --- src/local/model_postgres.coffee | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/local/model_postgres.coffee b/src/local/model_postgres.coffee index 67d2cf5..3cee258 100644 --- a/src/local/model_postgres.coffee +++ b/src/local/model_postgres.coffee @@ -277,11 +277,11 @@ class Transaction logger.debug "setSubscription #{node} #{user} isSet=#{isSet} toDelete=#{toDelete}" if isSet and not toDelete if listener - db.query "UPDATE subscriptions SET listener=$1, subscription=$2, updated=CURRENT_TIMESTAMP WHERE node=$3 AND \"user\"=$4" + db.query "UPDATE subscriptions SET listener=$1, subscription=$2, updated=CURRENT_TIMESTAMP, temporary=FALSE WHERE node=$3 AND \"user\"=$4" , [ listener, subscription, node, user ] , cb2 else - db.query "UPDATE subscriptions SET subscription=$1, updated=CURRENT_TIMESTAMP WHERE node=$2 AND \"user\"=$3" + db.query "UPDATE subscriptions SET subscription=$1, updated=CURRENT_TIMESTAMP, temporary=FALSE WHERE node=$2 AND \"user\"=$3" , [ subscription, node, user ] , cb2 else if not isSet and not toDelete @@ -757,4 +757,3 @@ parseEl = (xml) -> catch e logger.error "Parsing " + xml + ": " + e.stack return undefined - From 9fd1473fc2f03fe193ed86f6f8e38a8efd860c46 Mon Sep 17 00:00:00 2001 From: Denis Washington Date: Wed, 11 Jul 2012 18:48:31 +0200 Subject: [PATCH 4/6] Don't allow clients to make existing subscriptions temporary This means the only way to make a temporary subscription is to put a query next to a one in the same IQ. --- src/local/model_postgres.coffee | 22 ++++++++----------- src/local/operations.coffee | 20 ++++++----------- src/xmpp/pubsub_server.coffee | 39 +++++++-------------------------- 3 files changed, 24 insertions(+), 57 deletions(-) diff --git a/src/local/model_postgres.coffee b/src/local/model_postgres.coffee index 3cee258..e36c96e 100644 --- a/src/local/model_postgres.coffee +++ b/src/local/model_postgres.coffee @@ -261,7 +261,7 @@ class Transaction , (err, res) -> cb err, res?.rows?[0]?.listener or "none" - setSubscription: (node, user, listener, subscription, cb) -> + setSubscription: (node, user, listener, subscription, temporary, cb) -> unless node return cb(new Error("No node")) unless user @@ -277,22 +277,22 @@ class Transaction logger.debug "setSubscription #{node} #{user} isSet=#{isSet} toDelete=#{toDelete}" if isSet and not toDelete if listener - db.query "UPDATE subscriptions SET listener=$1, subscription=$2, updated=CURRENT_TIMESTAMP, temporary=FALSE WHERE node=$3 AND \"user\"=$4" - , [ listener, subscription, node, user ] + db.query "UPDATE subscriptions SET listener=$1, subscription=$2, updated=CURRENT_TIMESTAMP, temporary=$3 WHERE node=$4 AND \"user\"=$5" + , [ listener, subscription, temporary, node, user ] , cb2 else - db.query "UPDATE subscriptions SET subscription=$1, updated=CURRENT_TIMESTAMP, temporary=FALSE WHERE node=$2 AND \"user\"=$3" - , [ subscription, node, user ] + db.query "UPDATE subscriptions SET subscription=$1, updated=CURRENT_TIMESTAMP, temporary=$2 WHERE node=$3 AND \"user\"=$4" + , [ subscription, temporary, node, user ] , cb2 else if not isSet and not toDelete # listener=null is allowed for 3rd-party inboxes if listener - db.query "INSERT INTO subscriptions (node, \"user\", listener, subscription, updated) VALUES ($1, $2, $3, $4, CURRENT_TIMESTAMP)" - , [ node, user, listener, subscription ] + db.query "INSERT INTO subscriptions (node, \"user\", listener, subscription, updated, temporary) VALUES ($1, $2, $3, $4, CURRENT_TIMESTAMP, $5)" + , [ node, user, listener, subscription, temporary ] , cb2 else - db.query "INSERT INTO subscriptions (node, \"user\", subscription, updated) VALUES ($1, $2, $3, CURRENT_TIMESTAMP)" - , [ node, user, subscription ] + db.query "INSERT INTO subscriptions (node, \"user\", subscription, updated, temporary) VALUES ($1, $2, $3, CURRENT_TIMESTAMP, $4)" + , [ node, user, subscription, temporary ] , cb2 else if isSet and toDelete db.query "DELETE FROM subscriptions WHERE node=$1 AND \"user\"=$2" @@ -305,10 +305,6 @@ class Transaction ], (err) -> cb err - setSubscriptionTemporary: (node, user, temporary, cb) -> - logger.debug "setSubscriptionTemporary #{node} #{user} #{temporary}" - @db.query "UPDATE subscriptions SET temporary=$1 WHERE node=$2 AND \"user\"=$3", [ temporary, node, user ], cb - getSubscribers: (node, cb) -> unless node return cb(new Error("No node")) diff --git a/src/local/operations.coffee b/src/local/operations.coffee index a4a2650..3e0dc48 100644 --- a/src/local/operations.coffee +++ b/src/local/operations.coffee @@ -411,7 +411,7 @@ class Register extends ModelOperation created = created_ t.setAffiliation node, user, 'owner', cb2 , (cb2) => - t.setSubscription node, user, @req.sender, 'subscribed', cb2 + t.setSubscription node, user, @req.sender, 'subscribed', false, cb2 , (cb2) -> # if already present, don't overwrite config if created @@ -479,7 +479,7 @@ class CreateNode extends ModelOperation # Set t.setConfig @req.node, config, cb2 , (cb2) => - t.setSubscription @req.node, @req.actor, @req.sender, 'subscribed', cb2 + t.setSubscription @req.node, @req.actor, @req.sender, 'subscribed', false, cb2 , (cb2) => t.setAffiliation @req.node, @req.actor, 'owner', cb2 ], cb @@ -552,7 +552,7 @@ class Subscribe extends PrivilegedOperation privilegedTransaction: (t, cb) -> async.waterfall [ (cb2) => - t.setSubscription @req.node, @req.actor, @req.sender, @subscription, cb2 + t.setSubscription @req.node, @req.actor, @req.sender, @subscription, @req.temporary, cb2 , (cb2) => if @affiliation t.setAffiliation @req.node, @req.actor, @affiliation, cb2 @@ -592,7 +592,7 @@ class Unsubscribe extends PrivilegedOperation return cb new errors.Forbidden("You may not unsubscribe from your own nodes") async.waterfall [ (cb2) => - t.setSubscription @req.node, @req.actor, @req.sender, 'none', cb2 + t.setSubscription @req.node, @req.actor, @req.sender, 'none', false, cb2 , (cb2) => @fetchActorAffiliation t, cb2 , (cb2) => @@ -620,11 +620,6 @@ class Unsubscribe extends PrivilegedOperation affiliation: @actorAffiliation }] -class ModifySubscriptionOptions extends PrivilegedOperation - privilegedTransaction: (t, cb) -> - if @req.temporary? - t.setSubscriptionTemporary @req.node, @req.actor, @req.temporary, cb - class RetrieveItems extends PrivilegedOperation run: (cb) -> if @subscriptionsNodeOwner? @@ -887,7 +882,7 @@ class ManageNodeSubscriptions extends PrivilegedOperation subscription isnt 'subscribed' cb4 new errors.Forbidden("You may not unsubscribe the owner") else - t.setSubscription @req.node, user, null, subscription, cb4 + t.setSubscription @req.node, user, null, subscription, false, cb4 , (cb4) => t.getAffiliation @req.node, user, cb4 , (affiliation, cb4) => @@ -1032,7 +1027,7 @@ class AuthorizeSubscriber extends PrivilegedOperation @subscription = 'none' async.waterfall [ (cb2) => - t.setSubscription @req.node, @req.user, @req.sender, @subscription, cb2 + t.setSubscription @req.node, @req.user, @req.sender, @subscription, false, cb2 , (cb2) => if @affiliation t.setAffiliation @req.node, @req.user, @affiliation, cb2 @@ -1138,7 +1133,7 @@ class PushInbox extends ModelOperation {node, user, listener, subscription} = update if subscription isnt 'subscribed' unsubscribedNodes[node] = yes - t.setSubscription node, user, listener, subscription, cb3 + t.setSubscription node, user, listener, subscription, false, cb3 when 'affiliation' notification.push update @@ -1356,7 +1351,6 @@ OPERATIONS = 'publish-node-items': Publish 'subscribe-node': Subscribe 'unsubscribe-node': Unsubscribe - 'modify-subscription-options': ModifySubscriptionOptions 'retrieve-node-items': RetrieveItems 'retract-node-items': RetractItems 'retrieve-user-subscriptions': RetrieveUserSubscriptions diff --git a/src/xmpp/pubsub_server.coffee b/src/xmpp/pubsub_server.coffee index c3fdedb..6977cda 100644 --- a/src/xmpp/pubsub_server.coffee +++ b/src/xmpp/pubsub_server.coffee @@ -318,6 +318,14 @@ class PubsubSubscribeRequest extends PubsubRequest @subscribeEl = @pubsubEl?.getChild("subscribe") @node = @subscribeEl?.attrs.node + @temporary = false + + @optionsEl = @pubsubEl?.getChild("options") + @formEl = @optionsEl?.getChild("x") + if @formEl + for field in @formEl.getChildren("field") + if field.attrs.var == 'pubsub#expire' + @temporary = field.getChild("value")?.getText() is 'presence' matches: () -> super && @@ -360,36 +368,6 @@ class PubsubUnsubscribeRequest extends PubsubRequest writes: true -# -# -# -# -# ... -class PubsubOptionsRequest extends PubsubRequest - constructor: (stanza) -> - super - - @optionsEl = @pubsubEl?.getChild("options") - @node = @optionsEl?.attrs.node - @formEl = @optionsEl?.getChild("x") - if @formEl - for field in @formEl.getChildren("field") - if field.attrs.var == 'pubsub#expire' - @temporary = field.getChild("value")?.getText() is 'presence' - - matches: () -> - super && - @iq.attrs.type is 'set' && - @node && - @formEl - - operation: 'modify-subscription-options' - - writes: true - # Date: Sun, 15 Jul 2012 11:19:36 +0200 Subject: [PATCH 5/6] Include temporariness in subscription notifications to remote servers --- src/local/operations.coffee | 5 +++-- src/xmpp/backend_pubsub.coffee | 1 + src/xmpp/pubsub_notifications.coffee | 1 + 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/local/operations.coffee b/src/local/operations.coffee index 3e0dc48..f506e60 100644 --- a/src/local/operations.coffee +++ b/src/local/operations.coffee @@ -569,6 +569,7 @@ class Subscribe extends PrivilegedOperation node: @req.node user: @req.actor subscription: @subscription + temporary: @req.temporary }] if @affiliation ns.push @@ -1130,10 +1131,10 @@ class PushInbox extends ModelOperation when 'subscription' notification.push update - {node, user, listener, subscription} = update + {node, user, listener, subscription, temporary} = update if subscription isnt 'subscribed' unsubscribedNodes[node] = yes - t.setSubscription node, user, listener, subscription, false, cb3 + t.setSubscription node, user, listener, subscription, temporary, cb3 when 'affiliation' notification.push update diff --git a/src/xmpp/backend_pubsub.coffee b/src/xmpp/backend_pubsub.coffee index b089fc3..5db51a4 100644 --- a/src/xmpp/backend_pubsub.coffee +++ b/src/xmpp/backend_pubsub.coffee @@ -145,6 +145,7 @@ class exports.PubsubBackend extends EventEmitter node: node user: child.attrs.jid subscription: child.attrs.subscription + temporary: child.attrs.temporary? and child.attrs.temporary == '1' if child.is('affiliation') updates.push diff --git a/src/xmpp/pubsub_notifications.coffee b/src/xmpp/pubsub_notifications.coffee index 1692d29..18b969e 100644 --- a/src/xmpp/pubsub_notifications.coffee +++ b/src/xmpp/pubsub_notifications.coffee @@ -49,6 +49,7 @@ class EventNotification extends Notification jid: update.user node: update.node subscription: update.subscription + temporary: if update.temporary then '1' else '0' ) when 'affiliation' eventEl. From 255dc7605f7fb0e82ad97ec47eb41b958f9063cd Mon Sep 17 00:00:00 2001 From: Denis Washington Date: Sun, 15 Jul 2012 15:53:48 +0200 Subject: [PATCH 6/6] Don't allow temporary subscriptions to private nodes --- src/local/operations.coffee | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/local/operations.coffee b/src/local/operations.coffee index f506e60..886b129 100644 --- a/src/local/operations.coffee +++ b/src/local/operations.coffee @@ -533,9 +533,12 @@ class Subscribe extends PrivilegedOperation @fetchNodeConfig t, cb2 , (cb2) => if @nodeConfig.accessModel is 'authorize' - @subscription = 'pending' - # Immediately return: - return cb2() + if @req.temporary + return cb new errors.NotAllowed('Cannot subscribe temporarily to private node') + else + @subscription = 'pending' + # Immediately return: + return cb2() @subscription = 'subscribed' defaultAffiliation = @nodeConfig.defaultAffiliation or 'none'