/**************************************************************************** * Copyright (C) 2013-2015 HS Coburg. * All rights reserved. * Contact: ecsec GmbH (info@ecsec.de) * * This file is part of the Open eCard App. * * GNU General Public License Usage * This file may be used under the terms of the GNU General Public * License version 3.0 as published by the Free Software Foundation * and appearing in the file LICENSE.GPL included in the packaging of * this file. Please review the following information to ensure the * GNU General Public License version 3.0 requirements will be met: * http://www.gnu.org/copyleft/gpl.html. * * Other Usage * Alternatively, this file may be used in accordance with the terms * and conditions contained in a signed written agreement between * you and ecsec GmbH. * ***************************************************************************/ package org.openecard.binding.tctoken; import org.openecard.binding.tctoken.ex.ActivationError; import org.openecard.binding.tctoken.ex.FatalActivationError; import java.util.List; import java.util.Map; import java.util.concurrent.Semaphore; import org.openecard.addon.AddonManager; import org.openecard.addon.AddonNotFoundException; import org.openecard.addon.Context; import org.openecard.addon.bind.AppExtensionAction; import org.openecard.addon.bind.AppPluginAction; import org.openecard.addon.bind.Attachment; import org.openecard.addon.bind.BindingResult; import org.openecard.addon.bind.BindingResultCode; import org.openecard.addon.bind.RequestBody; import org.openecard.addon.manifest.AddonSpecification; import org.openecard.binding.tctoken.ex.NonGuiException; import org.openecard.common.ECardConstants; import org.openecard.common.I18n; import org.openecard.common.OpenecardProperties; import org.openecard.gui.UserConsent; import org.openecard.gui.message.DialogType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import static org.openecard.binding.tctoken.ex.ErrorTranslations.*; import org.openecard.gui.definition.ViewController; import org.openecard.common.DynamicContext; import org.openecard.transport.httpcore.cookies.CookieManager; import org.openecard.common.interfaces.Dispatcher; /** * Implementation of a plugin action performing a client activation with a TCToken. * * @author Dirk Petrautzki * @author Benedikt Biallowons * @author Tobias Wich * @author Hans-Martin Haase */ public class ActivationAction implements AppPluginAction { private static final Logger logger = LoggerFactory.getLogger(ActivationAction.class); private static final Semaphore SEMAPHORE = new Semaphore(1); private final I18n lang = I18n.getTranslation("tr03112"); private TCTokenHandler tokenHandler; private AppPluginAction statusAction; private AppExtensionAction pinManAction; private UserConsent gui; private AddonManager manager; private ViewController settingsAndDefaultView; private Dispatcher dispatcher; private Context ctx; @Override public void init(Context ctx) { tokenHandler = new TCTokenHandler(ctx); this.ctx = ctx; gui = ctx.getUserConsent(); dispatcher = ctx.getDispatcher(); manager = ctx.getManager(); settingsAndDefaultView = ctx.getViewController(); try { AddonSpecification addonSpecStatus = manager.getRegistry().search("Status"); statusAction = manager.getAppPluginAction(addonSpecStatus, "getStatus"); AddonSpecification addonSpecPinMngmt = manager.getRegistry().search("PIN-Plugin"); pinManAction = manager.getAppExtensionAction(addonSpecPinMngmt, "GetCardsAndPINStatusAction"); } catch (AddonNotFoundException ex) { // this should never happen because the status and pin plugin are always available String msg = "Failed to get Status or PIN Plugin."; logger.error(msg, ex); throw new RuntimeException(msg, ex); } } @Override public void destroy() { tokenHandler = null; } @Override public BindingResult execute(RequestBody body, Map<String, String> params, List<Attachment> attachments) { BindingResult response; try { if (SEMAPHORE.tryAcquire()) { response = checkRequestParameters(body, params, attachments); } else { response = new BindingResult(BindingResultCode.RESOURCE_LOCKED); response.setResultMessage("An authentication process is already running."); } } finally { SEMAPHORE.release(); // in some cases an error does not lead to a removal of the dynamic context so remove it here DynamicContext.remove(); } return response; } private boolean isShowRemoveCard() { String str = OpenecardProperties.getProperty("notification.omit_show_remove_card"); return ! Boolean.valueOf(str); } /** * Use the {@link UserConsent} to display the success message. */ private void showFinishMessage(TCTokenResponse response) { // show the finish message just if we have a major ok if (ECardConstants.Major.OK.equals(response.getResult().getResultMajor()) && isShowRemoveCard()) { String title = lang.translationForKey(FINISH_TITLE); String msg = lang.translationForKey(REMOVE_CARD); showBackgroundMessage(msg, title, DialogType.INFORMATION_MESSAGE); } } /** * Display a dialog in a separate thread. * * @param msg The message which shall be displayed. * @param title Title of the dialog window. * @param dialogType Type of the dialog. */ private void showBackgroundMessage(final String msg, final String title, final DialogType dialogType) { new Thread(new Runnable() { @Override public void run() { gui.obtainMessageDialog().showMessageDialog(msg, title, dialogType); } }, "Background_MsgBox").start(); } /** * Use the {@link UserConsent} to display the given error message. * * @param errMsg Error message to display. */ private void showErrorMessage(String errMsg) { String title = lang.translationForKey(ERROR_TITLE); String baseHeader = lang.translationForKey(ERROR_HEADER); String exceptionPart = lang.translationForKey(ERROR_MSG_IND); String removeCard = lang.translationForKey(REMOVE_CARD); String msg = String.format("%s\n\n%s\n%s\n\n%s", baseHeader, exceptionPart, errMsg, removeCard); showBackgroundMessage(msg, title, DialogType.ERROR_MESSAGE); } /** * Check the request for correct parameters and invoke their processing if they are ok. * * @param body The body of the request. * @param params The query parameters and their values. * @param attachments Attachments of the request. * @return A {@link BindingResult} with an error if the parameters are not correct or one depending on the processing * of the parameters. */ private BindingResult checkRequestParameters(RequestBody body, Map<String, String> params, List<Attachment> attachments) { BindingResult response; boolean emptyParms, tokenUrl, activationObject, status, showUI; emptyParms = tokenUrl = activationObject = status = showUI = false; if (params.isEmpty()) { emptyParms = true; } if (params.containsKey("tcTokenURL")) { tokenUrl = true; } if (params.containsKey("activationObject")) { activationObject = true; } if (params.containsKey("Status")) { status = true; } if (params.containsKey("ShowUI")) { showUI = true; } // only continue, when there are known parameters in the request if (emptyParms || !(tokenUrl || activationObject || status || showUI)) { response = new BindingResult(BindingResultCode.MISSING_PARAMETER); response.setResultMessage(lang.translationForKey(NO_ACTIVATION_PARAMETERS)); showErrorMessage(lang.translationForKey(NO_ACTIVATION_PARAMETERS)); return response; } // check illegal parameter combination if ((tokenUrl && activationObject) || (tokenUrl && showUI) || (tokenUrl && status) || (activationObject && showUI) || (activationObject && status) || (showUI && status)) { response = new BindingResult(BindingResultCode.WRONG_PARAMETER); response.setResultMessage(lang.translationForKey(NO_PARAMS)); showErrorMessage(lang.translationForKey(NO_PARAMS)); return response; } return processRequest(body, params, attachments, tokenUrl, activationObject, showUI, status); } /** * Process the request. * * @param body Body of the request. * @param params Query parameters of the request. * @param attachments Attachments of the request. * @param tokenUrl {@code TRUE} if {@code params} contains a TCTokenURL. * @param activationObject {@code TRUE} if {@code params} contains an activationObject. * @param showUI {@code TRUE} if {@code params} contains a ShowUI parameter. * @param status {@code TRUE} if {@code params} contains a Status parameter. * @return A {@link BindingResult} representing the result of the request processing. */ private BindingResult processRequest(RequestBody body, Map<String, String> params, List<Attachment> attachments, boolean tokenUrl, boolean activationObject, boolean showUI, boolean status) { BindingResult response = null; if (tokenUrl || activationObject) { response = processTcTokenOrActivationObject(params); return response; } if (status) { response = processStatus(body, params, attachments); return response; } if (showUI) { String requestedUI = params.get("ShowUI"); response = processShowUI(requestedUI); return response; } return response; } /** * Open the requested UI if no supported UI element is stated the default view is opened. * * @param requestedUI String containing the name of the UI component to open. Currently supported UI components are * {@code Settings} and {@code PINManagement}. All other values are ignored and the default view is opened also if * the value is null. * @return A {@link BindingResult} object containing {@link BindingResultCode#OK} because the UIs do not return * results. */ private BindingResult processShowUI(String requestedUI) { BindingResult response; if (requestedUI != null) { switch (requestedUI) { case "Settings": response = processShowSettings(); break; case "PINManagement": response = processShowPinManagement(); break; default: response = processShowDefault(); } } else { // open default gui response = processShowDefault(); } return response; } /** * Display the default view of the Open eCard App. * * There is no real default view that's a term used by the eID-Client specification BSI-TR-03124-1 v1.2 so we display * the About dialog. * * @return A {@link BindingResult} object containing {@link BindingResultCode#OK} because the gui does not return any * result. */ private BindingResult processShowDefault() { Thread defautlViewThread = new Thread(new Runnable() { @Override public void run() { settingsAndDefaultView.showDefaultViewUI(); } }, "ShowDefaultView"); defautlViewThread.start(); return new BindingResult(BindingResultCode.OK); } /** * Opens the PINManagement dialog. * * @return A {@link BindingResult} object containing {@link BindingResultCode#OK} because the gui does not return any * result. */ private BindingResult processShowPinManagement() { Thread pinManThread = new Thread(new Runnable() { @Override public void run() { pinManAction.execute(); } }, "ShowPINManagement"); pinManThread.start(); return new BindingResult(BindingResultCode.OK); } /** * Opens the Settings dialog. * * @return A {@link BindingResult} object containing {@link BindingResultCode#OK} because the gui does not return any * result. */ private BindingResult processShowSettings() { Thread settingsThread = new Thread(new Runnable() { @Override public void run() { settingsAndDefaultView.showSettingsUI(); } }, "ShowSettings"); settingsThread.start(); return new BindingResult(BindingResultCode.OK); } /** * Gets a BindingResult object containing the current status of the client. * * @param body Original RequestBody. * @param params Original Parameters. * @param attachments Original list of Attachment object. * @return A {@link BindingResult} object containing the current status of the App as XML structure. */ private BindingResult processStatus(RequestBody body, Map<String, String> params, List<Attachment> attachments) { BindingResult response = statusAction.execute(body, params, attachments); return response; } /** * Process the tcTokenURL or the activation object and perform a authentication. * * @param params Parameters of the request. * @return A {@link BindingResult} representing the result of the authentication. */ private BindingResult processTcTokenOrActivationObject(Map<String, String> params) { BindingResult response; DynamicContext dynCtx = DynamicContext.getInstance(TR03112Keys.INSTANCE_KEY); dynCtx.put(TR03112Keys.COOKIE_MANAGER, new CookieManager()); try { TCTokenRequest tcTokenRequest = null; try { tcTokenRequest = TCTokenRequest.convert(params, ctx); response = tokenHandler.handleActivate(tcTokenRequest); // Show success message. If we get here we have a valid StartPAOSResponse and a valid refreshURL if (!tcTokenRequest.isTokenFromObject()) { showFinishMessage((TCTokenResponse) response); } } catch (ActivationError ex) { if (ex instanceof NonGuiException) { // error already displayed to the user so do not repeat it here } else { if (ex.getMessage().equals("Invalid HTTP message received.")) { showErrorMessage(lang.translationForKey(ACTIVATION_INVALID_REFRESH_ADDRESS)); } else { showErrorMessage(ex.getLocalizedMessage()); } } logger.error(ex.getMessage()); logger.debug(ex.getMessage(), ex); // stack trace only in debug level logger.debug("Returning result: \n{}", ex.getBindingResult()); if (ex instanceof FatalActivationError) { logger.info("Authentication failed, displaying error in Browser."); } else { logger.info("Authentication failed, redirecting to with errors attached to the URL."); } response = ex.getBindingResult(); } finally { if (tcTokenRequest != null && tcTokenRequest.getTokenContext() != null) { // close connection to tctoken server in case PAOS didn't already perform this action tcTokenRequest.getTokenContext().closeStream(); } } } catch (RuntimeException e) { response = new BindingResult(BindingResultCode.INTERNAL_ERROR); logger.error(e.getMessage(), e); } return response; } }