/** * AbstractAPIHandler * Copyright 17.05.2016 by Michael Peter Christen, @0rb1t3r and Robert Mader, @treba123 * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program in the file lgpl21.txt * If not, see <http://www.gnu.org/licenses/>. */ package org.loklak.server; import java.io.IOException; import java.io.PrintWriter; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.time.Instant; import java.util.Base64; import java.util.Random; import javax.servlet.ServletException; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.eclipse.jetty.util.log.Log; import org.json.JSONObject; import org.loklak.data.DAO; import org.loklak.http.ClientConnection; import org.loklak.http.RemoteAccess; import org.loklak.tools.UTF8; import org.loklak.tools.storage.JSONObjectWithDefault; @SuppressWarnings("serial") public abstract class AbstractAPIHandler extends HttpServlet implements APIHandler { private String[] serverProtocolHostStub = null; public static final Long defaultCookieTime = (long) (60 * 60 * 24 * 7); public static final Long defaultAnonymousTime = (long) (60 * 60 * 24); public AbstractAPIHandler() { this.serverProtocolHostStub = null; } public AbstractAPIHandler(String[] serverProtocolHostStub) { this.serverProtocolHostStub = serverProtocolHostStub; } @Override public String[] getServerProtocolHostStub() { return this.serverProtocolHostStub; } @Override public abstract BaseUserRole getMinimalBaseUserRole(); @Override public abstract JSONObject getDefaultPermissions(BaseUserRole baseUserRole); @Override public JSONObject[] service(Query call, Authorization rights) throws APIException { // make call to the embedded api if (this.serverProtocolHostStub == null) return new JSONObject[]{serviceImpl(call, null, rights, rights.getPermissions(this))}; // make call(s) to a remote api(s) JSONObject[] results = new JSONObject[this.serverProtocolHostStub.length]; for (int rc = 0; rc < results.length; rc++) { try { StringBuilder urlquery = new StringBuilder(); for (String key: call.getKeys()) { urlquery.append(urlquery.length() == 0 ? '?' : '&').append(key).append('=').append(call.get(key, "")); } String urlstring = this.serverProtocolHostStub[rc] + this.getAPIPath() + urlquery.toString(); byte[] jsonb = ClientConnection.download(urlstring); if (jsonb == null || jsonb.length == 0) throw new IOException("empty content from " + urlstring); String jsons = UTF8.String(jsonb); JSONObject json = new JSONObject(jsons); if (json == null || json.length() == 0) { results[rc] = null; continue; }; results[rc] = json; } catch (Throwable e) { Log.getLog().warn(e); } } return results; } public abstract JSONObject serviceImpl(Query call, HttpServletResponse response, Authorization rights, final JSONObjectWithDefault permissions) throws APIException; @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { Query post = RemoteAccess.evaluate(request); process(request, response, post); } @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { Query query = RemoteAccess.evaluate(request); query.initPOST(RemoteAccess.getPostMap(request)); process(request, response, query); } private void process(HttpServletRequest request, HttpServletResponse response, Query query) throws ServletException, IOException { // basic protection BaseUserRole minimalBaseUserRole = getMinimalBaseUserRole() != null ? getMinimalBaseUserRole() : BaseUserRole.ANONYMOUS; if (query.isDoS_blackout()) {response.sendError(503, "your request frequency is too high"); return;} // DoS protection if (DAO.getConfig("users.admin.localonly", true) && minimalBaseUserRole == BaseUserRole.ADMIN && !query.isLocalhostAccess()) {response.sendError(503, "access only allowed from localhost, your request comes from " + query.getClientHost()); return;} // danger! do not remove this! // user identification ClientIdentity identity = getIdentity(request, response, query); // user authorization: we use the identification of the user to get the assigned authorization Authorization authorization = new Authorization(identity, DAO.authorization, DAO.userRoles); if(authorization.getBaseUserRole().ordinal() < minimalBaseUserRole.ordinal()){ response.sendError(401, "Base user role not sufficient. Your base user role is '" + authorization.getBaseUserRole().name() + "', your user role is '" + authorization.getUserRole().getDisplayName() + "'"); return; } // user accounting: we maintain static and persistent user data; we again search the accounts using the usder identity string //JSONObject accounting_persistent_obj = DAO.accounting_persistent.has(user_id) ? DAO.accounting_persistent.getJSONObject(anon_id) : DAO.accounting_persistent.put(user_id, new JSONObject()).getJSONObject(user_id); Accounting accounting_temporary = DAO.accounting_temporary.get(identity.toString()); if (accounting_temporary == null) { accounting_temporary = new Accounting(); DAO.accounting_temporary.put(identity.toString(), accounting_temporary); } // the accounting data is assigned to the authorization authorization.setAccounting(accounting_temporary); // extract standard query attributes String callback = query.get("callback", ""); boolean jsonp = callback.length() > 0; boolean minified = query.get("minified", false); try { JSONObject json = serviceImpl(query, response, authorization, authorization.getPermissions(this)); if (json == null) { response.sendError(400, "your request does not contain the required data"); return; } // evaluate special fields if (json.has("$EXPIRES")) { int expires = json.getInt("$EXPIRES"); FileHandler.setCaching(response, expires); json.remove("$EXPIRES"); } // add session information JSONObject session = new JSONObject(true); session.put("identity", identity.toJSON()); json.put("session", session); // write json query.setResponse(response, "application/javascript"); response.setCharacterEncoding("UTF-8"); PrintWriter sos = response.getWriter(); if (jsonp) sos.print(callback + "("); sos.print(json.toString(minified ? 0 : 2)); if (jsonp) sos.println(");"); sos.println(); query.finalize(); } catch (APIException e) { response.sendError(e.getStatusCode(), e.getMessage()); return; } } /** * Checks a request for valid login data, either a existing session, a cookie or an access token * @return user identity if some login is active, anonymous identity otherwise */ public static ClientIdentity getIdentity(HttpServletRequest request, HttpServletResponse response, Query query) { if(getLoginCookie(request) != null){ // check if login cookie is set Cookie loginCookie = getLoginCookie(request); ClientCredential credential = new ClientCredential(ClientCredential.Type.cookie, loginCookie.getValue()); Authentication authentication = new Authentication(credential, DAO.authentication); if(authentication.getIdentity() != null && authentication.checkExpireTime()) { //reset cookie validity time authentication.setExpireTime(defaultCookieTime); loginCookie.setMaxAge(defaultCookieTime.intValue()); loginCookie.setPath("/"); // bug. The path gets reset response.addCookie(loginCookie); return authentication.getIdentity(); } authentication.delete(); // delete cookie if set deleteLoginCookie(response); Log.getLog().info("Invalid login try via cookie from host: " + query.getClientHost()); } else if(request.getSession().getAttribute("identity") != null){ // check session is set return (ClientIdentity) request.getSession().getAttribute("identity"); } else if (request.getParameter("access_token") != null){ // access tokens can be used by api calls, somehow the stateless equivalent of sessions for browsers ClientCredential credential = new ClientCredential(ClientCredential.Type.access_token, request.getParameter("access_token")); Authentication authentication = new Authentication(credential, DAO.authentication); // check if access_token is valid if(authentication.getIdentity() != null){ ClientIdentity identity = authentication.getIdentity(); if(authentication.checkExpireTime()){ Log.getLog().info("login for user: " + identity.getName() + " via access token from host: " + query.getClientHost()); if("true".equals(request.getParameter("request_session"))){ request.getSession().setAttribute("identity",identity); } if(authentication.has("one_time") && authentication.getBoolean("one_time")){ authentication.delete(); } return identity; } } Log.getLog().info("Invalid access token from host: " + query.getClientHost()); authentication.delete(); return getAnonymousIdentity(query.getClientHost()); } return getAnonymousIdentity(query.getClientHost()); } /** * Create or fetch an anonymous identity * @return the anonymous ClientIdentity */ private static ClientIdentity getAnonymousIdentity(String remoteHost) { ClientCredential credential = new ClientCredential(ClientCredential.Type.host, remoteHost); Authentication authentication = new Authentication(credential, DAO.authentication); if (authentication.getIdentity() == null) authentication.setIdentity(new ClientIdentity(credential.toString())); authentication.setExpireTime(Instant.now().getEpochSecond() + defaultAnonymousTime); return authentication.getIdentity(); } /** * Create a hash for an input an salt * @param input * @param salt * @return String hash */ public static String getHash(String input, String salt){ try { MessageDigest md = MessageDigest.getInstance("SHA-256"); md.update((salt + input).getBytes()); return Base64.getEncoder().encodeToString(md.digest()); } catch (NoSuchAlgorithmException e) { Log.getLog().warn(e); } return null; } /** * Creates a random alphanumeric string * @param length * @return */ public static String createRandomString(Integer length){ char[] chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".toCharArray(); StringBuilder sb = new StringBuilder(); Random random = new Random(); for (int i = 0; i < length; i++) { char c = chars[random.nextInt(chars.length)]; sb.append(c); } return sb.toString(); } /** * Returns a login cookie if present in the request * @param request * @return the login cookie if present, null otherwise */ private static Cookie getLoginCookie(HttpServletRequest request) { if (request.getCookies() != null){ for (Cookie cookie: request.getCookies()){ if ("login".equals(cookie.getName())){ return cookie; } } } return null; } /** * Delete the login cookie if present * @param response */ protected static void deleteLoginCookie(HttpServletResponse response){ Cookie deleteCookie = new Cookie("login", null); deleteCookie.setPath("/"); deleteCookie.setMaxAge(0); response.addCookie(deleteCookie); } }