org.springframework.social.facebook.web.RealTimeUpdateController Maven / Gradle / Ivy
/*
* Copyright 2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.social.facebook.web;
import static org.springframework.web.bind.annotation.RequestMethod.*;
import java.util.List;
import java.util.Map;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.inject.Inject;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.crypto.codec.Hex;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* A Spring MVC controller that handles callbacks from Facebook's Real-Time Update API.
* Handles both the initial subscription verification request as well as individual updates:
* The following requests are handled:
*
* - GET /realtime/facebook/{subscription} - Verifies subscription.
* - POST /realtime/facebook/{subscription} - Receives an update.
*
*
* Note that these requests are performed by Facebook and are not typically linked to or otherwise called in a web application.
* @author Craig Walls
*/
@Controller
@RequestMapping("/realtime/facebook")
public class RealTimeUpdateController {
private Map tokens;
private List updateHandlers;
private String applicationSecret;
/**
* Constructs a RealTimeUpdateController.
* @param tokens A map of subscription names to verification tokens.
* @param updateHandlers A list of {@link UpdateHandler} implementations to handle incoming updates.
* @param applicationSecret the application's Facebook App Secret
*/
@Inject
public RealTimeUpdateController(Map tokens, List updateHandlers, String applicationSecret) {
this.tokens = tokens;
this.updateHandlers = updateHandlers;
this.applicationSecret = applicationSecret;
}
/**
* Handles subscription verification callback from Facebook.
* @param subscription The subscription name.
* @param challenge A challenge that Facebook expects to be returned.
* @param verifyToken A verification token that must match with the subscription's token given when the controller was created.
* @return The challenge if the verification token matches; blank string otherwise.
*/
@RequestMapping(value="/{subscription}", method=GET, params="hub.mode=subscribe")
public @ResponseBody String verifySubscription(
@PathVariable("subscription") String subscription,
@RequestParam("hub.challenge") String challenge,
@RequestParam("hub.verify_token") String verifyToken) {
logger.debug("Received subscription verification request for '" + subscription + "'.");
return tokens.containsKey(subscription) && tokens.get(subscription).equals(verifyToken) ? challenge : "";
}
/**
* Receives an update from Facebook's real-time API.
* @param subscription The subscription name.
* @param payload The request body payload.
* @param signature The SHA1 signature of the request.
* @return a String with the response back to Facebook
* @throws Exception an Exception if anything goes wrong while processing the update
*/
@RequestMapping(value="/{subscription}", method=POST)
public @ResponseBody String receiveUpdate(
@PathVariable("subscription") String subscription,
@RequestBody String payload,
@RequestHeader(X_HUB_SIGNATURE) String signature) throws Exception {
// Can only read body once and we need it as a raw String to calculate the signature.
// Therefore, use Jackson ObjectMapper to give us a RealTimeUpdate object from that raw String.
RealTimeUpdate update = new ObjectMapper().readValue(payload, RealTimeUpdate.class);
if (verifySignature(payload, signature)) {
logger.debug("Received " + update.getObject() + " update for '" + subscription + "'.");
for (UpdateHandler handler : updateHandlers) {
handler.handleUpdate(subscription, update);
}
} else {
logger.warn("Received an update, but signature was invalid. Not delegating to handlers.");
}
return "";
}
private boolean verifySignature(String payload, String signature) throws Exception {
if (!signature.startsWith("sha1=")) {
return false;
}
String expected = signature.substring(5);
Mac mac = Mac.getInstance(HMAC_SHA1_ALGORITHM);
SecretKeySpec signingKey = new SecretKeySpec(applicationSecret.getBytes(), HMAC_SHA1_ALGORITHM);
mac.init(signingKey);
byte[] rawHmac = mac.doFinal(payload.getBytes());
String actual = new String(Hex.encode(rawHmac));
return expected.equals(actual);
}
private static final String HMAC_SHA1_ALGORITHM = "HmacSHA1";
private static final String X_HUB_SIGNATURE = "X-Hub-Signature";
private final static Log logger = LogFactory.getLog(RealTimeUpdateController.class);
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy