package io.mangoo.helpers; import java.net.URI; import java.security.SecureRandom; import java.time.LocalDateTime; import java.util.Arrays; import java.util.Collections; import java.util.Deque; import java.util.HashMap; import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Optional; import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.lang3.StringUtils; import com.github.scribejava.apis.FacebookApi; import com.github.scribejava.apis.GoogleApi20; import com.github.scribejava.apis.TwitterApi; import com.github.scribejava.core.builder.ServiceBuilder; import com.github.scribejava.core.oauth.OAuthService; import io.mangoo.configuration.Config; import io.mangoo.core.Application; import io.mangoo.crypto.Crypto; import io.mangoo.enums.ContentType; import io.mangoo.enums.Default; import io.mangoo.enums.Key; import io.mangoo.enums.Required; import io.mangoo.enums.oauth.OAuthProvider; import io.mangoo.models.Identity; import io.mangoo.routing.Attachment; import io.undertow.security.api.AuthenticationMechanism; import io.undertow.security.api.AuthenticationMode; import io.undertow.security.handlers.AuthenticationCallHandler; import io.undertow.security.handlers.AuthenticationConstraintHandler; import io.undertow.security.handlers.AuthenticationMechanismsHandler; import io.undertow.security.handlers.SecurityInitialHandler; import io.undertow.security.impl.BasicAuthenticationMechanism; import io.undertow.server.HttpHandler; import io.undertow.server.HttpServerExchange; import io.undertow.server.handlers.Cookie; import io.undertow.server.handlers.sse.ServerSentEventConnection; import io.undertow.util.AttachmentKey; import io.undertow.util.Cookies; import io.undertow.util.HeaderMap; import io.undertow.util.Headers; import io.undertow.util.Methods; import io.undertow.websockets.core.WebSocketChannel; /** * * @author svenkubiak * */ public class RequestHelper { public static final AttachmentKey<Attachment> ATTACHMENT_KEY = AttachmentKey.create(Attachment.class); private static final String SCOPE = "https://www.googleapis.com/auth/userinfo.email"; private static final int MAX_RANDOM = 999_999; private static final int AUTH_PREFIX_LENGTH = 3; private static final int INDEX_0 = 0; private static final int INDEX_1 = 1; private static final int INDEX_2 = 2; /** * Converts request and query parameter into a single map * * @param exchange The Undertow HttpServerExchange * @return A single map contain both request and query parameter */ public Map<String, String> getRequestParameters(HttpServerExchange exchange) { Objects.requireNonNull(exchange, Required.HTTP_SERVER_EXCHANGE.toString()); final Map<String, String> requestParamater = new HashMap<>(); final Map<String, Deque<String>> queryParameters = exchange.getQueryParameters(); queryParameters.putAll(exchange.getPathParameters()); queryParameters.entrySet().forEach(entry -> requestParamater.put(entry.getKey(), entry.getValue().element())); //NOSONAR return requestParamater; } /** * Checks if the request is a POST or a PUT request * * @deprecated As of version 4.4.0, replaced by {@link #isPostPutPatch(HttpServerExchange)} * * @param exchange The Undertow HttpServerExchange * @return True if the request is a POST or a PUT request, false otherwise */ @Deprecated public boolean isPostOrPut(HttpServerExchange exchange) { Objects.requireNonNull(exchange, Required.HTTP_SERVER_EXCHANGE.toString()); return (Methods.POST).equals(exchange.getRequestMethod()) || (Methods.PUT).equals(exchange.getRequestMethod()); } /** * Checks if the request is a POST, PUT or PATCH request * * @param exchange The Undertow HttpServerExchange * @return True if the request is a POST, PUT or PATCH request, false otherwise */ public boolean isPostPutPatch(HttpServerExchange exchange) { Objects.requireNonNull(exchange, Required.HTTP_SERVER_EXCHANGE.toString()); return (Methods.POST).equals(exchange.getRequestMethod()) || (Methods.PUT).equals(exchange.getRequestMethod()) || (Methods.PATCH).equals(exchange.getRequestMethod()); } /** * Checks if the requests content-type contains application/json * * @param exchange The Undertow HttpServerExchange * @return True if the request content-type contains application/json, false otherwise */ public boolean isJsonRequest(HttpServerExchange exchange) { Objects.requireNonNull(exchange, Required.HTTP_SERVER_EXCHANGE.toString()); final HeaderMap headerMap = exchange.getRequestHeaders(); return headerMap != null && headerMap.get(Headers.CONTENT_TYPE) != null && headerMap.get(Headers.CONTENT_TYPE).element().toLowerCase(Locale.ENGLISH).contains(ContentType.APPLICATION_JSON.toString().toLowerCase(Locale.ENGLISH)); } /** * Creates an OAuthService for authentication a user with OAuth * * @param oAuthProvider The OAuth provider Enum * @return An OAuthService object or null if creating failed */ @SuppressWarnings("rawtypes") public Optional<OAuthService> createOAuthService(OAuthProvider oAuthProvider) { Objects.requireNonNull(oAuthProvider, Required.OAUTH_PROVIDER.toString()); Config config = Application.getInstance(Config.class); OAuthService oAuthService = null; switch (oAuthProvider) { case TWITTER: oAuthService = new ServiceBuilder() .callback(config.getString(Key.OAUTH_TWITTER_CALLBACK)) .apiKey(config.getString(Key.OAUTH_TWITTER_KEY)) .apiSecret(config.getString(Key.OAUTH_TWITTER_SECRET)) .build(TwitterApi.instance()); break; case GOOGLE: oAuthService = new ServiceBuilder() .scope(SCOPE) .callback(config.getString(Key.OAUTH_GOOGLE_CALLBACK)) .apiKey(config.getString(Key.OAUTH_GOOGLE_KEY)) .apiSecret(config.getString(Key.OAUTH_GOOGLE_SECRET)) .state("secret" + new SecureRandom().nextInt(MAX_RANDOM)) .build(GoogleApi20.instance()); break; case FACEBOOK: oAuthService = new ServiceBuilder() .callback(config.getString(Key.OAUTH_FACEBOOK_CALLBACK)) .apiKey(config.getString(Key.OAUTH_FACEBOOK_KEY)) .apiSecret(config.getString(Key.OAUTH_FACEBOOK_SECRET)) .build(FacebookApi.instance()); break; default: break; } return (oAuthService == null) ? Optional.empty() : Optional.of(oAuthService); } /** * Returns an OAuthProvider based on a given string * * @param oauth The string to lookup the OAuthProvider Enum * @return OAuthProvider Enum */ public Optional<OAuthProvider> getOAuthProvider(String oauth) { OAuthProvider oAuthProvider = null; if (OAuthProvider.FACEBOOK.toString().equals(oauth)) { oAuthProvider = OAuthProvider.FACEBOOK; } else if (OAuthProvider.TWITTER.toString().equals(oauth)) { oAuthProvider = OAuthProvider.TWITTER; } else if (OAuthProvider.GOOGLE.toString().equals(oauth)) { oAuthProvider = OAuthProvider.GOOGLE; } return (oAuthProvider == null) ? Optional.empty() : Optional.of(oAuthProvider); } /** * Checks if the given header contains a valid authentication * * @param cookieHeader The header to parse * @return True if the cookie contains a valid authentication, false otherwise */ public boolean hasValidAuthentication(String cookieHeader) { boolean validAuthentication = false; if (StringUtils.isNotBlank(cookieHeader)) { final Map<String, Cookie> cookies = Cookies.parseRequestCookies(1, false, Arrays.asList(cookieHeader)); Config config = Application.getInstance(Config.class); String cookieValue = cookies.get(config.getAuthenticationCookieName()).getValue(); if (StringUtils.isNotBlank(cookieValue) && !("null").equals(cookieValue)) { if (config.isAuthenticationCookieEncrypt()) { cookieValue = Application.getInstance(Crypto.class).decrypt(cookieValue); } String sign = null; String expires = null; String version = null; final String prefix = StringUtils.substringBefore(cookieValue, Default.DATA_DELIMITER.toString()); if (StringUtils.isNotBlank(prefix)) { final String [] prefixes = prefix.split("\\" + Default.DELIMITER.toString()); if (prefixes != null && prefixes.length == AUTH_PREFIX_LENGTH) { sign = prefixes [INDEX_0]; expires = prefixes [INDEX_1]; version = prefixes [INDEX_2]; } } if (StringUtils.isNotBlank(sign) && StringUtils.isNotBlank(expires)) { final String authenticatedUser = cookieValue.substring(cookieValue.indexOf(Default.DATA_DELIMITER.toString()) + 1, cookieValue.length()); final LocalDateTime expiresDate = LocalDateTime.parse(expires); if (LocalDateTime.now().isBefore(expiresDate) && DigestUtils.sha512Hex(authenticatedUser + expires + version + config.getApplicationSecret()).equals(sign)) { validAuthentication = true; } } } } return validAuthentication; } /** * Retrieves a URL from a Server-Sent Event connection * * @param connection The ServerSentEvent Connection * * @return The URL of the Server-Sent Event Connection */ public String getServerSentEventURL(ServerSentEventConnection connection) { return getURL(URI.create(connection.getRequestURI())); } /** * Retrieves the URL of a WebSocketChannel * * @param channel The WebSocket Channel * * @return The URL of the WebSocket Channel */ public String getWebSocketURL(WebSocketChannel channel) { return getURL(URI.create(channel.getUrl())); } /** * Creates and URL with only path and if present query and * fragment, e.g. /path/data?key=value#fragid1 * * @param uri The URI to generate from * @return The generated URL */ private String getURL(URI uri) { final StringBuilder buffer = new StringBuilder(); buffer.append(uri.getPath()); String query = uri.getQuery(); String fragment = uri.getFragment(); if (StringUtils.isNotBlank(query)) { buffer.append('?').append(query); } if (StringUtils.isNotBlank(fragment)) { buffer.append('#').append(fragment); } return buffer.toString(); } /** * Adds a Wrapper to the handler when the request requires authentication * * @param httpHandler The Handler to wrap * @param username The username to use * @param password The password to use * @return An HttpHandler wrapped through BasicAuthentication */ public HttpHandler wrapSecurity(HttpHandler httpHandler, String username, String password) { Objects.requireNonNull(httpHandler, Required.HTTP_HANDLER.toString()); Objects.requireNonNull(username, Required.USERNAME.toString()); Objects.requireNonNull(password, Required.PASSWORD.toString()); HttpHandler wrap = new AuthenticationCallHandler(httpHandler); wrap = new AuthenticationConstraintHandler(wrap); wrap = new AuthenticationMechanismsHandler(wrap, Collections.<AuthenticationMechanism>singletonList(new BasicAuthenticationMechanism("Authentication required"))); return new SecurityInitialHandler(AuthenticationMode.PRO_ACTIVE, new Identity(username, password), wrap); } }