/************************************************************************* * Copyright 2009-2015 Eucalyptus Systems, Inc. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3 of the License. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see http://www.gnu.org/licenses/. * * Please contact Eucalyptus Systems, Inc., 6755 Hollister Ave., Goleta * CA 93117, USA or visit http://www.eucalyptus.com/licenses/ if you need * additional information or have any questions. * * This file may incorporate work covered under the following copyright * and permission notice: * * Software License Agreement (BSD License) * * Copyright (c) 2008, Regents of the University of California * All rights reserved. * * Redistribution and use of this software in source and binary forms, * with or without modification, are permitted provided that the * following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. USERS OF THIS SOFTWARE ACKNOWLEDGE * THE POSSIBLE PRESENCE OF OTHER OPEN SOURCE LICENSED MATERIAL, * COPYRIGHTED MATERIAL OR PATENTED MATERIAL IN THIS SOFTWARE, * AND IF ANY SUCH MATERIAL IS DISCOVERED THE PARTY DISCOVERING * IT MAY INFORM DR. RICH WOLSKI AT THE UNIVERSITY OF CALIFORNIA, * SANTA BARBARA WHO WILL THEN ASCERTAIN THE MOST APPROPRIATE REMEDY, * WHICH IN THE REGENTS' DISCRETION MAY INCLUDE, WITHOUT LIMITATION, * REPLACEMENT OF THE CODE SO IDENTIFIED, LICENSING OF THE CODE SO * IDENTIFIED, OR WITHDRAWAL OF THE CODE CAPABILITY TO THE EXTENT * NEEDED TO COMPLY WITH ANY SUCH LICENSES OR RIGHTS. ************************************************************************/ package com.eucalyptus.crypto.util; import java.text.ParsePosition; import java.text.SimpleDateFormat; import java.util.Date; import java.util.List; import java.util.Map; import java.util.TimeZone; import org.apache.log4j.Logger; import com.eucalyptus.auth.login.AuthenticationException; import com.google.common.base.Function; import com.google.common.base.Strings; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; public class Timestamps { private static final Logger LOG = Logger.getLogger( Timestamps.class ); private static boolean allowTrailers = Boolean.parseBoolean( System.getProperty( "com.eucalyptus.crypto.util.allowTimestampTrailers", "false" ) ); public enum Type { RFC_2616(rfc2616), ISO_8601(iso8601); private final List<PatternHolder> patterns; private Type( final List<PatternHolder> patterns ) { this.patterns = patterns; } } /** * Parse a timestamp from an ISO 8601 format. * * @param timestamp The timestamp to parse. * @return The date representing the timestamp * @throws AuthenticationException If the timestamp cannot be parsed */ public static Date parseIso8601Timestamp( final String timestamp ) throws AuthenticationException { return parseTimestamp( timestamp, Timestamps.iso8601 ); } /** * Parse a timestamp from an RFC 2616 / HTTP 1.1 date format. * * @param timestamp The timestamp to parse. * @return The date representing the timestamp * @throws AuthenticationException If the timestamp cannot be parsed */ public static Date parseRfc2616Timestamp( final String timestamp ) throws AuthenticationException { return parseTimestamp( timestamp, Timestamps.rfc2616 ); } public static Date parseTimestamp( final String timestamp, final Type type ) throws AuthenticationException { return parseTimestamp( timestamp, type.patterns ); } private static Date parseTimestamp( final String timestamp, final Iterable<PatternHolder> patterns ) throws AuthenticationException { if ( timestamp != null ) for ( final PatternHolder pattern : patterns ) { final ParsePosition position = new ParsePosition(0); final Date parsed = pattern.parse( timestamp, position ); if ( parsed == null || (position.getIndex() < timestamp.length() && !allowTrailers)) { if ( LOG.isTraceEnabled() ) LOG.trace( "Parse of timestamp '"+timestamp+"' failed for pattern '"+pattern+"', at: " + position.getErrorIndex() ); } else { return parsed; } } throw new AuthenticationException( "Invalid timestamp format: " + timestamp ); } public static String formatRfc822Timestamp( final Date date ) { return sdf( rfc822Timestamp ).format( date ); } public static String formatIso8601Timestamp( final Date date ) { return sdf( iso8601Timestamp ).format( date ); } public static String formatShortIso8601Timestamp( final Date date ) { return sdf( iso8601ShortTimestamp ).format( date ); } public static String formatShortIso8601Date( final Date date ) { return sdf( iso8601ShortDate ).format( date ); } public static String formatIso8601UTCLongDateMillisTimezone( final Date date ) { final SimpleDateFormat format = sdf( iso8601TimestampWithMillisAndTimezone ); return format.format(date); } /** * RFC 822 timestamp format suitable for HTTP headers */ private static final String rfc822Timestamp = "EEE, dd MMM yyyy HH:mm:ss z"; /** * ISO 8601 short timestamp format */ private static final String iso8601TimestampWithMillisAndTimezone = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"; /** * ISO 8601 short timestamp format */ private static final String iso8601Timestamp = "yyyy-MM-dd'T'HH:mm:ss'Z'"; /** * ISO 8601 short timestamp format */ private static final String iso8601ShortTimestamp = "yyyyMMdd'T'HHmmss'Z'"; /** * ISO 8601 short date format */ private static final String iso8601ShortDate = "yyyyMMdd"; /** * Time zone to be used for simple date format. */ private static final Map<String,String> zonesByPattern = ImmutableMap.of( rfc822Timestamp, "GMT" ); /** * RFC 2616 / HTTP 1.1 date formats (http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.3.1) */ private static final List<PatternHolder> rfc2616 = ImmutableList.of( new PatternHolder( "EEE, dd MMM yyyy HH:mm:ss zzz" ), // RFC 822 / 1123 new PatternHolder( "EEEE, dd-MMM-yy HH:mm:ss zzz" ), // RFC 850 / 1036 new PatternHolder( "EEE MMM d HH:mm:ss yyyy" ) // ANSI C asctime() format ); /** * ISO 8601 date formats */ static final List<PatternHolder> iso8601; static { final List<String> patterns = Lists.newArrayList( "yyyy-MM-dd'T'HH:mm:ss", "yyyy-MM-dd'T'HH:mm:ssZ", "yyyy-MM-dd'T'HH:mm:ssX", "yyyy-MM-dd'T'HH:mm:ssXXX", "yyyy-MM-dd'T'HH:mm:ss.SSSZ", "yyyy-MM-dd'T'HH:mm:ss.SSSX", "yyyy-MM-dd'T'HH:mm:ss.SSSXXX", "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'Z", "yyyy-MM-dd'T'HH:mm:ss'Z'", "yyyy-MM-dd'T'HH:mm:ss'Z'Z" ); // Generate seed patterns with various sub-second precisions for ( int i=1; i<10; i++ ) { String pattern = "yyyy-MM-dd'T'HH:mm:ss." + Strings.repeat( "S", i ); patterns.add( pattern ); patterns.add( pattern + "'Z'" ); } final List<PatternHolder> generatedPatterns = Lists.newArrayList(); for ( final String pattern : patterns ) { for ( final Iso8601Variants variant : Iso8601Variants.values() ) { // Type hint required to compile on OpenJDK 1.6 generatedPatterns.add( PatternHolder.generate( ((Function<String,String>)variant).apply( pattern ) ) ); } } LOG.debug( "Using ISO 8601 date patterns: " + generatedPatterns ); iso8601 = ImmutableList.copyOf( generatedPatterns ); } private static ThreadLocal<Cache<String,SimpleDateFormat>> patternLocal = new ThreadLocal<Cache<String,SimpleDateFormat>>( ) { @Override protected Cache<String,SimpleDateFormat> initialValue( ) { return CacheBuilder.newBuilder( ).softValues( ).build( ); } }; private static SimpleDateFormat sdf( final String pattern ) { SimpleDateFormat format = patternLocal.get( ).getIfPresent( pattern ); if ( format == null ) { format = new SimpleDateFormat( pattern ); format.setTimeZone( TimeZone.getTimeZone( zone( pattern ) ) ); patternLocal.get( ).put( pattern, format ); } return format; } private static String zone( final String pattern ) { return zonesByPattern.getOrDefault( pattern, "UTC" ); } private static final class PatternHolder { private final String pattern; private final int length; // length of the text that can match the pattern if known private final int fractionTrunction; private final int fractionPadding; private PatternHolder( final String pattern ) { this( pattern, -1 ); } private PatternHolder( final String pattern, final int length ) { this.pattern = pattern; this.length = length; int precision = 0; for ( char character : pattern.toCharArray() ) { if ( character == 'S' ) precision++; } fractionTrunction = Math.max( 0, precision - 3 ); fractionPadding = precision == 0 ? 0 : Math.max( 0, 3 - precision ); } /** * TODO - WARNING, only supports a special case for expected ISO 8601 date formats */ private static PatternHolder generate( final String pattern ) { String representativeInput = pattern; representativeInput = representativeInput.replace( "'T'", "T" ); representativeInput = representativeInput.replace( "'Z'", "U" ); representativeInput = representativeInput.replace( "Z", "-0000" ); representativeInput = representativeInput.replace( "XXX", "-00:00" ); representativeInput = representativeInput.replace( "X", "-00" ); return new PatternHolder( pattern, representativeInput.length() ); } private Date parse( final String timestamp, final ParsePosition position ) { Date result = null; if ( length == -1 || length == timestamp.length() - position.getIndex() ) { String timestampForParsing = timestamp; boolean valid = true; if ( fractionTrunction > 0 || fractionPadding > 0 ) { valid = false; final int fractionIndex = timestamp.indexOf('.'); if ( fractionTrunction > 0 ) { // Date parser parses milliseconds from the wrong end of the value // e.g. 000581 is 581 milliseconds when it should be zero // To parse correctly we shift the value and pad with leading zeros. if ( fractionIndex > 0 && timestamp.length() >= (fractionIndex + 4 + fractionTrunction) ) { timestampForParsing = timestamp.substring( 0, fractionIndex + 1 ) + Strings.repeat( "0", fractionTrunction ) + timestamp.substring( fractionIndex + 1, fractionIndex + 4 ) + timestamp.substring( fractionIndex + 4 + fractionTrunction ); final String unparsed = timestamp.substring( fractionIndex + 4, fractionIndex + 4 + fractionTrunction ); valid = isDigits( unparsed ); } } else if ( fractionIndex > 0 && timestamp.length() >= (fractionIndex + 3 - fractionPadding) ) { // Date parser parses fractional tens or hundredths of a second incorrectly // e.g. .5 is 5 milliseconds when it should be 500 // To parse correctly we pad with trailing zeros. timestampForParsing = timestamp.substring( 0, fractionIndex + ( 4 - fractionPadding ) ) + Strings.repeat( "0", fractionPadding ) + timestamp.substring( fractionIndex + ( 4 - fractionPadding ) ); valid = true; } } if ( valid ) { result = sdf( pattern ).parse( timestampForParsing, position ); } } if ( result == null && position.getErrorIndex() < 0 ) { position.setErrorIndex( position.getIndex() ); } return result; } private boolean isDigits( final String text ) { boolean digits = true; for ( final char character : text.toCharArray() ) { if ( character < '0' || character > '9' ) { digits = false; break; } } return digits; } public String toString() { return pattern; } } private enum Iso8601Variants implements Function<String,String> { IDENTITY { @Override public String apply( final String pattern ) { return pattern; } }, /** * The hyphens can be omitted if compactness of the representation is more * important than human readability .... As with the date notation, the * separating colons can also be omitted */ SHORT { @Override public String apply( final String pattern ) { return pattern.replaceAll( ":|-", "" ); } } } }