/*
* 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.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import io.werval.api.http.Cookies;
import io.werval.api.http.Headers;
import io.werval.api.http.Method;
import io.werval.api.http.ProtocolVersion;
import io.werval.api.http.QueryString;
import io.werval.api.http.RequestHeader;
import io.werval.api.i18n.Lang;
import io.werval.api.i18n.Langs;
import io.werval.api.mime.MediaRange;
import io.werval.api.routes.ParameterBinders;
import io.werval.api.routes.Route;
import io.werval.runtime.exceptions.BadRequestException;
import io.werval.runtime.mime.MediaRangeInstance;
import io.werval.util.Couple;
import io.werval.util.Strings;
import static io.werval.api.http.Headers.Names.ACCEPT;
import static io.werval.api.http.Headers.Names.ACCEPT_LANGUAGE;
import static io.werval.api.http.Headers.Names.CONNECTION;
import static io.werval.api.http.Headers.Names.CONTENT_TYPE;
import static io.werval.api.http.Headers.Names.HOST;
import static io.werval.api.http.Headers.Names.X_FORWARDED_FOR;
import static io.werval.api.http.Headers.Values.CLOSE;
import static io.werval.api.http.Headers.Values.KEEP_ALIVE;
import static io.werval.runtime.http.HttpConstants.DEFAULT_HTTPS_PORT;
import static io.werval.runtime.http.HttpConstants.DEFAULT_HTTP_PORT;
import static java.util.Collections.emptyList;
import static java.util.Collections.unmodifiableMap;
import static java.util.stream.Collectors.toList;
/**
* RequestHeader instance.
*/
public class RequestHeaderInstance
implements RequestHeader
{
private final Langs langs;
private final String identity;
private final String remoteSocketAddress;
private final boolean xffEnabled;
private final boolean xffCheckProxies;
private final List<String> xffTrustedProxies;
private final ProtocolVersion version;
private final Method method;
private final String uri;
private final String path;
private final QueryString queryString;
private final Headers headers;
private final Cookies cookies;
private final Map<String, Object> parameters = new LinkedHashMap<>();
public RequestHeaderInstance(
Langs langs,
String identity, String remoteSocketAddress,
boolean xffEnabled, boolean xffCheckProxies, List<String> xffTrustedProxies,
ProtocolVersion version, Method method,
String uri, String path, QueryString queryString,
Headers headers, Cookies cookies
)
{
this.langs = langs;
this.identity = identity;
this.remoteSocketAddress = remoteSocketAddress;
this.xffEnabled = xffEnabled;
this.xffCheckProxies = xffCheckProxies;
this.xffTrustedProxies = xffTrustedProxies == null ? emptyList() : xffTrustedProxies;
this.version = version;
this.method = method;
this.uri = uri;
this.path = path;
this.queryString = queryString;
this.headers = headers;
this.cookies = cookies;
}
@Override
public String identity()
{
return identity;
}
@Override
public ProtocolVersion version()
{
return version;
}
@Override
public Method method()
{
return method;
}
@Override
public String uri()
{
return uri;
}
@Override
public String path()
{
return path;
}
@Override
public QueryString queryString()
{
return queryString;
}
@Override
public Headers headers()
{
return headers;
}
@Override
public Cookies cookies()
{
return cookies;
}
@Override
public String remoteAddress()
{
if( xffEnabled && headers.has( X_FORWARDED_FOR ) )
{
String xForwardedFor = headers.singleValue( X_FORWARDED_FOR );
if( Strings.isEmpty( xForwardedFor ) )
{
throw new BadRequestException( X_FORWARDED_FOR + " header is empty." );
}
String[] proxyChain = xForwardedFor.split( "," );
String remoteAddress = proxyChain[0].trim();
if( xffCheckProxies )
{
if( proxyChain.length == 1 )
{
throw new BadRequestException( X_FORWARDED_FOR + " header cannot be trusted, no proxy in chain." );
}
for( int idx = 1; idx < proxyChain.length; idx++ )
{
String proxy = proxyChain[idx].trim();
if( !xffTrustedProxies.contains( proxy ) )
{
throw new BadRequestException(
X_FORWARDED_FOR + " header cannot be trusted, untrusted proxy in chain: " + proxy
);
}
}
}
return remoteAddress;
}
return remoteSocketAddress;
}
@Override
public String host()
{
return headers.singleValue( HOST );
}
@Override
public int port()
{
if( uri.startsWith( "http" ) )
{
String parse = uri.substring( "https://".length() + 1 );
int slashIdx = parse.indexOf( '/' );
if( slashIdx < 0 )
{
return uri.startsWith( "https" ) ? DEFAULT_HTTPS_PORT : DEFAULT_HTTP_PORT;
}
parse = parse.substring( 0, slashIdx );
int colIdx = parse.indexOf( ':' );
return colIdx < 0
? uri.startsWith( "https" ) ? DEFAULT_HTTPS_PORT : DEFAULT_HTTP_PORT
: Integer.valueOf( parse.substring( colIdx + 1, parse.length() ) );
}
else
{
String host = headers.singleValue( HOST );
if( host.contains( ":" ) )
{
return Integer.valueOf( host.split( ":" )[1] );
}
return DEFAULT_HTTP_PORT;
}
}
@Override
public String domain()
{
String host = headers.singleValue( HOST );
if( host.contains( ":" ) )
{
return host.split( ":" )[0];
}
return host;
}
@Override
public Optional<String> contentType()
{
return headers.singleValueOptional( CONTENT_TYPE )
.map( Headers::extractContentMimeType )
.orElse( Optional.empty() );
}
@Override
public Optional<String> charset()
{
return headers.singleValueOptional( CONTENT_TYPE )
.map( Headers::extractCharset )
.orElse( Optional.empty() );
}
@Override
public boolean isKeepAlive()
{
Optional<String> connection = headers.singleValueOptional( CONNECTION );
if( connection.isPresent() && CLOSE.equalsIgnoreCase( connection.get() ) )
{
return false;
}
if( version.isKeepAliveDefault() )
{
return true;
}
return connection.isPresent() && KEEP_ALIVE.equalsIgnoreCase( connection.get() );
}
@Override
public RequestHeader bind( ParameterBinders parameterBinders, Route route )
{
parameters.clear();
parameters.putAll( route.bindParameters( parameterBinders, path, queryString ) );
return this;
}
@Override
public Map<String, Object> parameters()
{
return unmodifiableMap( parameters );
}
@Override
public List<Lang> acceptedLangs()
{
SortedSet<Couple<Double, String>> parsed = new TreeSet<>( (o1, o2) -> o2.left().compareTo( o1.left() ) );
parsed.addAll( parseAcceptHeader( ACCEPT_LANGUAGE ) );
return parsed.stream().map( e -> langs.fromCode( e.right() ) ).collect( toList() );
}
@Override
public Lang preferredLang()
{
return langs.preferred( acceptedLangs() );
}
@Override
public List<MediaRange> acceptedMimeTypes()
{
return MediaRangeInstance.parseList( Strings.join( headers.values( ACCEPT ), "," ) );
}
@Override
public boolean acceptsMimeType( String mimeType )
{
return MediaRangeInstance.accepts( acceptedMimeTypes(), mimeType );
}
@Override
public String preferredMimeType( String... mimeTypes )
{
return MediaRangeInstance.preferred( acceptedMimeTypes(), mimeTypes );
}
/**
* Pattern that match q-values of {@literal Accept*} headers.
*/
private static final Pattern Q_PATTERN = Pattern.compile( ";\\s*q=([0-9.]+)" );
/**
* Parse {@literal Accept*} headers.
*
* @param acceptHeaderName Any {@literal Accept*} header name
*
* @return Parsed {@literal Accept*} header items with their q-values
*/
private List<Couple<Double, String>> parseAcceptHeader( String acceptHeaderName )
{
if( !headers.has( acceptHeaderName ) )
{
return emptyList();
}
List<Couple<Double, String>> parsed = new ArrayList<>();
List<String> acceptLangHeaders = headers.values( acceptHeaderName );
for( String acceptLangHeader : acceptLangHeaders )
{
for( String acceptLang : acceptLangHeader.split( "," ) )
{
Matcher matcher = Q_PATTERN.matcher( acceptLang );
if( matcher.find() )
{
parsed.add( Couple.of(
Double.valueOf( matcher.group( 1 ) ),
acceptLang.substring( 0, matcher.start() ).trim()
) );
}
else
{
parsed.add( Couple.of( 1D, acceptLang.trim() ) );
}
}
}
return parsed;
}
}