diff options
Diffstat (limited to 'subsonic-backend/src')
26 files changed, 2462 insertions, 0 deletions
diff --git a/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/Util.java b/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/Util.java new file mode 100644 index 00000000..31a1be71 --- /dev/null +++ b/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/Util.java @@ -0,0 +1,62 @@ +package net.sourceforge.subsonic.backend; + +import org.apache.log4j.Logger; +import org.apache.commons.lang.StringUtils; +import org.apache.commons.io.IOUtils; + +import java.io.File; +import java.io.Reader; +import java.io.FileReader; +import java.io.FileNotFoundException; +import java.io.IOException;/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2010 (C) Sindre Mehus + */ + +/** + * @author Sindre Mehus + */ +public class Util { + + private static final File BACKEND_HOME = new File("/var/subsonic-backend"); + private static final Logger LOG = Logger.getLogger(Util.class); + + private Util() { + } + + public static synchronized File getBackendHome() { + if (!BACKEND_HOME.exists() || !BACKEND_HOME.isDirectory()) { + boolean success = BACKEND_HOME.mkdirs(); + if (!success) { + String message = "The directory " + BACKEND_HOME + " does not exist. Please create it and make it writable."; + LOG.error(message); + throw new RuntimeException(message); + } + } + return BACKEND_HOME; + } + + public static String getPassword(String filename) throws IOException { + File pwdFile = new File(getBackendHome(), filename); + Reader reader = new FileReader(pwdFile); + try { + return StringUtils.trimToNull(IOUtils.toString(reader)); + } finally { + IOUtils.closeQuietly(reader); + } + } +} diff --git a/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/controller/IPNController.java b/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/controller/IPNController.java new file mode 100644 index 00000000..f6ced54f --- /dev/null +++ b/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/controller/IPNController.java @@ -0,0 +1,152 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.backend.controller; + +import org.apache.log4j.Logger; +import org.apache.http.client.HttpClient; +import org.apache.http.client.ResponseHandler; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.impl.client.BasicResponseHandler; +import org.apache.http.params.HttpConnectionParams; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.Controller; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.net.URLEncoder; +import java.util.Enumeration; +import java.util.Date; +import java.io.UnsupportedEncodingException; + +import net.sourceforge.subsonic.backend.domain.Payment; +import net.sourceforge.subsonic.backend.dao.PaymentDao; + +/** + * Processes IPNs (Instant Payment Notifications) from PayPal. + * + * @author Sindre Mehus + */ +public class IPNController implements Controller { + + private static final Logger LOG = Logger.getLogger(IPNController.class); + + private static final String PAYPAL_URL = "https://www.paypal.com/cgi-bin/webscr"; + + private PaymentDao paymentDao; + + public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception { + try { + + LOG.info("Incoming IPN from " + request.getRemoteAddr()); + + String url = createValidationURL(request); + if (validate(url)) { + LOG.info("Verified payment. " + url); + } else { + LOG.warn("Failed to verify payment. " + url); + } + createOrUpdatePayment(request); + + return null; + } catch (Exception x) { + LOG.error("Failed to process IPN.", x); + throw x; + } + } + + private String createValidationURL(HttpServletRequest request) throws UnsupportedEncodingException { + Enumeration<?> en = request.getParameterNames(); + StringBuilder url = new StringBuilder(PAYPAL_URL).append("?cmd=_notify-validate"); + String encoding = request.getParameter("charset"); + if (encoding == null) { + encoding = "ISO-8859-1"; + } + + while (en.hasMoreElements()) { + String paramName = (String) en.nextElement(); + String paramValue = request.getParameter(paramName); + url.append("&").append(paramName).append("=").append(URLEncoder.encode(paramValue, encoding)); + } + + return url.toString(); + } + + private void createOrUpdatePayment(HttpServletRequest request) { + String item = request.getParameter("item_number"); + if (item == null) { + item = request.getParameter("item_number1"); + } + String paymentStatus = request.getParameter("payment_status"); + String paymentType = request.getParameter("payment_type"); + int paymentAmount = Math.round(new Float(request.getParameter("mc_gross"))); + String paymentCurrency = request.getParameter("mc_currency"); + String txnId = request.getParameter("txn_id"); + String txnType = request.getParameter("txn_type"); + String payerEmail = request.getParameter("payer_email"); + String payerFirstName = request.getParameter("first_name"); + String payerLastName = request.getParameter("last_name"); + String payerCountry = request.getParameter("address_country"); + + Payment payment = paymentDao.getPaymentByTransactionId(txnId); + if (payment == null) { + payment = new Payment(null, txnId, txnType, item, paymentType, paymentStatus, + paymentAmount, paymentCurrency, payerEmail, payerFirstName, payerLastName, + payerCountry, Payment.ProcessingStatus.NEW, new Date(), new Date()); + paymentDao.createPayment(payment); + } else { + payment.setTransactionType(txnType); + payment.setItem(item); + payment.setPaymentType(paymentType); + payment.setPaymentStatus(paymentStatus); + payment.setPaymentAmount(paymentAmount); + payment.setPaymentCurrency(paymentCurrency); + payment.setPayerEmail(payerEmail); + payment.setPayerFirstName(payerFirstName); + payment.setPayerLastName(payerLastName); + payment.setPayerCountry(payerCountry); + payment.setLastUpdated(new Date()); + paymentDao.updatePayment(payment); + } + + LOG.info("Received " + payment); + } + + private boolean validate(String url) throws Exception { + HttpClient client = new DefaultHttpClient(); + HttpConnectionParams.setConnectionTimeout(client.getParams(), 60000); + HttpConnectionParams.setSoTimeout(client.getParams(), 60000); + HttpGet method = new HttpGet(url); + String content; + try { + ResponseHandler<String> responseHandler = new BasicResponseHandler(); + content = client.execute(method, responseHandler); + + LOG.info("Validation result: " + content); + return "VERIFIED".equals(content); + } finally { + client.getConnectionManager().shutdown(); + } + } + + public void setPaymentDao(PaymentDao paymentDao) { + this.paymentDao = paymentDao; + } + +} diff --git a/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/controller/MultiController.java b/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/controller/MultiController.java new file mode 100644 index 00000000..cdc1674a --- /dev/null +++ b/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/controller/MultiController.java @@ -0,0 +1,256 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.backend.controller; + +import java.io.IOException; +import java.io.PrintWriter; +import java.util.Calendar; +import java.util.Date; +import java.util.Map; +import java.util.HashMap; +import java.util.List; +import java.util.Arrays; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import net.sourceforge.subsonic.backend.dao.PaymentDao; +import net.sourceforge.subsonic.backend.service.LicenseGenerator; +import net.sourceforge.subsonic.backend.service.WhitelistGenerator; +import org.apache.log4j.Logger; +import org.apache.commons.lang.exception.ExceptionUtils; +import org.springframework.web.bind.ServletRequestBindingException; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.multiaction.MultiActionController; +import org.springframework.web.bind.ServletRequestUtils; +import org.springframework.jdbc.core.ColumnMapRowMapper; +import org.springframework.dao.DataAccessException; +import net.sourceforge.subsonic.backend.dao.DaoHelper; +import net.sourceforge.subsonic.backend.Util; +import net.sourceforge.subsonic.backend.service.EmailSession; + +/** + * Multi-controller used for simple pages. + * + * @author Sindre Mehus + */ +public class MultiController extends MultiActionController { + + private static final Logger LOG = Logger.getLogger(RedirectionController.class); + + private static final String SUBSONIC_VERSION = "4.6"; + private static final String SUBSONIC_BETA_VERSION = "4.7.beta2"; + + private static final Date LICENSE_DATE_THRESHOLD; + + private DaoHelper daoHelper; + + private PaymentDao paymentDao; + private WhitelistGenerator whitelistGenerator; + private LicenseGenerator licenseGenerator; + + static { + Calendar calendar = Calendar.getInstance(); + calendar.clear(); + calendar.set(Calendar.YEAR, 2010); + calendar.set(Calendar.MONTH, Calendar.JUNE); + calendar.set(Calendar.DAY_OF_MONTH, 19); + LICENSE_DATE_THRESHOLD = calendar.getTime(); + } + + public ModelAndView version(HttpServletRequest request, HttpServletResponse response) throws Exception { + + String localVersion = request.getParameter("v"); + LOG.info(request.getRemoteAddr() + " asked for latest version. Local version: " + localVersion); + + PrintWriter writer = response.getWriter(); + + writer.println("SUBSONIC_VERSION_BEGIN" + SUBSONIC_VERSION + "SUBSONIC_VERSION_END"); + writer.println("SUBSONIC_FULL_VERSION_BEGIN" + SUBSONIC_VERSION + "SUBSONIC_FULL_VERSION_END"); + writer.println("SUBSONIC_BETA_VERSION_BEGIN" + SUBSONIC_BETA_VERSION + "SUBSONIC_BETA_VERSION_END"); + + return null; + } + + public ModelAndView validateLicense(HttpServletRequest request, HttpServletResponse response) throws Exception { + + String email = request.getParameter("email"); + Long date = ServletRequestUtils.getLongParameter(request, "date"); + + boolean valid = isLicenseValid(email, date); + LOG.info(request.getRemoteAddr() + " asked to validate license for " + email + ". Result: " + valid); + + PrintWriter writer = response.getWriter(); + writer.println(valid); + + return null; + } + + public ModelAndView sendMail(HttpServletRequest request, HttpServletResponse response) throws Exception { + String from = request.getParameter("from"); + String to = request.getParameter("to"); + String subject = request.getParameter("subject"); + String text = request.getParameter("text"); + + EmailSession session = new EmailSession(); + session.sendMessage(from, Arrays.asList(to), null, null, null, subject, text); + + LOG.info("Sent email on behalf of " + request.getRemoteAddr() + " to " + to + " with subject '" + subject + "'"); + + return null; + } + + public ModelAndView db(HttpServletRequest request, HttpServletResponse response) throws Exception { + + if (!authenticate(request, response)) { + return null; + } + + Map<String, Object> map = new HashMap<String, Object>(); + + map.put("p", request.getParameter("p")); + String query = request.getParameter("query"); + if (query != null) { + map.put("query", query); + + try { + List<?> result = daoHelper.getJdbcTemplate().query(query, new ColumnMapRowMapper()); + map.put("result", result); + } catch (DataAccessException x) { + map.put("error", ExceptionUtils.getRootCause(x).getMessage()); + } + } + + return new ModelAndView("backend/db", "model", map); + } + + public ModelAndView payment(HttpServletRequest request, HttpServletResponse response) throws Exception { + + if (!authenticate(request, response)) { + return null; + } + + Map<String, Object> map = new HashMap<String, Object>(); + + Calendar startOfToday = Calendar.getInstance(); + startOfToday.set(Calendar.HOUR_OF_DAY, 0); + startOfToday.set(Calendar.MINUTE, 0); + startOfToday.set(Calendar.SECOND, 0); + startOfToday.set(Calendar.MILLISECOND, 0); + + Calendar endOfToday = Calendar.getInstance(); + endOfToday.setTime(startOfToday.getTime()); + endOfToday.add(Calendar.DATE, 1); + + Calendar startOfYesterday = Calendar.getInstance(); + startOfYesterday.setTime(startOfToday.getTime()); + startOfYesterday.add(Calendar.DATE, -1); + + Calendar startOfMonth = Calendar.getInstance(); + startOfMonth.setTime(startOfToday.getTime()); + startOfMonth.set(Calendar.DATE, 1); + + int sumToday = paymentDao.getPaymentAmount(startOfToday.getTime(), endOfToday.getTime()); + int sumYesterday = paymentDao.getPaymentAmount(startOfYesterday.getTime(), startOfToday.getTime()); + int sumMonth = paymentDao.getPaymentAmount(startOfMonth.getTime(), endOfToday.getTime()); + int dayAverageThisMonth = sumMonth / startOfToday.get(Calendar.DATE); + + map.put("sumToday", sumToday); + map.put("sumYesterday", sumYesterday); + map.put("sumMonth", sumMonth); + map.put("dayAverageThisMonth", dayAverageThisMonth); + + return new ModelAndView("backend/payment", "model", map); + } + + public ModelAndView requestLicense(HttpServletRequest request, HttpServletResponse response) throws Exception { + + String email = request.getParameter("email"); + boolean valid = email != null && isLicenseValid(email, System.currentTimeMillis()); + if (valid) { + EmailSession session = new EmailSession(); + licenseGenerator.sendLicenseTo(email, session); + } + + Map<String, Object> map = new HashMap<String, Object>(); + + map.put("email", email); + map.put("valid", valid); + + return new ModelAndView("backend/requestLicense", "model", map); + } + + public ModelAndView whitelist(HttpServletRequest request, HttpServletResponse response) throws Exception { + if (!authenticate(request, response)) { + return null; + } + + Date newerThan = MultiController.LICENSE_DATE_THRESHOLD; + Integer days = ServletRequestUtils.getIntParameter(request, "days"); + if (days != null) { + Calendar cal = Calendar.getInstance(); + cal.add(Calendar.DATE, -days); + newerThan = cal.getTime(); + } + whitelistGenerator.generate(newerThan); + return null; + } + + private boolean authenticate(HttpServletRequest request, HttpServletResponse response) throws ServletRequestBindingException, IOException { + String password = ServletRequestUtils.getRequiredStringParameter(request, "p"); + if (!password.equals(Util.getPassword("backendpwd.txt"))) { + response.sendError(HttpServletResponse.SC_FORBIDDEN); + return false; + } + return true; + } + + private boolean isLicenseValid(String email, Long date) { + if (email == null || date == null) { + return false; + } + + if (paymentDao.isBlacklisted(email)) { + return false; + } + + // Always accept licenses that are older than 2010-06-19. + if (date < LICENSE_DATE_THRESHOLD.getTime()) { + return true; + } + + return paymentDao.getPaymentByEmail(email) != null || paymentDao.isWhitelisted(email); + } + + public void setDaoHelper(DaoHelper daoHelper) { + this.daoHelper = daoHelper; + } + + public void setPaymentDao(PaymentDao paymentDao) { + this.paymentDao = paymentDao; + } + + public void setWhitelistGenerator(WhitelistGenerator whitelistGenerator) { + this.whitelistGenerator = whitelistGenerator; + } + + public void setLicenseGenerator(LicenseGenerator licenseGenerator) { + this.licenseGenerator = licenseGenerator; + } +} diff --git a/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/controller/RedirectionController.java b/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/controller/RedirectionController.java new file mode 100644 index 00000000..3f36ec6d --- /dev/null +++ b/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/controller/RedirectionController.java @@ -0,0 +1,155 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.backend.controller; + +import net.sourceforge.subsonic.backend.dao.RedirectionDao; +import net.sourceforge.subsonic.backend.domain.Redirection; +import static net.sourceforge.subsonic.backend.controller.RedirectionManagementController.RESERVED_REDIRECTS; +import org.apache.commons.lang.StringUtils; +import org.apache.log4j.Logger; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.Controller; +import org.springframework.web.servlet.view.RedirectView; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLEncoder; +import java.util.Date; +import java.util.Enumeration; +import java.io.UnsupportedEncodingException; + +/** + * Redirects vanity URLs (such as http://sindre.subsonic.org). + * + * @author Sindre Mehus + */ +public class RedirectionController implements Controller { + + private static final Logger LOG = Logger.getLogger(RedirectionController.class); + private RedirectionDao redirectionDao; + + public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception { + + String redirectFrom = getRedirectFrom(request); + if (RESERVED_REDIRECTS.containsKey(redirectFrom)) { + LOG.info("Reserved redirection: " + redirectFrom); + return new ModelAndView(new RedirectView(RESERVED_REDIRECTS.get(redirectFrom))); + } + + Redirection redirection = redirectFrom == null ? null : redirectionDao.getRedirection(redirectFrom); + + if (redirection == null) { + LOG.info("No redirection found: " + redirectFrom); + return new ModelAndView(new RedirectView("http://subsonic.org/pages")); + } + + redirection.setLastRead(new Date()); + redirection.setReadCount(redirection.getReadCount() + 1); + redirectionDao.updateRedirection(redirection); + + // Check for trial expiration (unless called from REST client for which the Subsonic server manages trial expiry). + if (isTrialExpired(redirection) && !isREST(request)) { + LOG.info("Expired redirection: " + redirectFrom); + return new ModelAndView(new RedirectView("http://subsonic.org/pages/redirect-expired.jsp?redirectFrom=" + + redirectFrom + "&expired=" + redirection.getTrialExpires().getTime())); + } + + String requestUrl = getFullRequestURL(request); + String to = StringUtils.removeEnd(getRedirectTo(request, redirection), "/"); + String redirectTo = requestUrl.replaceFirst("http://" + redirectFrom + "\\.subsonic\\.org", to); + LOG.info("Redirecting from " + requestUrl + " to " + redirectTo); + + return new ModelAndView(new RedirectView(redirectTo)); + } + + private String getRedirectTo(HttpServletRequest request, Redirection redirection) { + + // If the request comes from within the same LAN as the destination Subsonic + // server, redirect using the local IP address of the server. + + String localRedirectTo = redirection.getLocalRedirectTo(); + if (localRedirectTo != null) { + try { + URL url = new URL(redirection.getRedirectTo()); + if (url.getHost().equals(request.getRemoteAddr())) { + return localRedirectTo; + } + } catch (Throwable x) { + LOG.error("Malformed local redirect URL.", x); + } + } + + return redirection.getRedirectTo(); + } + + private boolean isTrialExpired(Redirection redirection) { + return redirection.isTrial() && redirection.getTrialExpires() != null && redirection.getTrialExpires().before(new Date()); + } + + private boolean isREST(HttpServletRequest request) { + return request.getParameter("c") != null; + } + + private String getFullRequestURL(HttpServletRequest request) throws UnsupportedEncodingException { + StringBuilder builder = new StringBuilder(request.getRequestURL()); + + // For backwards compatibility; return query parameters in exact same sequence. + if ("GET".equalsIgnoreCase(request.getMethod())) { + if (request.getQueryString() != null) { + builder.append("?").append(request.getQueryString()); + } + return builder.toString(); + } + + builder.append("?"); + + Enumeration<?> paramNames = request.getParameterNames(); + while (paramNames.hasMoreElements()) { + String paramName = (String) paramNames.nextElement(); + String[] paramValues = request.getParameterValues(paramName); + for (String paramValue : paramValues) { + String p = URLEncoder.encode(paramValue, "UTF-8"); + builder.append(paramName).append("=").append(p).append("&"); + } + } + + return builder.toString(); + } + + private String getRedirectFrom(HttpServletRequest request) throws MalformedURLException { + URL url = new URL(request.getRequestURL().toString()); + String host = url.getHost(); + + String redirectFrom; + if (host.contains(".")) { + redirectFrom = StringUtils.substringBefore(host, "."); + } else { + // For testing. + redirectFrom = request.getParameter("redirectFrom"); + } + + return StringUtils.lowerCase(redirectFrom); + } + + public void setRedirectionDao(RedirectionDao redirectionDao) { + this.redirectionDao = redirectionDao; + } +} diff --git a/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/controller/RedirectionManagementController.java b/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/controller/RedirectionManagementController.java new file mode 100644 index 00000000..01ff90fa --- /dev/null +++ b/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/controller/RedirectionManagementController.java @@ -0,0 +1,273 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.backend.controller; + +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; +import java.net.URL; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.HashMap; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.net.ssl.SSLPeerUnverifiedException; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang.StringUtils; +import org.apache.log4j.Logger; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.HttpStatus; +import org.apache.http.HttpResponse; +import org.apache.http.StatusLine; +import org.apache.http.params.HttpConnectionParams; +import org.springframework.web.bind.ServletRequestUtils; +import org.springframework.web.servlet.mvc.multiaction.MultiActionController; + +import net.sourceforge.subsonic.backend.dao.RedirectionDao; +import net.sourceforge.subsonic.backend.domain.Redirection; + +/** + * @author Sindre Mehus + */ +public class RedirectionManagementController extends MultiActionController { + + private static final Logger LOG = Logger.getLogger(RedirectionManagementController.class); + + public static final Map<String,String> RESERVED_REDIRECTS = new HashMap<String, String>(); + + static { + RESERVED_REDIRECTS.put("forum", "http://www.activeobjects.no/subsonic/forum/index.php"); + RESERVED_REDIRECTS.put("www", "http://www.subsonic.org/pages/index.jsp"); + RESERVED_REDIRECTS.put("web", "http://www.subsonic.org/pages/index.jsp"); + RESERVED_REDIRECTS.put("ftp", "http://www.subsonic.org/pages/index.jsp"); + RESERVED_REDIRECTS.put("mail", "http://www.subsonic.org/pages/index.jsp"); + RESERVED_REDIRECTS.put("s", "http://www.subsonic.org/pages/index.jsp"); + RESERVED_REDIRECTS.put("subsonic", "http://www.subsonic.org/pages/index.jsp"); + RESERVED_REDIRECTS.put("gosubsonic", "http://www.subsonic.org/pages/index.jsp"); + RESERVED_REDIRECTS.put("android", "http://www.subsonic.org/pages/android.jsp"); + RESERVED_REDIRECTS.put("iphone", "http://www.subsonic.org/pages/index.jsp"); + RESERVED_REDIRECTS.put("subair", "http://www.subsonic.org/pages/index.jsp"); + RESERVED_REDIRECTS.put("m", "http://www.subsonic.org/pages/index.jsp"); + RESERVED_REDIRECTS.put("link", "http://www.subsonic.org/pages/index.jsp"); + RESERVED_REDIRECTS.put("share", "http://www.subsonic.org/pages/index.jsp"); + RESERVED_REDIRECTS.put("mobile", "http://www.subsonic.org/pages/index.jsp"); + RESERVED_REDIRECTS.put("mobil", "http://www.subsonic.org/pages/index.jsp"); + RESERVED_REDIRECTS.put("phone", "http://www.subsonic.org/pages/index.jsp"); + RESERVED_REDIRECTS.put("wap", "http://www.subsonic.org/pages/index.jsp"); + RESERVED_REDIRECTS.put("db", "http://www.subsonic.org/pages/index.jsp"); + RESERVED_REDIRECTS.put("shop", "http://www.subsonic.org/pages/index.jsp"); + RESERVED_REDIRECTS.put("wiki", "http://www.subsonic.org/pages/index.jsp"); + RESERVED_REDIRECTS.put("test", "http://www.subsonic.org/pages/index.jsp"); + } + + private RedirectionDao redirectionDao; + + public void register(HttpServletRequest request, HttpServletResponse response) throws Exception { + + String redirectFrom = StringUtils.lowerCase(ServletRequestUtils.getRequiredStringParameter(request, "redirectFrom")); + String licenseHolder = ServletRequestUtils.getStringParameter(request, "licenseHolder"); + String serverId = ServletRequestUtils.getRequiredStringParameter(request, "serverId"); + int port = ServletRequestUtils.getRequiredIntParameter(request, "port"); + Integer localPort = ServletRequestUtils.getIntParameter(request, "localPort"); + String localIp = ServletRequestUtils.getStringParameter(request, "localIp"); + String contextPath = ServletRequestUtils.getRequiredStringParameter(request, "contextPath"); + boolean trial = ServletRequestUtils.getBooleanParameter(request, "trial", false); + + Date now = new Date(); + Date trialExpires = null; + if (trial) { + trialExpires = new Date(ServletRequestUtils.getRequiredLongParameter(request, "trialExpires")); + } + + if (RESERVED_REDIRECTS.containsKey(redirectFrom)) { + sendError(response, "\"" + redirectFrom + "\" is a reserved address. Please select another."); + return; + } + + if (!redirectFrom.matches("(\\w|\\-)+")) { + sendError(response, "Illegal characters present in \"" + redirectFrom + "\". Please select another."); + return; + } + + String host = request.getRemoteAddr(); + URL url = new URL("http", host, port, "/" + contextPath); + String redirectTo = url.toExternalForm(); + + String localRedirectTo = null; + if (localIp != null && localPort != null) { + URL localUrl = new URL("http", localIp, localPort, "/" + contextPath); + localRedirectTo = localUrl.toExternalForm(); + } + + Redirection redirection = redirectionDao.getRedirection(redirectFrom); + if (redirection == null) { + + // Delete other redirects for same server ID. + redirectionDao.deleteRedirectionsByServerId(serverId); + + redirection = new Redirection(0, licenseHolder, serverId, redirectFrom, redirectTo, localRedirectTo, trial, trialExpires, now, null, 0); + redirectionDao.createRedirection(redirection); + LOG.info("Created " + redirection); + + } else { + + boolean sameServerId = serverId.equals(redirection.getServerId()); + boolean sameLicenseHolder = licenseHolder != null && licenseHolder.equals(redirection.getLicenseHolder()); + + // Note: A licensed user can take over any expired trial domain. + boolean existingTrialExpired = redirection.getTrialExpires() != null && redirection.getTrialExpires().before(now); + + if (sameServerId || sameLicenseHolder || (existingTrialExpired && !trial)) { + redirection.setLicenseHolder(licenseHolder); + redirection.setServerId(serverId); + redirection.setRedirectFrom(redirectFrom); + redirection.setRedirectTo(redirectTo); + redirection.setLocalRedirectTo(localRedirectTo); + redirection.setTrial(trial); + redirection.setTrialExpires(trialExpires); + redirection.setLastUpdated(now); + redirectionDao.updateRedirection(redirection); + LOG.info("Updated " + redirection); + } else { + sendError(response, "The web address \"" + redirectFrom + "\" is already in use. Please select another."); + } + } + } + + public void unregister(HttpServletRequest request, HttpServletResponse response) throws Exception { + String serverId = ServletRequestUtils.getStringParameter(request, "serverId"); + if (!StringUtils.isEmpty(serverId)) { + redirectionDao.deleteRedirectionsByServerId(serverId); + } + } + + public void get(HttpServletRequest request, HttpServletResponse response) throws Exception { + String redirectFrom = StringUtils.lowerCase(ServletRequestUtils.getRequiredStringParameter(request, "redirectFrom")); + + Redirection redirection = redirectionDao.getRedirection(redirectFrom); + if (redirection == null) { + response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Web address " + redirectFrom + ".subsonic.org not registered."); + return; + } + + PrintWriter writer = response.getWriter(); + String url = redirection.getRedirectTo(); + if (!url.endsWith("/")) { + url += "/"; + } + writer.println(url); + + url = redirection.getLocalRedirectTo(); + if (!url.endsWith("/")) { + url += "/"; + } + writer.println(url); + } + + public void test(HttpServletRequest request, HttpServletResponse response) throws Exception { + String redirectFrom = StringUtils.lowerCase(ServletRequestUtils.getRequiredStringParameter(request, "redirectFrom")); + PrintWriter writer = response.getWriter(); + + Redirection redirection = redirectionDao.getRedirection(redirectFrom); + String webAddress = redirectFrom + ".subsonic.org"; + if (redirection == null) { + writer.print("Web address " + webAddress + " not registered."); + return; + } + + if (redirection.getTrialExpires() != null && redirection.getTrialExpires().before(new Date())) { + writer.print("Trial period expired. Please donate to activate web address."); + return; + } + + String url = redirection.getRedirectTo(); + if (!url.endsWith("/")) { + url += "/"; + } + url += "icons/favicon.ico"; + + HttpClient client = new DefaultHttpClient(); + HttpConnectionParams.setConnectionTimeout(client.getParams(), 15000); + HttpConnectionParams.setSoTimeout(client.getParams(), 15000); + HttpGet method = new HttpGet(url); + + try { + HttpResponse resp = client.execute(method); + StatusLine status = resp.getStatusLine(); + + if (status.getStatusCode() == HttpStatus.SC_OK) { + String msg = webAddress + " responded successfully."; + writer.print(msg); + LOG.info(msg); + } else { + String msg = webAddress + " returned HTTP error code " + status.getStatusCode() + " " + status.getReasonPhrase(); + writer.print(msg); + LOG.info(msg); + } + } catch (SSLPeerUnverifiedException x) { + String msg = webAddress + " responded successfully, but could not authenticate it."; + writer.print(msg); + LOG.info(msg); + + } catch (Throwable x) { + String msg = webAddress + " is registered, but could not connect to it. (" + x.getClass().getSimpleName() + ")"; + writer.print(msg); + LOG.info(msg); + } finally { + client.getConnectionManager().shutdown(); + } + } + + private void sendError(HttpServletResponse response, String message) throws IOException { + response.getWriter().print(message); + response.setStatus(HttpServletResponse.SC_BAD_REQUEST); + } + + public void dump(HttpServletRequest request, HttpServletResponse response) throws Exception { + + File file = File.createTempFile("redirections", ".txt"); + PrintWriter writer = new PrintWriter(file, "UTF-8"); + try { + int offset = 0; + int count = 100; + while (true) { + List<Redirection> redirections = redirectionDao.getAllRedirections(offset, count); + if (redirections.isEmpty()) { + break; + } + offset += redirections.size(); + for (Redirection redirection : redirections) { + writer.println(redirection); + } + } + LOG.info("Dumped redirections to " + file.getAbsolutePath()); + } finally { + IOUtils.closeQuietly(writer); + } + } + + public void setRedirectionDao(RedirectionDao redirectionDao) { + this.redirectionDao = redirectionDao; + } +}
\ No newline at end of file diff --git a/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/dao/AbstractDao.java b/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/dao/AbstractDao.java new file mode 100644 index 00000000..86830b7c --- /dev/null +++ b/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/dao/AbstractDao.java @@ -0,0 +1,57 @@ +package net.sourceforge.subsonic.backend.dao; + +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; + +import java.util.List; + +/** + * Abstract superclass for all DAO's. + * + * @author Sindre Mehus + */ +public class AbstractDao { + private DaoHelper daoHelper; + + /** + * Returns a JDBC template for performing database operations. + * @return A JDBC template. + */ + public JdbcTemplate getJdbcTemplate() { + return daoHelper.getJdbcTemplate(); + } + + protected String questionMarks(String columns) { + int count = columns.split(", ").length; + StringBuffer buf = new StringBuffer(); + for (int i = 0; i < count; i++) { + buf.append('?'); + if (i < count - 1) { + buf.append(", "); + } + } + return buf.toString(); + } + + protected int update(String sql, Object... args) { + return getJdbcTemplate().update(sql, args); + } + + protected <T> List<T> query(String sql, RowMapper rowMapper, Object... args) { + return getJdbcTemplate().query(sql, args, rowMapper); + } + + protected <T> T queryOne(String sql, RowMapper rowMapper, Object... args) { + List<T> result = query(sql, rowMapper, args); + return result.isEmpty() ? null : result.get(0); + } + + protected Integer queryForInt(String sql, Integer defaultValue, Object... args) { + List<Integer> result = getJdbcTemplate().queryForList(sql, args, Integer.class); + return result.isEmpty() ? defaultValue : result.get(0) == null ? defaultValue : result.get(0); + } + + public void setDaoHelper(DaoHelper daoHelper) { + this.daoHelper = daoHelper; + } +} diff --git a/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/dao/DaoHelper.java b/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/dao/DaoHelper.java new file mode 100644 index 00000000..2f5911f9 --- /dev/null +++ b/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/dao/DaoHelper.java @@ -0,0 +1,84 @@ +package net.sourceforge.subsonic.backend.dao; + +import net.sourceforge.subsonic.backend.dao.schema.Schema; +import net.sourceforge.subsonic.backend.dao.schema.Schema10; +import net.sourceforge.subsonic.backend.dao.schema.Schema20; +import net.sourceforge.subsonic.backend.Util; +import org.apache.log4j.Logger; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.datasource.DriverManagerDataSource; + +import javax.sql.DataSource; +import java.io.File; + +/** + * DAO helper class which creates the data source, and updates the database schema. + * + * @author Sindre Mehus + */ +public class DaoHelper { + + private static final Logger LOG = Logger.getLogger(DaoHelper.class); + + private Schema[] schemas = {new Schema10(), new Schema20()}; + private DataSource dataSource; + private static boolean shutdownHookAdded; + + public DaoHelper() { + dataSource = createDataSource(); + checkDatabase(); + addShutdownHook(); + } + + private void addShutdownHook() { + if (shutdownHookAdded) { + return; + } + shutdownHookAdded = true; + Runtime.getRuntime().addShutdownHook(new Thread() { + @Override + public void run() { + System.err.println("Shutting down database."); + try { + getJdbcTemplate().execute("shutdown"); + System.err.println("Done."); + } catch (Throwable x) { + System.err.println("Failed to shut down database."); + x.printStackTrace(); + } + } + }); + } + + /** + * Returns a JDBC template for performing database operations. + * + * @return A JDBC template. + */ + public JdbcTemplate getJdbcTemplate() { + return new JdbcTemplate(dataSource); + } + + private DataSource createDataSource() { + File home = Util.getBackendHome(); + DriverManagerDataSource ds = new DriverManagerDataSource(); + ds.setDriverClassName("org.hsqldb.jdbcDriver"); + ds.setUrl("jdbc:hsqldb:file:" + home.getPath() + "/db/subsonic-backend"); + ds.setUsername("sa"); + ds.setPassword(""); + + return ds; + } + + private void checkDatabase() { + LOG.info("Checking database schema."); + try { + for (Schema schema : schemas) { + schema.execute(getJdbcTemplate()); + } + LOG.info("Done checking database schema."); + } catch (Exception x) { + LOG.error("Failed to initialize database.", x); + } + } +} diff --git a/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/dao/PaymentDao.java b/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/dao/PaymentDao.java new file mode 100644 index 00000000..e9f2eb21 --- /dev/null +++ b/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/dao/PaymentDao.java @@ -0,0 +1,125 @@ +package net.sourceforge.subsonic.backend.dao; + +import net.sourceforge.subsonic.backend.domain.Payment; +import org.apache.commons.lang.StringUtils; +import org.apache.log4j.Logger; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.simple.ParameterizedRowMapper; +import org.springframework.jdbc.core.simple.ParameterizedSingleColumnRowMapper; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Date; +import java.util.List; + +/** + * Provides database services for PayPal payments. + * + * @author Sindre Mehus + */ +public class PaymentDao extends AbstractDao { + + private static final Logger LOG = Logger.getLogger(PaymentDao.class); + private static final String COLUMNS = "id, transaction_id, transaction_type, item, " + + "payment_type, payment_status, payment_amount, payment_currency, " + + "payer_email, payer_email_lower, payer_first_name, payer_last_name, payer_country, " + + "processing_status, created, last_updated"; + + private RowMapper paymentRowMapper = new PaymentRowMapper(); + private RowMapper listRowMapper = new ParameterizedSingleColumnRowMapper<Integer>(); + + /** + * Returns the payment with the given transaction ID. + * + * @param transactionId The transaction ID. + * @return The payment or <code>null</code> if not found. + */ + public Payment getPaymentByTransactionId(String transactionId) { + String sql = "select " + COLUMNS + " from payment where transaction_id=?"; + return queryOne(sql, paymentRowMapper, transactionId); + } + + /** + * Returns the payment with the given payer email. + * + * @param email The payer email. + * @return The payment or <code>null</code> if not found. + */ + public Payment getPaymentByEmail(String email) { + if (email == null) { + return null; + } + String sql = "select " + COLUMNS + " from payment where payer_email_lower=?"; + return queryOne(sql, paymentRowMapper, email.toLowerCase()); + } + + /** + * Returns all payments with the given processing status. + * + * @param status The status. + * @return List of payments. + */ + public List<Payment> getPaymentsByProcessingStatus(Payment.ProcessingStatus status) { + return query("select " + COLUMNS + " from payment where processing_status=?", paymentRowMapper, status.name()); + } + + /** + * Creates a new payment. + * + * @param payment The payment to create. + */ + public void createPayment(Payment payment) { + String sql = "insert into payment (" + COLUMNS + ") values (" + questionMarks(COLUMNS) + ")"; + update(sql, null, payment.getTransactionId(), payment.getTransactionType(), payment.getItem(), + payment.getPaymentType(), payment.getPaymentStatus(), payment.getPaymentAmount(), + payment.getPaymentCurrency(), payment.getPayerEmail(), StringUtils.lowerCase(payment.getPayerEmail()), + payment.getPayerFirstName(), payment.getPayerLastName(), payment.getPayerCountry(), + payment.getProcessingStatus().name(), payment.getCreated(), payment.getLastUpdated()); + LOG.info("Created " + payment); + } + + /** + * Updates the given payment. + * + * @param payment The payment to update. + */ + public void updatePayment(Payment payment) { + String sql = "update payment set transaction_type=?, item=?, payment_type=?, payment_status=?, " + + "payment_amount=?, payment_currency=?, payer_email=?, payer_email_lower=?, payer_first_name=?, payer_last_name=?, " + + "payer_country=?, processing_status=?, created=?, last_updated=? where id=?"; + update(sql, payment.getTransactionType(), payment.getItem(), payment.getPaymentType(), payment.getPaymentStatus(), + payment.getPaymentAmount(), payment.getPaymentCurrency(), payment.getPayerEmail(), StringUtils.lowerCase(payment.getPayerEmail()), + payment.getPayerFirstName(), payment.getPayerLastName(), payment.getPayerCountry(), payment.getProcessingStatus().name(), + payment.getCreated(), payment.getLastUpdated(), payment.getId()); + LOG.info("Updated " + payment); + } + + public int getPaymentAmount(Date from, Date to) { + String sql = "select sum(payment_amount) from payment where created between ? and ?"; + return queryForInt(sql, 0, from, to); + } + + public boolean isBlacklisted(String email) { + String sql = "select 1 from blacklist where email=?"; + return queryOne(sql, listRowMapper, StringUtils.lowerCase(email)) != null; + } + + public boolean isWhitelisted(String email) { + String sql = "select 1 from whitelist where email=?"; + return queryOne(sql, listRowMapper, StringUtils.lowerCase(email)) != null; + } + + public void whitelist(String email) { + update("insert into whitelist(email) values (?)", StringUtils.lowerCase(email)); + } + + private static class PaymentRowMapper implements ParameterizedRowMapper<Payment> { + + public Payment mapRow(ResultSet rs, int rowNum) throws SQLException { + return new Payment(rs.getString(1), rs.getString(2), rs.getString(3), rs.getString(4), rs.getString(5), + rs.getString(6), rs.getInt(7), rs.getString(8), rs.getString(9), rs.getString(11), + rs.getString(12), rs.getString(13), Payment.ProcessingStatus.valueOf(rs.getString(14)), + rs.getTimestamp(15), rs.getTimestamp(16)); + } + } +}
\ No newline at end of file diff --git a/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/dao/RedirectionDao.java b/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/dao/RedirectionDao.java new file mode 100644 index 00000000..edd73222 --- /dev/null +++ b/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/dao/RedirectionDao.java @@ -0,0 +1,96 @@ +package net.sourceforge.subsonic.backend.dao; + +import net.sourceforge.subsonic.backend.domain.Redirection; +import org.apache.log4j.Logger; +import org.springframework.jdbc.core.simple.ParameterizedRowMapper; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +/** + * Provides database services for xxx.subsonic.org redirections. + * + * @author Sindre Mehus + */ +public class RedirectionDao extends AbstractDao { + + private static final Logger LOG = Logger.getLogger(RedirectionDao.class); + private static final String COLUMNS = "id, license_holder, server_id, redirect_from, redirect_to, local_redirect_to, trial, trial_expires, last_updated, last_read, read_count"; + + private RedirectionRowMapper rowMapper = new RedirectionRowMapper(); + + /** + * Returns the redirection with the given "redirect from". + * + * @param redirectFrom The "redirect from" string. + * @return The redirection or <code>null</code> if not found. + */ + public Redirection getRedirection(String redirectFrom) { + String sql = "select " + COLUMNS + " from redirection where redirect_from=?"; + return queryOne(sql, rowMapper, redirectFrom); + } + + /** + * Returns all redirections with respect to the given row offset and count. + * + * @param offset Number of rows to skip. + * @param count Maximum number of rows to return. + * @return Redirections with respect to the given row offset and count. + */ + public List<Redirection> getAllRedirections(int offset, int count) { + if (count < 1) { + return new ArrayList<Redirection>(); + } + String sql = "select " + COLUMNS + " from redirection " + + "order by id " + + "limit " + count + " offset " + offset; + return query(sql, rowMapper); + } + + /** + * Creates a new redirection. + * + * @param redirection The redirection to create. + */ + public void createRedirection(Redirection redirection) { + String sql = "insert into redirection (" + COLUMNS + ") values (null, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; + update(sql, redirection.getLicenseHolder(), redirection.getServerId(), redirection.getRedirectFrom(), + redirection.getRedirectTo(), redirection.getLocalRedirectTo(), redirection.isTrial(), + redirection.getTrialExpires(), redirection.getLastUpdated(), + redirection.getLastRead(), redirection.getReadCount()); + LOG.info("Created redirection " + redirection.getRedirectFrom() + " -> " + redirection.getRedirectTo()); + } + + /** + * Updates the given redirection. + * + * @param redirection The redirection to update. + */ + public void updateRedirection(Redirection redirection) { + String sql = "update redirection set license_holder=?, server_id=?, redirect_from=?, redirect_to=?, " + + "local_redirect_to=?, trial=?, trial_expires=?, last_updated=?, last_read=?, read_count=? where id=?"; + update(sql, redirection.getLicenseHolder(), redirection.getServerId(), redirection.getRedirectFrom(), + redirection.getRedirectTo(), redirection.getLocalRedirectTo(), redirection.isTrial(), redirection.getTrialExpires(), + redirection.getLastUpdated(), redirection.getLastRead(), redirection.getReadCount(), redirection.getId()); + } + + /** + * Deletes all redirections with the given server ID. + * + * @param serverId The server ID. + */ + public void deleteRedirectionsByServerId(String serverId) { + update("delete from redirection where server_id=?", serverId); + LOG.info("Deleted redirections for server ID " + serverId); + } + + private static class RedirectionRowMapper implements ParameterizedRowMapper<Redirection> { + public Redirection mapRow(ResultSet rs, int rowNum) throws SQLException { + return new Redirection(rs.getInt(1), rs.getString(2), rs.getString(3), rs.getString(4), rs.getString(5), + rs.getString(6), rs.getBoolean(7), rs.getTimestamp(8), rs.getTimestamp(9), rs.getTimestamp(10), + rs.getInt(11)); + } + } +} diff --git a/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/dao/schema/Schema.java b/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/dao/schema/Schema.java new file mode 100644 index 00000000..850b82da --- /dev/null +++ b/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/dao/schema/Schema.java @@ -0,0 +1,66 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.backend.dao.schema; + +import org.springframework.jdbc.core.JdbcTemplate; + +/** + * Used for creating and evolving the database schema. + * + * @author Sindre Mehus + */ +public abstract class Schema { + + /** + * Executes this schema. + * @param template The JDBC template to use. + */ + public abstract void execute(JdbcTemplate template); + + /** + * Returns whether the given table exists. + * @param template The JDBC template to use. + * @param table The table in question. + * @return Whether the table exists. + */ + protected boolean tableExists(JdbcTemplate template, String table) { + try { + template.execute("select 1 from " + table); + } catch (Exception x) { + return false; + } + return true; + } + + /** + * Returns whether the given column in the given table exists. + * @param template The JDBC template to use. + * @param column The column in question. + * @param table The table in question. + * @return Whether the column exists. + */ + protected boolean columnExists(JdbcTemplate template, String column, String table) { + try { + template.execute("select " + column + " from " + table + " where 1 = 0"); + } catch (Exception x) { + return false; + } + return true; + } +} diff --git a/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/dao/schema/Schema10.java b/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/dao/schema/Schema10.java new file mode 100644 index 00000000..29c4c492 --- /dev/null +++ b/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/dao/schema/Schema10.java @@ -0,0 +1,100 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.backend.dao.schema; + +import org.apache.log4j.Logger; +import org.springframework.jdbc.core.JdbcTemplate; + +/** + * Used for creating and evolving the database schema. + * This class implementes the database schema for Subsonic Backend version 1.0. + * + * @author Sindre Mehus + */ +public class Schema10 extends Schema { + private static final Logger LOG = Logger.getLogger(Schema10.class); + + public void execute(JdbcTemplate template) { + + /* + Example row 1: + + id: 123 + license_holder: sindre@activeobjects.no + server_id: 972342834928656 + redirect_from: sindre + redirect_to: http://23.45.123.56:8080/subsonic + local_redirect_to: http://192.168.0.7:80/subsonic + trial: false + trial_expires: null + + Example row 2: + + id: 124 + license_holder: null + server_id: 72121983567129 + redirect_from: joe + redirect_to: http://232.21.18.14/subsonic + local_redirect_to: http://192.168.0.7:80/subsonic + trial: true + trial_expires: 2010-01-13 05:34:17 + */ + + if (!tableExists(template, "redirection")) { + LOG.info("Database table 'redirection' not found. Creating it."); + template.execute("create cached table redirection (" + + "id identity," + + "license_holder varchar," + + "server_id varchar not null," + + "redirect_from varchar not null," + + "redirect_to varchar not null," + + "trial boolean not null," + + "trial_expires datetime," + + "last_updated datetime," + + "last_read datetime," + + "unique(redirect_from))"); + template.execute("create index idx_redirection_redirect_from on redirection(redirect_from)"); + template.execute("create index idx_redirection_server_id on redirection(server_id)"); + + createRedirection(template, "demo", "http://subsonic.org/demo"); + + LOG.info("Database table 'redirection' was created successfully."); + } + + if (!columnExists(template, "local_redirect_to", "redirection")) { + LOG.info("Database column 'redirection.local_redirect_to' not found. Creating it."); + template.execute("alter table redirection " + + "add local_redirect_to varchar"); + LOG.info("Database column 'redirection.local_redirect_to' was added successfully."); + } + + if (!columnExists(template, "read_count", "redirection")) { + LOG.info("Database column 'redirection.read_count' not found. Creating it."); + template.execute("alter table redirection " + + "add read_count int default 0 not null"); + LOG.info("Database column 'redirection.read_count' was added successfully."); + } + } + + private void createRedirection(JdbcTemplate template, String redirectFrom, String redirectTo) { + template.update("insert into redirection values (null, 'sindre@activeobjects.no', '-1', ?, ?, false, null, null, null)", + new Object[] {redirectFrom, redirectTo}); + LOG.info("Creating redirection from " + redirectFrom + " to " + redirectTo); + } +} diff --git a/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/dao/schema/Schema20.java b/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/dao/schema/Schema20.java new file mode 100644 index 00000000..99ea679e --- /dev/null +++ b/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/dao/schema/Schema20.java @@ -0,0 +1,90 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.backend.dao.schema; + +import org.apache.log4j.Logger; +import org.springframework.jdbc.core.JdbcTemplate; + +/** + * Used for creating and evolving the database schema. + * This class implementes the database schema for Subsonic Backend version 2.0. + * + * @author Sindre Mehus + */ +public class Schema20 extends Schema { + private static final Logger LOG = Logger.getLogger(Schema20.class); + + public void execute(JdbcTemplate template) { + + if (!tableExists(template, "payment")) { + LOG.info("Database table 'payment' not found. Creating it."); + template.execute("create cached table payment (" + + "id identity," + + "transaction_id varchar not null," + + "transaction_type varchar," + // cart, web_accept + "item varchar," + + "payment_type varchar," + // echeck, instant + "payment_status varchar," + // Completed, Pending, Denied, Failed, ... + "payment_amount int," + + "payment_currency varchar," + + "payer_email varchar," + + "payer_first_name varchar," + + "payer_last_name varchar," + + "payer_country varchar," + + "processing_status varchar not null," + + "created datetime," + + "last_updated datetime," + + "unique(transaction_id))"); + template.execute("create index idx_payment_transaction_id on payment(transaction_id)"); + template.execute("create index idx_payment_created on payment(created)"); + template.execute("create index idx_payment_payer_email on payment(payer_email)"); + + LOG.info("Database table 'payment' was created successfully."); + } + + if (!columnExists(template, "payer_email_lower", "payment")) { + LOG.info("Database column 'payment.payer_email_lower' not found. Creating it."); + template.execute("alter table payment " + + "add payer_email_lower varchar"); + template.execute("update payment set payer_email_lower=lcase(payer_email)"); + template.execute("create index idx_payment_payer_email_lower on payment(payer_email_lower)"); + LOG.info("Database column 'payment.payer_email_lower' was added successfully."); + } + + if (!tableExists(template, "whitelist")) { + LOG.info("Database table 'whitelist' not found. Creating it."); + template.execute("create cached table whitelist (" + + "id identity," + + "email varchar not null)"); + template.execute("create index idx_whitelist_email on whitelist(email)"); + + LOG.info("Database table 'whitelist' was created successfully."); + } + + if (!tableExists(template, "blacklist")) { + LOG.info("Database table 'blacklist' not found. Creating it."); + template.execute("create cached table blacklist (" + + "id identity," + + "email varchar not null)"); + template.execute("create index idx_blacklist_email on blacklist(email)"); + + LOG.info("Database table 'blacklist' was created successfully."); + } + } +}
\ No newline at end of file diff --git a/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/domain/Payment.java b/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/domain/Payment.java new file mode 100644 index 00000000..9e32b2cd --- /dev/null +++ b/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/domain/Payment.java @@ -0,0 +1,200 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2010 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.backend.domain; + +import java.util.Date; + +/** + * @author Sindre Mehus + */ +public class Payment { + + private String id; + private String transactionId; + private String transactionType; + private String item; + private String paymentType; + private String paymentStatus; + private int paymentAmount; + private String paymentCurrency; + private String payerEmail; + private String payerFirstName; + private String payerLastName; + private String payerCountry; + private ProcessingStatus processingStatus; + private Date created; + private Date lastUpdated; + + public Payment(String id, String transactionId, String transactionType, String item, String paymentType, + String paymentStatus, int paymentAmount, String paymentCurrency, String payerEmail, + String payerFirstName, String payerLastName, String payerCountry, ProcessingStatus processingStatus, + Date created, Date lastUpdated) { + this.id = id; + this.transactionId = transactionId; + this.transactionType = transactionType; + this.item = item; + this.paymentType = paymentType; + this.paymentStatus = paymentStatus; + this.paymentAmount = paymentAmount; + this.paymentCurrency = paymentCurrency; + this.payerEmail = payerEmail; + this.payerFirstName = payerFirstName; + this.payerLastName = payerLastName; + this.payerCountry = payerCountry; + this.processingStatus = processingStatus; + this.created = created; + this.lastUpdated = lastUpdated; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getTransactionId() { + return transactionId; + } + + public void setTransactionId(String transactionId) { + this.transactionId = transactionId; + } + + public String getTransactionType() { + return transactionType; + } + + public void setTransactionType(String transactionType) { + this.transactionType = transactionType; + } + + public String getItem() { + return item; + } + + public void setItem(String item) { + this.item = item; + } + + public String getPaymentType() { + return paymentType; + } + + public void setPaymentType(String paymentType) { + this.paymentType = paymentType; + } + + public String getPaymentStatus() { + return paymentStatus; + } + + public void setPaymentStatus(String paymentStatus) { + this.paymentStatus = paymentStatus; + } + + public int getPaymentAmount() { + return paymentAmount; + } + + public void setPaymentAmount(int paymentAmount) { + this.paymentAmount = paymentAmount; + } + + public String getPaymentCurrency() { + return paymentCurrency; + } + + public void setPaymentCurrency(String paymentCurrency) { + this.paymentCurrency = paymentCurrency; + } + + public String getPayerEmail() { + return payerEmail; + } + + public void setPayerEmail(String payerEmail) { + this.payerEmail = payerEmail; + } + + public String getPayerFirstName() { + return payerFirstName; + } + + public void setPayerFirstName(String payerFirstName) { + this.payerFirstName = payerFirstName; + } + + public String getPayerLastName() { + return payerLastName; + } + + public void setPayerLastName(String payerLastName) { + this.payerLastName = payerLastName; + } + + public String getPayerCountry() { + return payerCountry; + } + + public void setPayerCountry(String payerCountry) { + this.payerCountry = payerCountry; + } + + public ProcessingStatus getProcessingStatus() { + return processingStatus; + } + + public void setProcessingStatus(ProcessingStatus processingStatus) { + this.processingStatus = processingStatus; + } + + public Date getCreated() { + return created; + } + + public void setCreated(Date created) { + this.created = created; + } + + public Date getLastUpdated() { + return lastUpdated; + } + + public void setLastUpdated(Date lastUpdated) { + this.lastUpdated = lastUpdated; + } + + @Override + public String toString() { + return "Payment{" + + "tx='" + transactionId + '\'' + + ", type='" + paymentType + '\'' + + ", status='" + paymentStatus + '\'' + + ", amount=" + paymentAmount + + ", email='" + payerEmail + '\'' + + '}'; + } + + public enum ProcessingStatus { + NEW, + COMPLETED + } +} diff --git a/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/domain/Redirection.java b/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/domain/Redirection.java new file mode 100644 index 00000000..aa62bd75 --- /dev/null +++ b/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/domain/Redirection.java @@ -0,0 +1,155 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.backend.domain; + +import java.util.Date; + +/** + * @author Sindre Mehus + */ +public class Redirection { + + private int id; + private String licenseHolder; + private String serverId; + private String redirectFrom; + private String redirectTo; + private String localRedirectTo; + private boolean trial; + private Date trialExpires; + private Date lastUpdated; + private Date lastRead; + private int readCount; + + public Redirection(int id, String licenseHolder, String serverId, String redirectFrom, String redirectTo, + String localRedirectTo, boolean trial, Date trialExpires, Date lastUpdated, Date lastRead, int readCount) { + this.id = id; + this.licenseHolder = licenseHolder; + this.serverId = serverId; + this.redirectFrom = redirectFrom; + this.redirectTo = redirectTo; + this.localRedirectTo = localRedirectTo; + this.trial = trial; + this.trialExpires = trialExpires; + this.lastUpdated = lastUpdated; + this.lastRead = lastRead; + this.readCount = readCount; + } + + public int getId() { + return id; + } + + public String getLicenseHolder() { + return licenseHolder; + } + + public void setLicenseHolder(String licenseHolder) { + this.licenseHolder = licenseHolder; + } + + public String getServerId() { + return serverId; + } + + public void setServerId(String serverId) { + this.serverId = serverId; + } + + public String getRedirectFrom() { + return redirectFrom; + } + + public void setRedirectFrom(String redirectFrom) { + this.redirectFrom = redirectFrom; + } + + public String getRedirectTo() { + return redirectTo; + } + + public void setRedirectTo(String redirectTo) { + this.redirectTo = redirectTo; + } + + public String getLocalRedirectTo() { + return localRedirectTo; + } + + public void setLocalRedirectTo(String localRedirectTo) { + this.localRedirectTo = localRedirectTo; + } + + public boolean isTrial() { + return trial; + } + + public void setTrial(boolean trial) { + this.trial = trial; + } + + public Date getTrialExpires() { + return trialExpires; + } + + public void setTrialExpires(Date trialExpires) { + this.trialExpires = trialExpires; + } + + public Date getLastUpdated() { + return lastUpdated; + } + + public void setLastUpdated(Date lastUpdated) { + this.lastUpdated = lastUpdated; + } + + public Date getLastRead() { + return lastRead; + } + + public void setLastRead(Date lastRead) { + this.lastRead = lastRead; + } + + public int getReadCount() { + return readCount; + } + + public void setReadCount(int readCount) { + this.readCount = readCount; + } + + @Override + public String toString() { + return "Redirection{" + + "id=" + id + + ", licenseHolder='" + licenseHolder + '\'' + + ", serverId='" + serverId + '\'' + + ", redirectFrom='" + redirectFrom + '\'' + + ", redirectTo='" + redirectTo + '\'' + + ", localRedirectTo='" + localRedirectTo + '\'' + + ", trial=" + trial + + ", trialExpires=" + trialExpires + + ", lastUpdated=" + lastUpdated + + ", lastRead=" + lastRead + + ", readCount=" + readCount + + '}'; + } +} diff --git a/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/service/EmailSession.java b/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/service/EmailSession.java new file mode 100644 index 00000000..cfd96651 --- /dev/null +++ b/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/service/EmailSession.java @@ -0,0 +1,108 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2010 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.backend.service; + +import net.sourceforge.subsonic.backend.Util; + +import javax.mail.Folder; +import javax.mail.Session; +import javax.mail.Message; +import javax.mail.Address; +import javax.mail.MessagingException; +import javax.mail.Store; +import javax.mail.Transport; +import javax.mail.internet.InternetAddress; +import javax.mail.internet.MimeMessage; +import javax.mail.internet.AddressException; +import java.util.Properties; +import java.util.List; + +/** + * @author Sindre Mehus + */ +public class EmailSession { + + private static final String SMTP_MAIL_SERVER = "smtp.gmail.com"; + private static final String POP_MAIL_SERVER = "pop.gmail.com"; + private static final String IMAP_MAIL_SERVER = "imap.gmail.com"; + private static final String USER = "subsonic@activeobjects.no"; + + private Session session; + private String password; + + public EmailSession() throws Exception { + Properties props = new Properties(); +// props.setProperty("mail.debug", "true"); + props.setProperty("mail.store.protocol", "pop3s"); + props.setProperty("mail.smtps.host", SMTP_MAIL_SERVER); + props.setProperty("mail.smtps.auth", "true"); + props.setProperty("mail.smtps.timeout", "10000"); + props.setProperty("mail.smtps.connectiontimeout", "10000"); + props.setProperty("mail.pop3s.timeout", "10000"); + props.setProperty("mail.pop3s.connectiontimeout", "10000"); + + session = Session.getDefaultInstance(props, null); + password = Util.getPassword("gmailpwd.txt"); + } + + public void sendMessage(String from, List<String> to, List<String> cc, List<String> bcc, List<String> replyTo, + String subject, String text) throws MessagingException { + Message message = new MimeMessage(session); + + message.setFrom(new InternetAddress(from)); + message.setReplyTo(new Address[]{new InternetAddress(from)}); + message.setRecipients(Message.RecipientType.TO, convertAddress(to)); + message.setRecipients(Message.RecipientType.CC, convertAddress(cc)); + message.setRecipients(Message.RecipientType.BCC, convertAddress(bcc)); + message.setReplyTo(convertAddress(replyTo)); + message.setSubject(subject); + message.setText(text); + + // Send the message + Transport transport = null; + try { + transport = session.getTransport("smtps"); + transport.connect(USER, password); + transport.sendMessage(message, message.getAllRecipients()); + } finally { + if (transport != null) { + transport.close(); + } + } + } + + public Folder getFolder(String name) throws Exception { + Store store = session.getStore("imaps"); + store.connect(IMAP_MAIL_SERVER, USER, password); + Folder folder = store.getFolder(name); + folder.open(Folder.READ_ONLY); + return folder; + } + + private Address[] convertAddress(List<String> addresses) throws AddressException { + if (addresses == null) { + return null; + } + Address[] result = new Address[addresses.size()]; + for (int i = 0; i < addresses.size(); i++) { + result[i] = new InternetAddress(addresses.get(i)); + } + return result; + } +} diff --git a/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/service/LicenseGenerator.java b/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/service/LicenseGenerator.java new file mode 100644 index 00000000..77376a8e --- /dev/null +++ b/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/service/LicenseGenerator.java @@ -0,0 +1,170 @@ +package net.sourceforge.subsonic.backend.service; + +import net.sourceforge.subsonic.backend.dao.PaymentDao; +import net.sourceforge.subsonic.backend.domain.Payment; +import org.apache.commons.codec.binary.Hex; +import org.apache.log4j.Logger; + +import javax.mail.MessagingException; +import java.security.MessageDigest; +import java.util.Arrays; +import java.util.Date; +import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +/** + * Runs a task at regular intervals, checking for incoming donations and sending + * out license keys by email. + * + * @author Sindre Mehus + * @version $Id$ + */ +public class LicenseGenerator { + + private static final Logger LOG = Logger.getLogger(LicenseGenerator.class); + private static final long DELAY = 60; // One minute. + + private PaymentDao paymentDao; + private final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); + + public void init() { + Runnable task = new Runnable() { + public void run() { + try { + LOG.info("Starting license generator."); + processPayments(); + LOG.info("Completed license generator."); + } catch (Throwable x) { + LOG.error("Failed to process license emails.", x); + } + } + }; + executor.scheduleWithFixedDelay(task, DELAY, DELAY, TimeUnit.SECONDS); + LOG.info("Scheduled license generator to run every " + DELAY + " seconds."); + } + + private void processPayments() throws Exception { + List<Payment> payments = paymentDao.getPaymentsByProcessingStatus(Payment.ProcessingStatus.NEW); + LOG.info(payments.size() + " new payment(s)."); + if (payments.isEmpty()) { + return; + } + + EmailSession emailSession = new EmailSession(); + for (Payment payment : payments) { + processPayment(payment, emailSession); + } + } + + private void processPayment(Payment payment, EmailSession emailSession) { + try { + LOG.info("Processing " + payment); + String email = payment.getPayerEmail(); + if (email == null) { + throw new Exception("Missing email address."); + } + + boolean eligible = isEligible(payment); + boolean ignorable = isIgnorable(payment); + if (eligible) { + sendLicenseTo(email, emailSession); + LOG.info("Sent license key for " + payment); + } else { + LOG.info("Payment not eligible for " + payment); + } + + if (eligible || ignorable) { + payment.setProcessingStatus(Payment.ProcessingStatus.COMPLETED); + payment.setLastUpdated(new Date()); + paymentDao.updatePayment(payment); + } + + } catch (Throwable x) { + LOG.error("Failed to process " + payment, x); + } + } + + private boolean isEligible(Payment payment) { + String status = payment.getPaymentStatus(); + if ("echeck".equalsIgnoreCase(payment.getPaymentType())) { + return "Pending".equalsIgnoreCase(status) || "Completed".equalsIgnoreCase(status); + } + return "Completed".equalsIgnoreCase(status); + } + + private boolean isIgnorable(Payment payment) { + String status = payment.getPaymentStatus(); + return "Denied".equalsIgnoreCase(status) || + "Reversed".equalsIgnoreCase(status) || + "Refunded".equalsIgnoreCase(status); + } + + public void sendLicenseTo(String to, EmailSession emailSession) throws MessagingException { + emailSession.sendMessage("subsonic_donation@activeobjects.no", + Arrays.asList(to), + null, + Arrays.asList("subsonic_donation@activeobjects.no", "sindre@activeobjects.no"), + Arrays.asList("subsonic_donation@activeobjects.no"), + "Subsonic License", + createLicenseContent(to)); + LOG.info("Sent license to " + to); + } + + private String createLicenseContent(String to) { + String license = md5Hex(to.toLowerCase()); + + return "Dear Subsonic donor,\n" + + "\n" + + "Many thanks for your kind donation to Subsonic!\n" + + "Please find your license key below.\n" + + "\n" + + "Email: " + to + "\n" + + "License: " + license + " \n" + + "\n" + + "To install the license key, click the \"Donate\" link in the top right corner of the Subsonic web interface.\n" + + "\n" + + "More info here: http://subsonic.org/pages/getting-started.jsp#3\n" + + "\n" + + "This license is valid for personal, non-commercial of Subsonic. For commercial use, please contact us for licensing options.\n" + + "\n" + + "Thanks again for supporting the project!\n" + + "\n" + + "Best regards,\n" + + "The Subsonic team"; + } + + /** + * Calculates the MD5 digest and returns the value as a 32 character hex string. + * + * @param s Data to digest. + * @return MD5 digest as a hex string. + */ + private String md5Hex(String s) { + if (s == null) { + return null; + } + + try { + MessageDigest md5 = MessageDigest.getInstance("MD5"); + return new String(Hex.encodeHex(md5.digest(s.getBytes("UTF-8")))); + } catch (Exception x) { + throw new RuntimeException(x.getMessage(), x); + } + } + + public void setPaymentDao(PaymentDao paymentDao) { + this.paymentDao = paymentDao; + } + + public static void main(String[] args) throws Exception { + String address = args[0]; +// String license = md5Hex(address.toLowerCase()); +// System.out.println("Email: " + address); +// System.out.println("License: " + license); + + LicenseGenerator generator = new LicenseGenerator(); + generator.sendLicenseTo(address, new EmailSession()); + } +} diff --git a/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/service/WhitelistGenerator.java b/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/service/WhitelistGenerator.java new file mode 100644 index 00000000..eef29611 --- /dev/null +++ b/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/service/WhitelistGenerator.java @@ -0,0 +1,53 @@ +package net.sourceforge.subsonic.backend.service; + +import net.sourceforge.subsonic.backend.dao.PaymentDao; +import org.apache.log4j.Logger; + +import javax.mail.Address; +import javax.mail.Folder; +import javax.mail.Message; +import javax.mail.internet.InternetAddress; +import java.util.Date; + +/** + * Creates a license whitelist. + * + * @author Sindre Mehus + * @version $Id$ + */ +public class WhitelistGenerator { + + private static final Logger LOG = Logger.getLogger(WhitelistGenerator.class); + + private PaymentDao paymentDao; + + public void generate(Date newerThan) throws Exception { + LOG.info("Starting whitelist update for emails newer than " + newerThan); + + EmailSession session = new EmailSession(); + Folder folder = session.getFolder("[Gmail]/Sent Mail"); + int n = folder.getMessageCount(); + + for (int i = n; i >= 0; i--) { + Message message = folder.getMessage(i); + Date date = message.getSentDate(); + InternetAddress address = (InternetAddress) message.getRecipients(Message.RecipientType.TO)[0]; + String recipient = address.getAddress(); + if (date.before(newerThan)) { + break; + } + LOG.info(date + " " + recipient); + + if (paymentDao.getPaymentByEmail(recipient) == null && !paymentDao.isWhitelisted(recipient)) { + paymentDao.whitelist(recipient); + LOG.info("WHITELISTED " + recipient); + } + } + folder.close(false); + LOG.info("Completed whitelist update."); + } + + public void setPaymentDao(PaymentDao paymentDao) { + this.paymentDao = paymentDao; + } +} diff --git a/subsonic-backend/src/main/resources/log4j.properties b/subsonic-backend/src/main/resources/log4j.properties new file mode 100644 index 00000000..a49f34e5 --- /dev/null +++ b/subsonic-backend/src/main/resources/log4j.properties @@ -0,0 +1,19 @@ +log4j.rootLogger=INFO, file
+
+log4j.appender.file=org.apache.log4j.RollingFileAppender
+log4j.appender.file.file=/var/subsonic-backend/subsonic-backend.log
+log4j.appender.file.maxBackupIndex=9
+log4j.appender.file.layout=org.apache.log4j.PatternLayout
+log4j.appender.file.layout.ConversionPattern=[%d{ISO8601}] %-5p %c - %m%n
+
+log4j.appender.ipn=org.apache.log4j.RollingFileAppender
+log4j.appender.ipn.file=/var/subsonic-backend/ipn.log
+log4j.appender.ipn.maxBackupIndex=9
+log4j.appender.ipn.layout=org.apache.log4j.PatternLayout
+log4j.appender.ipn.layout.ConversionPattern=[%d{ISO8601}] %-5p %c - %m%n
+
+log4j.logger.net.sourceforge.subsonic.backend.controller.IPNController=INFO, ipn
+log4j.logger.net.sourceforge.subsonic.backend.service.LicenseGenerator=INFO, ipn
+log4j.logger.net.sourceforge.subsonic.backend.dao.PaymentDao=INFO, ipn
+
+log4j.logger.org.springframework=INFO
\ No newline at end of file diff --git a/subsonic-backend/src/main/webapp/WEB-INF/applicationContext-backend.xml b/subsonic-backend/src/main/webapp/WEB-INF/applicationContext-backend.xml new file mode 100644 index 00000000..f1008a29 --- /dev/null +++ b/subsonic-backend/src/main/webapp/WEB-INF/applicationContext-backend.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="ISO-8859-1"?>
+
+<beans xmlns="http://www.springframework.org/schema/beans"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">
+
+ <bean id="redirectionDao" class="net.sourceforge.subsonic.backend.dao.RedirectionDao">
+ <property name="daoHelper" ref="daoHelper"/>
+ </bean>
+
+ <bean id="paymentDao" class="net.sourceforge.subsonic.backend.dao.PaymentDao">
+ <property name="daoHelper" ref="daoHelper"/>
+ </bean>
+
+ <bean id="daoHelper" class="net.sourceforge.subsonic.backend.dao.DaoHelper"/>
+
+ <bean id="licenseGenerator" class="net.sourceforge.subsonic.backend.service.LicenseGenerator" init-method="init">
+ <property name="paymentDao" ref="paymentDao"/>
+ </bean>
+
+ <bean id="whitelistGenerator" class="net.sourceforge.subsonic.backend.service.WhitelistGenerator">
+ <property name="paymentDao" ref="paymentDao"/>
+ </bean>
+
+</beans>
\ No newline at end of file diff --git a/subsonic-backend/src/main/webapp/WEB-INF/jsp/backend/db.jsp b/subsonic-backend/src/main/webapp/WEB-INF/jsp/backend/db.jsp new file mode 100644 index 00000000..c3e1eb39 --- /dev/null +++ b/subsonic-backend/src/main/webapp/WEB-INF/jsp/backend/db.jsp @@ -0,0 +1,51 @@ +<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="iso-8859-1"%>
+
+<html><head>
+ <%@ include file="../head.jsp" %>
+ <link rel="stylesheet" type="text/css" href="http://yui.yahooapis.com/2.8.0r4/build/reset/reset.css">
+ <link rel="stylesheet" type="text/css" href="http://yui.yahooapis.com/2.8.0r4/build/fonts/fonts.css">
+ <link rel="stylesheet" type="text/css" href="http://yui.yahooapis.com/2.8.0r4/build/grid/grid.css">
+ <link rel="stylesheet" type="text/css" href="http://yui.yahooapis.com/2.8.0r4/build/base/base.css">
+</head><body>
+
+<h1>Database query</h1>
+
+<form method="post" action="db.view">
+ <textarea rows="10" cols="80" name="query" style="margin-top:1em">${model.query}</textarea>
+ <input type="submit" value="OK">
+ <input type="hidden" name="p" value="${model.p}">
+</form>
+
+
+<c:if test="${not empty model.result}">
+ <h1 style="margin-top:2em">Result</h1>
+
+ <table>
+ <c:forEach items="${model.result}" var="row" varStatus="loopStatus">
+
+ <c:if test="${loopStatus.count == 1}">
+ <tr>
+ <c:forEach items="${row}" var="entry">
+ <td>${entry.key}</td>
+ </c:forEach>
+ </tr>
+ </c:if>
+ <tr>
+ <c:forEach items="${row}" var="entry">
+ <td>${entry.value}</td>
+ </c:forEach>
+ </tr>
+ </c:forEach>
+
+ </table>
+</c:if>
+
+<c:if test="${not empty model.error}">
+ <h1 style="margin-top:2em">Error</h1>
+
+ <p>
+ ${model.error}
+ </p>
+</c:if>
+
+</body></html>
\ No newline at end of file diff --git a/subsonic-backend/src/main/webapp/WEB-INF/jsp/backend/payment.jsp b/subsonic-backend/src/main/webapp/WEB-INF/jsp/backend/payment.jsp new file mode 100644 index 00000000..c72f798d --- /dev/null +++ b/subsonic-backend/src/main/webapp/WEB-INF/jsp/backend/payment.jsp @@ -0,0 +1,23 @@ +<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="iso-8859-1" %>
+
+<html>
+<head>
+ <%@ include file="../head.jsp" %>
+ <link rel="stylesheet" type="text/css" href="http://yui.yahooapis.com/2.8.0r4/build/reset/reset.css">
+ <link rel="stylesheet" type="text/css" href="http://yui.yahooapis.com/2.8.0r4/build/fonts/fonts.css">
+ <link rel="stylesheet" type="text/css" href="http://yui.yahooapis.com/2.8.0r4/build/grid/grid.css">
+ <link rel="stylesheet" type="text/css" href="http://yui.yahooapis.com/2.8.0r4/build/base/base.css">
+ <meta http-equiv="refresh" content="300">
+</head>
+<body>
+
+<div style="margin-left: auto; margin-right: auto;width:10em">
+ <h1 style="text-align: center;">€${model.sumToday}</h1>
+
+ <div style="white-space: nowrap; text-align:center;">
+ <span title="Sum yesterday">Y <b>€${model.sumYesterday}</b></span>
+ <span title="Daily average this month">M <b>€${model.dayAverageThisMonth}</b></span>
+ </div>
+</div>
+</body>
+</html>
\ No newline at end of file diff --git a/subsonic-backend/src/main/webapp/WEB-INF/jsp/backend/requestLicense.jsp b/subsonic-backend/src/main/webapp/WEB-INF/jsp/backend/requestLicense.jsp new file mode 100644 index 00000000..c8654663 --- /dev/null +++ b/subsonic-backend/src/main/webapp/WEB-INF/jsp/backend/requestLicense.jsp @@ -0,0 +1,42 @@ +<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="iso-8859-1" %>
+
+<html>
+<head>
+ <%@ include file="../head.jsp" %>
+ <link rel="stylesheet" type="text/css" href="http://yui.yahooapis.com/2.8.0r4/build/reset/reset.css">
+ <link rel="stylesheet" type="text/css" href="http://yui.yahooapis.com/2.8.0r4/build/fonts/fonts.css">
+ <link rel="stylesheet" type="text/css" href="http://yui.yahooapis.com/2.8.0r4/build/grid/grid.css">
+ <link rel="stylesheet" type="text/css" href="http://yui.yahooapis.com/2.8.0r4/build/base/base.css">
+</head>
+<body>
+
+<h1>Resend Subsonic license key</h1>
+
+<c:if test="${empty model.email}">
+ <p>Have you purchased a Subsonic license but lost the license key?</p>
+
+ <p>Enter your email address below to have it resent to you.</p>
+</c:if>
+
+<c:if test="${not empty model.email and not model.valid}">
+ <p>Sorry, no license key is associated to ${model.email}. Did you use a different email address when
+ creating the payment on PayPal?</p>
+</c:if>
+
+<c:choose>
+ <c:when test="${model.valid}">
+ <p>Your license key has been sent to ${model.email}. Didn't get it? Please remember to check your spam
+ folder.</p>
+ </c:when>
+ <c:otherwise>
+ <form method="post" action="requestLicense.view">
+ <label>Email address
+ <input type="text" size="30" name="email">
+ </label>
+ <input type="submit" value="Send license key">
+ </form>
+ </c:otherwise>
+</c:choose>
+
+</body>
+</html>
\ No newline at end of file diff --git a/subsonic-backend/src/main/webapp/WEB-INF/jsp/head.jsp b/subsonic-backend/src/main/webapp/WEB-INF/jsp/head.jsp new file mode 100644 index 00000000..a8b170f4 --- /dev/null +++ b/subsonic-backend/src/main/webapp/WEB-INF/jsp/head.jsp @@ -0,0 +1,3 @@ +<%@ include file="include.jsp" %>
+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+<title>Subsonic</title>
diff --git a/subsonic-backend/src/main/webapp/WEB-INF/jsp/include.jsp b/subsonic-backend/src/main/webapp/WEB-INF/jsp/include.jsp new file mode 100644 index 00000000..3758aa0a --- /dev/null +++ b/subsonic-backend/src/main/webapp/WEB-INF/jsp/include.jsp @@ -0,0 +1,7 @@ +<%@ page session="false"%>
+<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
+<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
+<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
+<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>
+<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
+<%@ taglib prefix="str" uri="http://jakarta.apache.org/taglibs/string-1.1" %>
diff --git a/subsonic-backend/src/main/webapp/WEB-INF/subsonic-backend-servlet.xml b/subsonic-backend/src/main/webapp/WEB-INF/subsonic-backend-servlet.xml new file mode 100644 index 00000000..d166d271 --- /dev/null +++ b/subsonic-backend/src/main/webapp/WEB-INF/subsonic-backend-servlet.xml @@ -0,0 +1,54 @@ +<?xml version="1.0" encoding="ISO-8859-1"?>
+
+<beans xmlns="http://www.springframework.org/schema/beans"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">
+
+ <bean id="multiController" class="net.sourceforge.subsonic.backend.controller.MultiController">
+ <property name="daoHelper" ref="daoHelper"/>
+ <property name="paymentDao" ref="paymentDao"/>
+ <property name="whitelistGenerator" ref="whitelistGenerator"/>
+ <property name="licenseGenerator" ref="licenseGenerator"/>
+ </bean>
+
+ <bean id="redirectionController" class="net.sourceforge.subsonic.backend.controller.RedirectionController">
+ <property name="redirectionDao" ref="redirectionDao"/>
+ </bean>
+
+ <bean id="ipnController" class="net.sourceforge.subsonic.backend.controller.IPNController">
+ <property name="paymentDao" ref="paymentDao"/>
+ </bean>
+
+ <bean id="redirectionManagementController" class="net.sourceforge.subsonic.backend.controller.RedirectionManagementController">
+ <property name="redirectionDao" ref="redirectionDao"/>
+ </bean>
+
+ <bean id="urlMapping" class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
+ <property name="alwaysUseFullPath" value="true"/>
+ <property name="mappings">
+ <props>
+ <prop key="/backend/version.view">multiController</prop>
+ <prop key="/backend/db.view">multiController</prop>
+ <prop key="/backend/payment.view">multiController</prop>
+ <prop key="/backend/sendMail.view">multiController</prop>
+ <prop key="/backend/requestLicense.view">multiController</prop>
+ <prop key="/backend/validateLicense.view">multiController</prop>
+ <prop key="/backend/whitelist.view">multiController</prop>
+ <prop key="/backend/ipn.view">ipnController</prop>
+ <prop key="/backend/redirect/register.view">redirectionManagementController</prop>
+ <prop key="/backend/redirect/unregister.view">redirectionManagementController</prop>
+ <prop key="/backend/redirect/get.view">redirectionManagementController</prop>
+ <prop key="/backend/redirect/test.view">redirectionManagementController</prop>
+ <prop key="/backend/redirect/dump.view">redirectionManagementController</prop>
+ <prop key="/**">redirectionController</prop>
+ </props>
+ </property>
+ </bean>
+
+ <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
+ <property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/>
+ <property name="prefix" value="/WEB-INF/jsp/"/>
+ <property name="suffix" value=".jsp"/>
+ </bean>
+
+</beans>
\ No newline at end of file diff --git a/subsonic-backend/src/main/webapp/WEB-INF/web.xml b/subsonic-backend/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000..3e69f937 --- /dev/null +++ b/subsonic-backend/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="ISO-8859-1"?>
+<web-app id="subsonic-backend" version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
+
+ <display-name>Subsonic Backend</display-name>
+
+ <!-- Location of application context. Used by ContextLoaderListener. -->
+ <context-param>
+ <param-name>contextConfigLocation</param-name>
+ <param-value>
+ /WEB-INF/applicationContext-backend.xml
+ </param-value>
+ </context-param>
+
+ <listener>
+ <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
+ </listener>
+
+ <servlet>
+ <servlet-name>subsonic-backend</servlet-name>
+ <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
+ <load-on-startup>1</load-on-startup>
+ </servlet>
+
+ <servlet-mapping>
+ <servlet-name>subsonic-backend</servlet-name>
+ <url-pattern>/</url-pattern>
+ </servlet-mapping>
+
+ <welcome-file-list>
+ <welcome-file>index.html</welcome-file>
+ <welcome-file>index.jsp</welcome-file>
+ </welcome-file-list>
+
+</web-app>
\ No newline at end of file |