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.UserAccount;
import com.cloud.user.UserVO; import com.cloud.user.UserVO;
import com.cloud.utils.ConstantTimeComparator; import com.cloud.utils.ConstantTimeComparator;
import com.cloud.utils.DateUtil;
import com.cloud.utils.HttpUtils; import com.cloud.utils.HttpUtils;
import com.cloud.utils.NumbersUtil; import com.cloud.utils.NumbersUtil;
import com.cloud.utils.Pair; import com.cloud.utils.Pair;
@ -166,9 +167,7 @@ import java.net.URISyntaxException;
import java.net.URLEncoder; import java.net.URLEncoder;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.security.Security; import java.security.Security;
import java.text.DateFormat;
import java.text.ParseException; import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.Date; import java.util.Date;
@ -228,7 +227,6 @@ public class ApiServer extends ManagerBase implements HttpRequestHandler, ApiSer
private ApiAsyncJobDispatcher asyncDispatcher; private ApiAsyncJobDispatcher asyncDispatcher;
private static int s_workerCount = 0; 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 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( 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); s_logger.debug("Missing Expires parameter -- ignoring request. Signature: " + signature + ", apiKey: " + apiKey);
return false; return false;
} }
synchronized (DateFormatToUse) {
try { try {
expiresTS = DateFormatToUse.parse(expires); expiresTS = DateUtil.parseTZDateString(expires);
} catch (final ParseException pe) { } catch (final ParseException pe) {
s_logger.debug("Incorrect date format for Expires parameter", pe); s_logger.debug("Incorrect date format for Expires parameter", pe);
return false; return false;
}
} }
final Date now = new Date(System.currentTimeMillis()); final Date now = new Date(System.currentTimeMillis());
if (expiresTS.before(now)) { if (expiresTS.before(now)) {
s_logger.debug("Request expired -- ignoring ...sig: " + signature + ", apiKey: " + apiKey); 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.Date;
import java.util.TimeZone; import java.util.TimeZone;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.time.OffsetDateTime;
import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.utils.exception.CloudRuntimeException;
public class DateUtil { public class DateUtil {
@ -33,19 +37,33 @@ public class DateUtil {
public static final String YYYYMMDD_FORMAT = "yyyyMMddHHmmss"; 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 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() { public static Date currentGMTTime() {
// Date object always stores miliseconds offset based on GMT internally // Date object always stores miliseconds offset based on GMT internally
return new Date(); return new Date();
} }
// yyyy-MM-ddTHH:mm:ssZZZZ or yyyy-MM-ddTHH:mm:ssZxxxx
public static Date parseTZDateString(String str) throws ParseException { public static Date parseTZDateString(String str) throws ParseException {
try { for (DateTimeFormatter formatter : parseFormats) {
return s_outputFormat.parse(str); try {
} catch (ParseException e) { OffsetDateTime dt = OffsetDateTime.parse(str, formatter);
final DateFormat dfParse = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'Z"); return Date.from(dt.toInstant());
return dfParse.parse(str); } catch (DateTimeParseException e) {
// do nothing
}
} }
throw new ParseException("Unparseable date: \"" + str + "\"", 0);
} }
public static Date parseDateString(TimeZone tz, String dateString) { public static Date parseDateString(TimeZone tz, String dateString) {

View File

@ -25,6 +25,10 @@ import java.util.Calendar;
import java.util.Date; import java.util.Date;
import java.util.TimeZone; import java.util.TimeZone;
import java.time.format.DateTimeFormatter;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import com.cloud.utils.DateUtil.IntervalType; import com.cloud.utils.DateUtil.IntervalType;
import org.junit.Test; import org.junit.Test;
@ -32,7 +36,6 @@ import org.junit.Test;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
public class DateUtilTest { public class DateUtilTest {
// command line test tool // command line test tool
public static void main(String[] args) { public static void main(String[] args) {
TimeZone localTimezone = Calendar.getInstance().getTimeZone(); TimeZone localTimezone = Calendar.getInstance().getTimeZone();
@ -56,7 +59,7 @@ public class DateUtilTest {
String str = dfDate.format(time); String str = dfDate.format(time);
Date dtParsed = DateUtil.parseTZDateString(str); Date dtParsed = DateUtil.parseTZDateString(str);
assertEquals(time.toString(), dtParsed.toString()); assertEquals(str, time.toString(), dtParsed.toString());
} }
@Test @Test
@ -66,6 +69,59 @@ public class DateUtilTest {
String str = dfDate.format(time); String str = dfDate.format(time);
Date dtParsed = DateUtil.parseTZDateString(str); 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());
} }
} }