api: signature v3 to accept more formats (#2893)

It does it by reusing the DateUtil helpers. DateUtil uses java.time.* as that one knows how to deal
with timezones correctly.

The format expected by signatureVersion=3&expires=.... is quite limited.

It should accept the following formats that are containing a timezone and/or milliseconds.

2018-10-01T08:12:14Z
2018-10-01T08:12:14+01:00
2018-10-01T08:12:14+0100
2018-10-01T08:12:14.000Z
2018-10-01T08:12:14.000+01:00
2018-10-01T08:12:14.000+0100
afaik only 2018-10-01T08:12:14+0100 is accepted by the current codebase.

This PR echoes other pull requests I made earlier this year. #2392 and #2867

Signed-off-by: Yoan Blanc <yoan.blanc@exoscale.ch>
This commit is contained in:
Yoan Blanc 2018-10-31 12:27:48 +01:00 committed by Rohit Yadav
parent 7479e2877f
commit 17c164d59a
3 changed files with 91 additions and 19 deletions

View File

@ -42,6 +42,7 @@ import com.cloud.user.User;
import com.cloud.user.UserAccount;
import com.cloud.user.UserVO;
import com.cloud.utils.ConstantTimeComparator;
import com.cloud.utils.DateUtil;
import com.cloud.utils.HttpUtils;
import com.cloud.utils.NumbersUtil;
import com.cloud.utils.Pair;
@ -166,9 +167,7 @@ import java.net.URISyntaxException;
import java.net.URLEncoder;
import java.security.SecureRandom;
import java.security.Security;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
@ -228,7 +227,6 @@ public class ApiServer extends ManagerBase implements HttpRequestHandler, ApiSer
private ApiAsyncJobDispatcher asyncDispatcher;
private static int s_workerCount = 0;
private static final DateFormat DateFormatToUse = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ");
private static Map<String, List<Class<?>>> s_apiNameCmdClassMap = new HashMap<String, List<Class<?>>>();
private static ExecutorService s_executor = new ThreadPoolExecutor(10, 150, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(), new NamedThreadFactory(
@ -883,14 +881,14 @@ public class ApiServer extends ManagerBase implements HttpRequestHandler, ApiSer
s_logger.debug("Missing Expires parameter -- ignoring request. Signature: " + signature + ", apiKey: " + apiKey);
return false;
}
synchronized (DateFormatToUse) {
try {
expiresTS = DateFormatToUse.parse(expires);
expiresTS = DateUtil.parseTZDateString(expires);
} catch (final ParseException pe) {
s_logger.debug("Incorrect date format for Expires parameter", pe);
return false;
}
}
final Date now = new Date(System.currentTimeMillis());
if (expiresTS.before(now)) {
s_logger.debug("Request expired -- ignoring ...sig: " + signature + ", apiKey: " + apiKey);

View File

@ -26,6 +26,10 @@ import java.util.Calendar;
import java.util.Date;
import java.util.TimeZone;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.time.OffsetDateTime;
import com.cloud.utils.exception.CloudRuntimeException;
public class DateUtil {
@ -33,20 +37,34 @@ public class DateUtil {
public static final String YYYYMMDD_FORMAT = "yyyyMMddHHmmss";
private static final DateFormat s_outputFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ");
private static final DateTimeFormatter[] parseFormats = new DateTimeFormatter[]{
DateTimeFormatter.ISO_OFFSET_DATE_TIME,
DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssZ"),
DateTimeFormatter.ISO_INSTANT,
// with milliseconds
DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSSSSX"),
DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSSSSZ"),
DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'"),
// legacy and non-sensical format
DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'Z")
};
public static Date currentGMTTime() {
// Date object always stores miliseconds offset based on GMT internally
return new Date();
}
// yyyy-MM-ddTHH:mm:ssZZZZ or yyyy-MM-ddTHH:mm:ssZxxxx
public static Date parseTZDateString(String str) throws ParseException {
for (DateTimeFormatter formatter : parseFormats) {
try {
return s_outputFormat.parse(str);
} catch (ParseException e) {
final DateFormat dfParse = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'Z");
return dfParse.parse(str);
OffsetDateTime dt = OffsetDateTime.parse(str, formatter);
return Date.from(dt.toInstant());
} catch (DateTimeParseException e) {
// do nothing
}
}
throw new ParseException("Unparseable date: \"" + str + "\"", 0);
}
public static Date parseDateString(TimeZone tz, String dateString) {
return parseDateString(tz, dateString, "yyyy-MM-dd HH:mm:ss");

View File

@ -25,6 +25,10 @@ import java.util.Calendar;
import java.util.Date;
import java.util.TimeZone;
import java.time.format.DateTimeFormatter;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import com.cloud.utils.DateUtil.IntervalType;
import org.junit.Test;
@ -32,7 +36,6 @@ import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class DateUtilTest {
// command line test tool
public static void main(String[] args) {
TimeZone localTimezone = Calendar.getInstance().getTimeZone();
@ -56,7 +59,7 @@ public class DateUtilTest {
String str = dfDate.format(time);
Date dtParsed = DateUtil.parseTZDateString(str);
assertEquals(time.toString(), dtParsed.toString());
assertEquals(str, time.toString(), dtParsed.toString());
}
@Test
@ -66,6 +69,59 @@ public class DateUtilTest {
String str = dfDate.format(time);
Date dtParsed = DateUtil.parseTZDateString(str);
assertEquals(time.toString(), dtParsed.toString());
assertEquals(str, time.toString(), dtParsed.toString());
}
@Test
public void zonedTimeFormatIsoOffsetDateTime() throws ParseException {
Date time = new Date();
String str = OffsetDateTime.now().format(DateTimeFormatter.ISO_OFFSET_DATE_TIME);
Date dtParsed = DateUtil.parseTZDateString(str);
assertEquals(str, time.toString(), dtParsed.toString());
}
@Test
public void zonedTimeFormatIsoInstant() throws ParseException {
Date time = new Date();
String str = OffsetDateTime.now().format(DateTimeFormatter.ISO_INSTANT);
Date dtParsed = DateUtil.parseTZDateString(str);
assertEquals(str, time.toString(), dtParsed.toString());
}
@Test
public void zonedTimeFormatIsoOffsetDateTimeMs() throws ParseException {
Date time = new Date();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSSSSX");
String str = OffsetDateTime.now().format(formatter);
Date dtParsed = DateUtil.parseTZDateString(str);
assertEquals(str, time.toString(), dtParsed.toString());
}
@Test
public void zonedTimeFormatIsoInstantMs() throws ParseException {
Date time = new Date();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'");
String str = OffsetDateTime.now(ZoneOffset.UTC).format(formatter);
Date dtParsed = DateUtil.parseTZDateString(str);
assertEquals(str, time.toString(), dtParsed.toString());
}
@Test
public void zonedTimeFormatIsoNoColonZMs() throws ParseException {
Date time = new Date();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSSSSZ");
String str = OffsetDateTime.now().format(formatter);
Date dtParsed = DateUtil.parseTZDateString(str);
assertEquals(str, time.toString(), dtParsed.toString());
}
}