Java tutorial
/* * Copyright 2012 Janrain, Inc. * * 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 * * http://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 com.janrain.backplane.server; import com.janrain.backplane.server.config.AuthException; import com.janrain.backplane.server.config.Backplane1Config; import com.janrain.backplane.server.config.BpServerConfig; import com.janrain.backplane.server.dao.DaoFactory; import com.janrain.backplane.server.dao.redis.RedisBackplaneMessageDAO; import com.janrain.backplane2.server.config.User; import com.janrain.backplane.DateTimeUtils; import com.janrain.cache.CachedL1; import com.janrain.commons.supersimpledb.SimpleDBException; import com.janrain.crypto.HmacHashUtils; import com.janrain.servlet.ServletUtil; import com.janrain.utils.BackplaneSystemProps; import com.janrain.utils.AnalyticsLogger; import com.yammer.metrics.Metrics; import com.yammer.metrics.core.Histogram; import com.yammer.metrics.core.MetricName; import com.yammer.metrics.core.TimerContext; import org.apache.commons.codec.binary.Base64; import org.apache.log4j.Logger; import org.codehaus.jackson.map.ObjectMapper; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.ModelAndView; import javax.inject.Inject; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.security.SecureRandom; import java.util.*; import java.util.concurrent.TimeUnit; /** * Backplane API implementation. * * @author Johnny Bufu */ @Controller @RequestMapping(value = "/*") @SuppressWarnings({ "UnusedDeclaration" }) public class Backplane1Controller { // - PUBLIC @RequestMapping(value = "/", method = { RequestMethod.GET, RequestMethod.HEAD }) public ModelAndView greetings(HttpServletRequest request, HttpServletResponse response) { if (RequestMethod.HEAD.toString().equals(request.getMethod())) { response.setContentLength(0); } return new ModelAndView("welcome"); } @RequestMapping(value = "/admin", method = { RequestMethod.GET, RequestMethod.HEAD }) public ModelAndView admin(HttpServletRequest request, HttpServletResponse response) { ServletUtil.checkSecure(request); boolean adminUserExists = true; // check to see if an admin record already exists, if it does, do not allow an update User admin = DaoFactory.getAdminDAO().get(BackplaneSystemProps.ADMIN_USER); if (admin == null) { adminUserExists = false; } if (RequestMethod.HEAD.toString().equals(request.getMethod())) { response.setContentLength(0); } BpServerConfig bpServerConfig = DaoFactory.getConfigDAO().get(BackplaneSystemProps.BPSERVER_CONFIG_KEY); if (bpServerConfig == null) { bpServerConfig = new BpServerConfig(); } // add it to the L1 cache CachedL1.getInstance().setObject(BackplaneSystemProps.BPSERVER_CONFIG_KEY, -1, bpServerConfig); ModelAndView view = new ModelAndView("admin"); view.addObject("adminUserExists", adminUserExists); view.addObject("configKey", bpServerConfig.getIdValue()); view.addObject("debugMode", bpConfig.isDebugMode()); view.addObject("defaultMessagesMax", bpServerConfig.get(BpServerConfig.Field.DEFAULT_MESSAGES_MAX)); return view; } @RequestMapping(value = "/adminupdate", method = { RequestMethod.POST }) public ModelAndView updateConfiguration(HttpServletRequest request, HttpServletResponse response) { ServletUtil.checkSecure(request); BpServerConfig bpServerConfig = DaoFactory.getConfigDAO().get(BackplaneSystemProps.BPSERVER_CONFIG_KEY); if (bpServerConfig == null) { bpServerConfig = new BpServerConfig(); } ModelAndView view = new ModelAndView("adminadd"); String debugModeString = request.getParameter("debug_mode"); String defaultMessagesMax = request.getParameter("default_messages_max"); bpServerConfig.put(BpServerConfig.Field.DEBUG_MODE.getFieldName(), Boolean.valueOf(debugModeString).toString()); bpServerConfig.put(BpServerConfig.Field.DEFAULT_MESSAGES_MAX.getFieldName(), defaultMessagesMax); try { bpServerConfig.validate(); DaoFactory.getConfigDAO().persist(bpServerConfig); // add it to the L1 cache CachedL1.getInstance().setObject(BackplaneSystemProps.BPSERVER_CONFIG_KEY, -1, bpServerConfig); logger.info(bpServerConfig.toString()); } catch (Exception e) { logger.error(e); view.addObject("message", "An error has occurred " + e.getMessage()); return view; } view.addObject("message", "Configuration updated"); return view; } @RequestMapping(value = "/adminadd", method = { RequestMethod.POST }) public ModelAndView addAdmin(HttpServletRequest request, HttpServletResponse response) { try { ServletUtil.checkSecure(request); ModelAndView view = new ModelAndView("adminadd"); // be sure no record exists User admin = DaoFactory.getAdminDAO().get(BackplaneSystemProps.ADMIN_USER); if (admin == null) { String name = request.getParameter("username"); if (!name.equals(BackplaneSystemProps.ADMIN_USER)) { view.addObject("message", "Admin user name must be " + BackplaneSystemProps.ADMIN_USER); return view; } String password = request.getParameter("password"); // hash password password = HmacHashUtils.hmacHash(password); User user = new User(); user.setUserNamePassword(name, password); DaoFactory.getAdminDAO().persist(user); view.addObject("message", "Admin user " + name + " updated"); } else { view.addObject("message", "Admin user already exists. You must delete the entry from the database before submitting a new admin user."); } if (RequestMethod.HEAD.toString().equals(request.getMethod())) { response.setContentLength(0); } return view; } catch (Exception e) { logger.error(e); throw new RuntimeException(e); } } @RequestMapping(value = "/{version}/bus/{bus}", method = RequestMethod.GET) public @ResponseBody List<HashMap<String, Object>> getBusMessages(@PathVariable String version, @RequestHeader(value = "Authorization", required = false) String basicAuth, @PathVariable String bus, @RequestParam(value = "since", defaultValue = "") String since, @RequestParam(value = "sticky", required = false) String sticky) throws AuthException, SimpleDBException, BackplaneServerException { final TimerContext context = getBusMessagesTime.time(); try { checkAuth(basicAuth, bus, BusConfig1.BUS_PERMISSION.GETALL); List<BackplaneMessage> messages = DaoFactory.getBackplaneMessageDAO().getMessagesByBus(bus, since, sticky); List<HashMap<String, Object>> frames = new ArrayList<HashMap<String, Object>>(); for (BackplaneMessage message : messages) { frames.add(message.asFrame(version)); } return frames; } finally { context.stop(); } } @RequestMapping(value = "/{version}/bus/{bus}/channel/{channel}", method = RequestMethod.GET) public ResponseEntity<String> getChannel(HttpServletRequest request, HttpServletResponse response, @PathVariable String version, @PathVariable String bus, @PathVariable String channel, @RequestHeader(value = "Referer", required = false) String referer, @RequestParam(required = false) String callback, @RequestParam(value = "since", required = false) String since, @RequestParam(value = "sticky", required = false) String sticky) throws SimpleDBException, AuthException, BackplaneServerException { logger.debug("request started"); try { boolean newChannel = NEW_CHANNEL_LAST_PATH.equals(channel); String resp; List<BackplaneMessage> messages = new ArrayList<BackplaneMessage>(); if (newChannel) { resp = newChannel(); aniLogNewChannel(request, referer, version, bus, resp.substring(1, resp.length() - 1)); } else { messages = getChannelMessages(bus, channel, since, sticky); resp = messagesToFrames(messages, version); aniLogPollMessages(request, referer, version, bus, channel, messages); } return new ResponseEntity<String>(resp, new HttpHeaders() { { add("Content-Type", "application/json"); } }, HttpStatus.OK); } finally { logger.debug("request ended"); } } @RequestMapping(value = "/{version}/bus/{bus}/channel/{channel}", method = RequestMethod.POST) public @ResponseBody String postToChannel(HttpServletRequest request, HttpServletResponse response, @PathVariable String version, @RequestHeader(value = "Authorization", required = false) String basicAuth, @RequestBody List<Map<String, Object>> messages, @PathVariable String bus, @PathVariable String channel) throws AuthException, SimpleDBException, BackplaneServerException { User user = checkAuth(basicAuth, bus, BusConfig1.BUS_PERMISSION.POST); final TimerContext context = postMessagesTime.time(); try { RedisBackplaneMessageDAO backplaneMessageDAO = DaoFactory.getBackplaneMessageDAO(); //Block post if the caller has exceeded the message post limit if (backplaneMessageDAO.getMessageCount(bus, channel) >= bpConfig.getDefaultMaxMessageLimit()) { logger.warn("Channel " + bus + ":" + channel + " has reached the maximum of " + bpConfig.getDefaultMaxMessageLimit() + " messages"); throw new BackplaneServerException("Message limit exceeded for this channel"); } BusConfig1 busConfig = DaoFactory.getBusDAO().get(bus); // For analytics. String channelId = "https://" + request.getServerName() + "/" + version + "/bus/" + bus + "/channel/" + channel; String clientId = user.getIdValue(); for (Map<String, Object> messageData : messages) { BackplaneMessage message = new BackplaneMessage(bus, channel, busConfig.getRetentionTimeSeconds(), busConfig.getRetentionTimeStickySeconds(), messageData); backplaneMessageDAO.persist(message); aniLogNewMessage(version, bus, channelId, clientId); } return ""; } finally { context.stop(); } } /** * Handle auth errors */ @ExceptionHandler @ResponseBody public Map<String, String> handle(final AuthException e, HttpServletResponse response) { logger.error("Backplane authentication error: " + e.getMessage(), bpConfig.getDebugException(e)); response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); return new HashMap<String, String>() { { put(ERR_MSG_FIELD, e.getMessage()); } }; } @ExceptionHandler @ResponseBody public Map<String, String> handle(final BackplaneServerException bse, HttpServletResponse response) { logger.error("Backplane server error: " + bse.getMessage(), bpConfig.getDebugException(bse)); response.setStatus(HttpServletResponse.SC_SERVICE_UNAVAILABLE); return new HashMap<String, String>() { { put(ERR_MSG_FIELD, bpConfig.isDebugMode() ? bse.getMessage() : "Service unavailable"); } }; } /** * Handle all other errors */ @ExceptionHandler @ResponseBody public Map<String, String> handle(final Exception e, HttpServletRequest request, HttpServletResponse response) { String path = request.getPathInfo(); logger.error("Error handling backplane request for " + path + ": " + e.getMessage(), bpConfig.getDebugException(e)); response.setStatus(HttpServletResponse.SC_BAD_REQUEST); return new HashMap<String, String>() { { put(ERR_MSG_FIELD, bpConfig.isDebugMode() ? e.getMessage() : "Error processing request."); } }; } public static String randomString(int length) { byte[] randomBytes = new byte[length]; random.nextBytes(randomBytes); for (int i = 0; i < length; i++) { byte b = randomBytes[i]; int c = Math.abs(b % 16); if (c < 10) c += 48; // map (0..9) to '0' .. '9' else c += (97 - 10); // map (10..15) to 'a'..'f' randomBytes[i] = (byte) c; } try { return new String(randomBytes, "US-ASCII"); } catch (UnsupportedEncodingException e) { logger.error("US-ASCII character encoding not supported", e); // shouldn't happen return null; } } // - PRIVATE private static final Logger logger = Logger.getLogger(Backplane1Controller.class); private static final String NEW_CHANNEL_LAST_PATH = "new"; private static final String ERR_MSG_FIELD = "ERR_MSG"; private static final int CHANNEL_NAME_LENGTH = 32; private final com.yammer.metrics.core.Timer getBusMessagesTime = Metrics.newTimer( new MetricName("v1", this.getClass().getName().replace(".", "_"), "get_bus_messages_time"), TimeUnit.MILLISECONDS, TimeUnit.MINUTES); private final com.yammer.metrics.core.Timer getChannelMessagesTime = Metrics.newTimer( new MetricName("v1", this.getClass().getName().replace(".", "_"), "get_channel_messages_time"), TimeUnit.MILLISECONDS, TimeUnit.MINUTES); private final com.yammer.metrics.core.Timer getNewChannelTime = Metrics.newTimer( new MetricName("v1", this.getClass().getName().replace(".", "_"), "get_new_channel_time"), TimeUnit.MILLISECONDS, TimeUnit.MINUTES); private final com.yammer.metrics.core.Timer postMessagesTime = Metrics.newTimer( new MetricName("v1", this.getClass().getName().replace(".", "_"), "post_messages_time"), TimeUnit.MILLISECONDS, TimeUnit.MINUTES); private final Histogram payLoadSizesOnGets = Metrics .newHistogram(new MetricName("v1", this.getClass().getName().replace(".", "_"), "payload_sizes_gets")); @Inject private Backplane1Config bpConfig; @Inject private AnalyticsLogger anilogger; private static final Random random = new SecureRandom(); private User checkAuth(String basicAuth, String bus, BusConfig1.BUS_PERMISSION permission) throws AuthException { // authN String userPass = null; if (basicAuth == null || !basicAuth.startsWith("Basic ") || basicAuth.length() < 7) { authError("Invalid Authorization header: " + basicAuth); } else { try { userPass = new String(Base64.decodeBase64(basicAuth.substring(6).getBytes("utf-8"))); } catch (UnsupportedEncodingException e) { authError("Cannot check authentication, unsupported encoding: utf-8"); // shouldn't happen } } @SuppressWarnings({ "ConstantConditions" }) int delim = userPass.indexOf(":"); if (delim == -1) { authError("Invalid Basic auth token: " + userPass); } String user = userPass.substring(0, delim); String pass = userPass.substring(delim + 1); User userEntry; //userEntry = superSimpleDb.retrieve(bpConfig.getTableName(Backplane1Config.SimpleDBTables.BP1_USERS), User.class, user); userEntry = DaoFactory.getUserDAO().get(user); if (userEntry == null) { authError("User not found: " + user); } else if (!HmacHashUtils.checkHmacHash(pass, userEntry.get(User.Field.PWDHASH))) { authError("Incorrect password for user " + user); } // authZ BusConfig1 busConfig; //busConfig = superSimpleDb.retrieve(bpConfig.getTableName(BP1_BUS_CONFIG), BusConfig1.class, bus); busConfig = DaoFactory.getBusDAO().get(bus); if (busConfig == null) { authError("Bus configuration not found for " + bus); } else if (!busConfig.getPermissions(user).contains(permission)) { authError("User " + user + " denied " + permission + " to " + bus); } return userEntry; } private void authError(String errMsg) throws AuthException { logger.error(errMsg); try { throw new AuthException("Access denied. " + (bpConfig.isDebugMode() ? errMsg : "")); } catch (Exception e) { throw new AuthException("Access denied."); } } private String newChannel() { final TimerContext context = getNewChannelTime.time(); String newChannel = "\"" + randomString(CHANNEL_NAME_LENGTH) + "\""; context.stop(); return newChannel; } private List<BackplaneMessage> getChannelMessages(final String bus, final String channel, final String since, final String sticky) throws SimpleDBException, BackplaneServerException { final TimerContext context = getChannelMessagesTime.time(); try { return DaoFactory.getBackplaneMessageDAO().getMessagesByChannel(bus, channel, since, sticky); } catch (SimpleDBException sdbe) { throw sdbe; } catch (BackplaneServerException bse) { throw bse; } catch (Exception e) { throw new BackplaneServerException(e.getMessage(), e); } finally { context.stop(); } } private String messagesToFrames(List<BackplaneMessage> messages, final String version) throws BackplaneServerException { try { List<Map<String, Object>> frames = new ArrayList<Map<String, Object>>(); for (BackplaneMessage message : messages) { frames.add(message.asFrame(version)); } ObjectMapper mapper = new ObjectMapper(); try { String payload = mapper.writeValueAsString(frames); payLoadSizesOnGets.update(payload.length()); return payload; } catch (IOException e) { String errMsg = "Error converting frames to JSON: " + e.getMessage(); logger.error(errMsg, bpConfig.getDebugException(e)); throw new BackplaneServerException(errMsg, e); } } catch (BackplaneServerException bse) { throw bse; } catch (Exception e) { throw new BackplaneServerException(e.getMessage(), e); } } private void aniLogNewChannel(HttpServletRequest request, String referer, String version, String bus, String channel) { if (!anilogger.isEnabled()) { return; } String channelId = "https://" + request.getServerName() + "/" + version + "/bus/" + bus + "/channel/" + channel; String siteHost = (referer != null) ? ServletUtil.getHostFromUrl(referer) : null; Map<String, Object> aniEvent = new HashMap<String, Object>(); aniEvent.put("channel_id", channelId); aniEvent.put("bus", bus); aniEvent.put("version", version); aniEvent.put("site_host", siteHost); aniLog("new_channel", aniEvent); } private void aniLogPollMessages(HttpServletRequest request, String referer, String version, String bus, String channel, List<BackplaneMessage> messages) { if (!anilogger.isEnabled()) { return; } String channelId = "https://" + request.getServerName() + "/" + version + "/bus/" + bus + "/channel/" + channel; String siteHost = (referer != null) ? ServletUtil.getHostFromUrl(referer) : null; Map<String, Object> aniEvent = new HashMap<String, Object>(); aniEvent.put("channel_id", channelId); aniEvent.put("bus", bus); aniEvent.put("version", version); aniEvent.put("site_host", siteHost); List<String> messageIds = new ArrayList<String>(); for (BackplaneMessage message : messages) { messageIds.add(message.getIdValue()); } aniEvent.put("message_ids", messageIds); aniLog("poll_messages", aniEvent); } private void aniLogNewMessage(String version, String bus, String channelId, String clientId) { if (!anilogger.isEnabled()) { return; } Map<String, Object> aniEvent = new HashMap<String, Object>(); aniEvent.put("channel_id", channelId); aniEvent.put("bus", bus); aniEvent.put("version", version); aniEvent.put("client_id", clientId); aniLog("new_message", aniEvent); } private void aniLog(String eventName, Map<String, Object> eventData) { ObjectMapper mapper = new ObjectMapper(); String time = DateTimeUtils.ISO8601.get().format(new Date(System.currentTimeMillis())); eventData.put("time", time); try { anilogger.log(eventName, mapper.writeValueAsString(eventData)); } catch (Exception e) { String errMsg = "Error sending analytics event: " + e.getMessage(); logger.error(errMsg, bpConfig.getDebugException(e)); } } }