/*
* Copyright (c) 2013-2015 the original author or authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.werval.runtime.http;
import java.util.Collections;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import io.werval.api.Config;
import io.werval.api.Crypto;
import io.werval.api.http.Cookies.Cookie;
import io.werval.api.http.Session;
import io.werval.runtime.http.CookiesInstance.CookieInstance;
import io.werval.runtime.util.Comparators;
import io.werval.util.Strings;
import io.werval.util.URLs;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static io.werval.runtime.ConfigKeys.APP_SESSION_COOKIE_DOMAIN;
import static io.werval.runtime.ConfigKeys.APP_SESSION_COOKIE_HTTPONLY;
import static io.werval.runtime.ConfigKeys.APP_SESSION_COOKIE_NAME;
import static io.werval.runtime.ConfigKeys.APP_SESSION_COOKIE_PATH;
import static io.werval.runtime.ConfigKeys.APP_SESSION_COOKIE_SECURE;
import static io.werval.runtime.ConfigKeys.WERVAL_CHARACTER_ENCODING;
/**
* Session instance.
*/
public final class SessionInstance
implements Session
{
private static final Logger LOG = LoggerFactory.getLogger( SessionInstance.class );
private static final Pattern COOKIE_VALUE_PATTERN = Pattern.compile( "\u0000([^:]*):([^\u0000]*)\u0000" );
private final Config config;
private final Crypto crypto;
private final Map<String, String> session;
private boolean changed = false;
public SessionInstance( Config config, Crypto crypto )
{
this.config = config;
this.crypto = crypto;
this.session = new TreeMap<>( Comparators.LOWER_CASE );
}
public SessionInstance( Config config, Crypto crypto, Map<String, String> session )
{
this( config, crypto );
this.session.putAll( session );
}
public SessionInstance( Config config, Crypto crypto, Optional<Cookie> cookie )
{
this( config, crypto );
if( !cookie.isPresent() || Strings.isEmpty( cookie.get().value() ) )
{
return;
}
String cookieValue = cookie.get().value();
String[] splitted = cookieValue.split( "-", 2 );
if( splitted.length != 2 )
{
LOG.warn( "Invalid Session Cookie Value: '{}'. Will use an empty Session.", cookieValue );
return;
}
String signature = splitted[0];
String payload = splitted[1];
if( !signature.equals( crypto.hmacSha256Hex( payload ) ) )
{
LOG.warn( "Invalid Session Cookie Signature: '{}'. Will use an empty Session.", cookieValue );
return;
}
String decoded = URLs.decode( payload, config.charset( WERVAL_CHARACTER_ENCODING ) );
Matcher matcher = COOKIE_VALUE_PATTERN.matcher( decoded );
while( matcher.find() )
{
session.put( matcher.group( 1 ), matcher.group( 2 ) );
}
}
@Override
public boolean hasChanged()
{
return changed;
}
@Override
public boolean has( String key )
{
return session.containsKey( key );
}
@Override
public Optional<String> get( String key )
{
return Optional.ofNullable( session.get( key ) );
}
@Override
public void set( String key, String value )
{
if( key.contains( ":" ) )
{
throw new IllegalArgumentException( "Character ':' is not allowed in a session key." );
}
changed = true;
if( value == null )
{
session.remove( key );
}
else
{
session.put( key, value );
}
}
@Override
public String remove( String key )
{
changed = true;
return session.remove( key );
}
@Override
public void clear()
{
changed = true;
session.clear();
}
@Override
public Map<String, String> asMap()
{
return Collections.unmodifiableMap( session );
}
@Override
public Cookie signedCookie()
{
StringBuilder sb = new StringBuilder();
for( Entry<String, String> entry : session.entrySet() )
{
sb.append( "\u0000" ).append( entry.getKey() ).append( ":" ).append( entry.getValue() ).append( "\u0000" );
}
String sessionData = URLs.encode( sb.toString(), config.charset( WERVAL_CHARACTER_ENCODING ) );
String signedCookieValue = crypto.hmacSha256Hex( sessionData ) + "-" + sessionData;
return new CookieInstance(
0,
config.string( APP_SESSION_COOKIE_NAME ),
signedCookieValue,
config.string( APP_SESSION_COOKIE_PATH ),
config.stringOptional( APP_SESSION_COOKIE_DOMAIN ).orElse( null ),
Long.MIN_VALUE,
config.bool( APP_SESSION_COOKIE_SECURE ),
config.bool( APP_SESSION_COOKIE_HTTPONLY ),
null,
null
);
}
}