/*
* Copyright (C) 2013 Stichting Akvo (Akvo Foundation)
*
* This file is part of Akvo Flow.
*
* Akvo Flow 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, either version 3 of the License, or
* (at your option) any later version.
*
* Akvo Flow 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 Akvo Flow. If not, see <http://www.gnu.org/licenses/>.
*/
package org.akvo.flow.api;
import android.content.Context;
import android.util.Base64;
import org.akvo.flow.exception.HttpException;
import org.akvo.flow.util.ConstantUtil;
import org.akvo.flow.util.FileUtil;
import org.akvo.flow.util.HttpUtil;
import org.akvo.flow.util.PropertyUtil;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import timber.log.Timber;
public class S3Api {
private static final String URL = "https://%s.s3.amazonaws.com/%s";
private static final String PAYLOAD_GET = "GET\n\n\n%s\n/%s/%s";// date, bucket, obj
private static final String PAYLOAD_PUT_PUBLIC = "PUT\n%s\n%s\n%s\nx-amz-acl:public-read\n/%s/%s";// md5, type, date, bucket, obj
private static final String PAYLOAD_PUT_PRIVATE = "PUT\n%s\n%s\n%s\n/%s/%s";// md5, type, date, bucket, obj
private static final String PAYLOAD_HEAD = "HEAD\n\n\n%s\n/%s/%s";// date, bucket, obj
private String mBucket;
private String mAccessKey;
private String mSecret;
public S3Api(Context c) {
PropertyUtil properties = new PropertyUtil(c.getResources());
mBucket = properties.getProperty(ConstantUtil.S3_BUCKET);
mAccessKey = properties.getProperty(ConstantUtil.S3_ACCESSKEY);
mSecret = properties.getProperty(ConstantUtil.S3_SECRET);
}
public String getEtag(String objectKey) throws IOException {
// Get date and signature
final String date = getDate();
final String payload = String.format(PAYLOAD_HEAD, date, mBucket, objectKey);
final String signature = getSignature(payload);
final URL url = new URL(String.format(URL, mBucket, objectKey));
HttpURLConnection conn = null;
String etag = null;
try {
conn = (HttpURLConnection) url.openConnection();
conn.setRequestProperty("Date", date);
conn.setRequestProperty("Authorization", "AWS " + mAccessKey + ":" + signature);
// Handle EOS bug in Android pre Jelly Bean: https://code.google.com/p/android/issues/detail?id=24672
conn.setRequestProperty("Accept-Encoding", "");
conn.setRequestMethod("HEAD");
if (conn.getResponseCode() == 200) {
etag = getEtag(conn);
}
return etag;
} finally {
if (conn != null) {
conn.disconnect();
}
}
}
public void syncFile(String objectKey, File dst) throws IOException {
final String etag = getEtag(objectKey);
if (etag == null) {
throw new HttpException("Could not read ETag from object: " + objectKey, 404);
}
if (dst.exists() && etag.equals(FileUtil.hexMd5(dst))) {
// No need to re-fetch the file. The integrity of the local copy has been verified
return;
}
get(objectKey, dst);
}
public void get(String objectKey, File dst) throws IOException {
// Get date and signature
final String date = getDate();
final String payload = String.format(PAYLOAD_GET, date, mBucket, objectKey);
final String signature = getSignature(payload);
final URL url = new URL(String.format(URL, mBucket, objectKey));
InputStream in = null;
OutputStream out = null;
HttpURLConnection conn = null;
try {
conn = (HttpURLConnection) url.openConnection();
conn.setRequestProperty("Date", date);
conn.setRequestProperty("Authorization", "AWS " + mAccessKey + ":" + signature);
in = new BufferedInputStream(conn.getInputStream());
out = new BufferedOutputStream(new FileOutputStream(dst));
HttpUtil.copyStream(in, out);
int status = conn.getResponseCode();
if (status != HttpURLConnection.HTTP_OK) {
throw new IOException("Status Code: " + status + ". Expected: 200 - OK");
}
} finally {
if (conn != null) {
conn.disconnect();
}
FileUtil.close(in);
FileUtil.close(out);
}
}
public boolean put(String objectKey, File file, String type, boolean isPublic) throws IOException {
// Calculate data size, up to 2 GB
final int size = file.length() < Integer.MAX_VALUE ? (int)file.length() : -1;
// Get date and signature
final byte[] rawMd5 = FileUtil.getMD5Checksum(file);
final String md5Base64 = Base64.encodeToString(rawMd5, Base64.NO_WRAP);
final String md5Hex = FileUtil.hexMd5(rawMd5);
final String date = getDate();
String payloadStr = isPublic ? PAYLOAD_PUT_PUBLIC : PAYLOAD_PUT_PRIVATE;
final String payload = String.format(payloadStr, md5Base64, type, date, mBucket, objectKey);
final String signature = getSignature(payload);
final URL url = new URL(String.format(URL, mBucket, objectKey));
InputStream in = null;
OutputStream out = null;
HttpURLConnection conn = null;
try {
conn = (HttpURLConnection) url.openConnection();
conn.setDoOutput(true);
if (size > 0) {
conn.setFixedLengthStreamingMode(size);
} else {
conn.setChunkedStreamingMode(0);
}
conn.setRequestMethod("PUT");
conn.setRequestProperty("Content-MD5", md5Base64);
conn.setRequestProperty("Content-Type", type);
conn.setRequestProperty("Date", date);
if (isPublic) {
// If we don't send this header, the object will be private by default
conn.setRequestProperty("x-amz-acl", "public-read");
}
conn.setRequestProperty("Authorization", "AWS " + mAccessKey + ":" + signature);
in = new BufferedInputStream(new FileInputStream(file));
out = new BufferedOutputStream(conn.getOutputStream());
HttpUtil.copyStream(in, out);
out.flush();
int status = conn.getResponseCode();
if (status != 200 && status != 201) {
Timber.e("Status Code: " + status + ". Expected: 200 or 201");
return false;
}
String etag = getEtag(conn);
if (!md5Hex.equals(etag)) {
Timber.e("ETag comparison failed. Response ETag: " + etag +
"Locally computed MD5: " + md5Hex);
return false;
}
Timber.d("File successfully uploaded: " + file.getName());
return true;
} finally {
if (conn != null) {
conn.disconnect();
}
FileUtil.close(in);
FileUtil.close(out);
}
}
private String getDate() {
final DateFormat df = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss ", Locale.US);
df.setTimeZone(TimeZone.getTimeZone("GMT"));
return df.format(new Date()) + "GMT";
}
private String getSignature(String payload) {
try {
Key signingKey = new SecretKeySpec(mSecret.getBytes(), "HmacSHA1");
Mac mac = Mac.getInstance("HmacSHA1");
mac.init(signingKey);
byte[] rawHmac = mac.doFinal(payload.getBytes());
return Base64.encodeToString(rawHmac, Base64.NO_WRAP);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
return null;
} catch (InvalidKeyException e) {
e.printStackTrace();
return null;
}
}
private String getEtag(HttpURLConnection conn) {
String etag = conn.getHeaderField("ETag");
return etag != null ? etag.replaceAll("\"", "") : null;// Remove quotes
}
}