package railo.commons.io.res.type.s3;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.TimeZone;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.collections.map.ReferenceMap;
import org.xml.sax.SAXException;
import railo.commons.io.CharsetUtil;
import railo.commons.lang.Md5;
import railo.commons.lang.StringUtil;
import railo.commons.net.URLEncoder;
import railo.commons.net.http.Entity;
import railo.commons.net.http.HTTPResponse;
import railo.commons.net.http.Header;
import railo.commons.net.http.httpclient3.HTTPEngine3Impl;
import railo.loader.util.Util;
import railo.runtime.config.Constants;
import railo.runtime.engine.ThreadLocalPageContext;
import railo.runtime.exp.PageException;
import railo.runtime.op.Caster;
import railo.runtime.type.dt.DateTime;
public final class S3 implements S3Constants {
private static final String DEFAULT_URL="s3.amazonaws.com";
private String secretAccessKey;
private String accessKeyId;
private TimeZone timezone;
private String host;
private static final Map<String,S3Info> infos=new ReferenceMap();
private static final Map<String,AccessControlPolicy> acps=new ReferenceMap();
public static final int MAX_REDIRECT = 15;
public String toString(){
return "secretAccessKey:"+secretAccessKey+";accessKeyId:"+accessKeyId+";host:"+host+";timezone:"+
(timezone==null?"":timezone.getID());
}
public String hash() {
try {
return Md5.getDigestAsString(toString());
} catch (IOException e) {
return null;
}
}
public S3(String secretAccessKey, String accessKeyId,TimeZone tz) {
host=DEFAULT_URL;
this.secretAccessKey = secretAccessKey;
this.accessKeyId = accessKeyId;
this.timezone = tz;
//testFinal();
}
public S3() {
//testFinal();
}
/**
* @return the secretAccessKey
* @throws S3Exception
*/
String getSecretAccessKeyValidate() throws S3Exception {
if(StringUtil.isEmpty(secretAccessKey))
throw new S3Exception("secretAccessKey is not defined, define in "+Constants.APP_CFC+" (s3.awsSecretKey) or as part of the path.");
return secretAccessKey;
}
/**
* @return the accessKeyId
* @throws S3Exception
*/
String getAccessKeyIdValidate() throws S3Exception {
if(StringUtil.isEmpty(accessKeyId))
throw new S3Exception("accessKeyId is not defined, define in "+Constants.APP_CFC+" (this.s3.accessKeyId) or as part of the path.");
return accessKeyId;
}
String getSecretAccessKey() {
return secretAccessKey;
}
/**
* @return the accessKeyId
* @throws S3Exception
*/
String getAccessKeyId() {
return accessKeyId;
}
/**
* @return the tz
*/
TimeZone getTimeZone() {
if(timezone==null)timezone=ThreadLocalPageContext.getTimeZone();
return timezone;
}
private static byte[] HMAC_SHA1(String key, String message,String charset) throws UnsupportedEncodingException, NoSuchAlgorithmException, InvalidKeyException {
SecretKeySpec sks = new SecretKeySpec(key.getBytes(charset),"HmacSHA1");
Mac mac = Mac.getInstance(sks.getAlgorithm());
mac.init(sks);
mac.update(message.getBytes(charset));
return mac.doFinal();
}
private static String createSignature(String str, String secretAccessKey,String charset) throws InvalidKeyException, NoSuchAlgorithmException, IOException {
//str=StringUtil.replace(str, "\\n", String.valueOf((char)10), false);
byte[] digest = HMAC_SHA1(secretAccessKey,str,charset);
try {
return Caster.toB64(digest);
} catch (Throwable t) {
throw new IOException(t.getMessage());
}
}
public InputStream listBucketsRaw() throws MalformedURLException, IOException, InvalidKeyException, NoSuchAlgorithmException {
String dateTimeString = Util.toHTTPTimeString();
String signature = createSignature("GET\n\n\n"+dateTimeString+"\n/", getSecretAccessKeyValidate(), "iso-8859-1");
HTTPResponse rsp = HTTPEngine3Impl.get(new URL("http://"+host), null, null, -1,MAX_REDIRECT, null, "Railo", null,
new Header[]{
header("Date",dateTimeString),
header("Authorization","AWS "+getAccessKeyIdValidate()+":"+signature)
}
);
return rsp.getContentAsStream();
}
public HTTPResponse head(String bucketName, String objectName) throws MalformedURLException, IOException, InvalidKeyException, NoSuchAlgorithmException {
bucketName=checkBucket(bucketName);
boolean hasObj=!StringUtil.isEmpty(objectName);
if(hasObj)objectName=checkObjectName(objectName);
String dateTimeString = Util.toHTTPTimeString();
String signature = createSignature("HEAD\n\n\n"+dateTimeString+"\n/"+bucketName+"/"+(hasObj?objectName:""), getSecretAccessKeyValidate(), "iso-8859-1");
List<Header> headers=new ArrayList<Header>();
headers.add(header("Date",dateTimeString));
headers.add(header("Authorization","AWS "+getAccessKeyIdValidate()+":"+signature));
headers.add(header("Host",bucketName+"."+host));
String strUrl="http://"+bucketName+"."+host+"/";
//if(Util.hasUpperCase(bucketName))strUrl="http://"+host+"/"+bucketName+"/";
if(hasObj) {
strUrl+=objectName;
}
HTTPResponse method = HTTPEngine3Impl.head(new URL(strUrl), null, null, -1,MAX_REDIRECT, null, "Railo", null,headers.toArray(new Header[headers.size()]));
return method;
}
public InputStream aclRaw(String bucketName, String objectName) throws MalformedURLException, IOException, InvalidKeyException, NoSuchAlgorithmException {
bucketName=checkBucket(bucketName);
boolean hasObj=!StringUtil.isEmpty(objectName);
if(hasObj)objectName=checkObjectName(objectName);
String dateTimeString = Util.toHTTPTimeString();
String signature = createSignature("GET\n\n\n"+dateTimeString+"\n/"+bucketName+"/"+(hasObj?objectName:"")+"?acl", getSecretAccessKeyValidate(), "iso-8859-1");
List<Header> headers=new ArrayList<Header>();
headers.add(header("Date",dateTimeString));
headers.add(header("Authorization","AWS "+getAccessKeyIdValidate()+":"+signature));
headers.add(header("Host",bucketName+"."+host));
String strUrl="http://"+bucketName+"."+host+"/";
//if(Util.hasUpperCase(bucketName))strUrl="http://"+host+"/"+bucketName+"/";
if(hasObj) {
strUrl+=objectName;
}
strUrl+="?acl";
HTTPResponse method = HTTPEngine3Impl.get(new URL(strUrl), null, null, -1,MAX_REDIRECT, null, "Railo", null,headers.toArray(new Header[headers.size()]));
return method.getContentAsStream();
}
public AccessControlPolicy getAccessControlPolicy(String bucketName, String objectName) throws InvalidKeyException, MalformedURLException, NoSuchAlgorithmException, IOException, SAXException {
InputStream raw = aclRaw(bucketName,objectName);
//print.o(IOUtil.toString(raw, null));
ACLFactory factory=new ACLFactory(raw, this);
return factory.getAccessControlPolicy();
}
public void setAccessControlPolicy(String bucketName, String objectName,AccessControlPolicy acp) throws IOException, InvalidKeyException, NoSuchAlgorithmException, SAXException {
bucketName=checkBucket(bucketName);
boolean hasObj=!StringUtil.isEmpty(objectName);
if(hasObj)objectName=checkObjectName(objectName);
Entity re = HTTPEngine3Impl.getByteArrayEntity(acp.toXMLString().getBytes(CharsetUtil.ISO88591),"text/html");
String dateTimeString = Util.toHTTPTimeString();
String cs = "PUT\n\n"+re.contentType()+"\n"+dateTimeString+"\n/"+bucketName+"/"+(hasObj?objectName:"")+"?acl";
String signature = createSignature(cs, getSecretAccessKeyValidate(), "iso-8859-1");
Header[] headers = new Header[]{
header("Content-Type",re.contentType()),
header("Content-Length",Long.toString(re.contentLength())),
header("Date",dateTimeString),
header("Authorization","AWS "+getAccessKeyIdValidate()+":"+signature),
};
String strUrl="http://"+bucketName+"."+host+"/";
if(hasObj) {
strUrl+=objectName;
}
strUrl+="?acl";
HTTPResponse method = HTTPEngine3Impl.put(new URL(strUrl), null, null, -1,MAX_REDIRECT, null,
"Railo", null,headers,re);
if(method.getStatusCode()!=200){
new ErrorFactory(method.getContentAsStream());
}
}
public InputStream listContentsRaw(String bucketName,String prefix,String marker,int maxKeys) throws MalformedURLException, IOException, InvalidKeyException, NoSuchAlgorithmException {
bucketName=checkBucket(bucketName);
String dateTimeString = Util.toHTTPTimeString();
String signature = createSignature("GET\n\n\n"+dateTimeString+"\n/"+bucketName+"/", getSecretAccessKeyValidate(), "iso-8859-1");
List<Header> headers=new ArrayList<Header>();
headers.add(header("Date",dateTimeString));
headers.add(header("Authorization","AWS "+getAccessKeyIdValidate()+":"+signature));
headers.add(header("Host",bucketName+"."+host));
String strUrl="http://"+bucketName+"."+host+"/";
if(Util.hasUpperCase(bucketName))strUrl="http://"+host+"/"+bucketName+"/";
char amp='?';
if(!Util.isEmpty(prefix)){
strUrl+=amp+"prefix="+encodeEL(prefix);
amp='&';
}
if(!Util.isEmpty(marker)) {
strUrl+=amp+"marker="+encodeEL(marker);
amp='&';
}
if(maxKeys!=-1) {
strUrl+=amp+"max-keys="+maxKeys;
amp='&';
}
HTTPResponse method = HTTPEngine3Impl.get(new URL(strUrl), null, null, -1,MAX_REDIRECT, null, "Railo", null,headers.toArray(new Header[headers.size()]));
return method.getContentAsStream();
}
public Content[] listContents(String bucketName,String prefix) throws InvalidKeyException, MalformedURLException, NoSuchAlgorithmException, IOException, SAXException {
String marker=null,last=null;
ContentFactory factory;
Content[] contents;
List<Content[]> list = new ArrayList<Content[]>();
int size=0;
while(true) {
factory = new ContentFactory(listContentsRaw(bucketName, prefix, marker, -1),this);
contents = factory.getContents();
list.add(contents);
size+=contents.length;
if(factory.isTruncated() && contents.length>0) {
last=marker;
marker=contents[contents.length-1].getKey();
if(marker.equals(last))break;
}
else break;
}
if(list.size()==1) return list.get(0);
if(list.size()==0) return new Content[0];
Content[] rtn=new Content[size];
Iterator<Content[]> it = list.iterator();
int index=0;
while(it.hasNext()) {
contents=it.next();
for(int i=0;i<contents.length;i++) {
rtn[index++]=contents[i];
}
}
return rtn;
}
public Content[] listContents(String bucketName,String prefix,String marker,int maxKeys) throws InvalidKeyException, MalformedURLException, NoSuchAlgorithmException, IOException, SAXException {
InputStream raw = listContentsRaw(bucketName, prefix, marker, maxKeys);
//print.o(IOUtil.toString(raw, null));
ContentFactory factory = new ContentFactory(raw,this);
return factory.getContents();
}
public Bucket[] listBuckets() throws InvalidKeyException, MalformedURLException, NoSuchAlgorithmException, IOException, SAXException {
InputStream raw = listBucketsRaw();
//print.o(IOUtil.toString(raw, null));
BucketFactory factory = new BucketFactory(raw,this);
return factory.getBuckets();
}
public void putBuckets(String bucketName,int acl, int storage) throws IOException, InvalidKeyException, NoSuchAlgorithmException, SAXException {
String strXML = "";
if(storage==STORAGE_EU) {
strXML="<CreateBucketConfiguration><LocationConstraint>EU</LocationConstraint></CreateBucketConfiguration>";
}
byte[] barr = strXML.getBytes(CharsetUtil.ISO88591);
put(bucketName, null, acl,HTTPEngine3Impl.getByteArrayEntity(barr,"text/html"));
}
/*public void putObject(String bucketName,String objectName,int acl,Resource res) throws IOException, InvalidKeyException, NoSuchAlgorithmException, PageException, SAXException, EncoderException {
String contentType = IOUtil.getMimeType(res, "application");
InputStream is = null;
try {
is = res.getInputStream();
put(bucketName, objectName, acl, is, contentType);
}
finally {
IOUtil.closeEL(is);
}
}*/
/*
public void put(String bucketName,String objectName,int acl, InputStream is,long length, String contentType) throws IOException, InvalidKeyException, NoSuchAlgorithmException, PageException, SAXException, EncoderException {
put(bucketName, objectName, acl, HTTPUtil.toRequestEntity(is),length, contentType);
}*/
public void put(String bucketName,String objectName,int acl, Entity re) throws IOException, InvalidKeyException, NoSuchAlgorithmException, SAXException {
bucketName=checkBucket(bucketName);
objectName=checkObjectName(objectName);
String dateTimeString = Util.toHTTPTimeString();
// Create a canonical string to send based on operation requested
String cs = "PUT\n\n"+re.contentType()+"\n"+dateTimeString+"\nx-amz-acl:"+toStringACL(acl)+"\n/"+bucketName+"/"+objectName;
String signature = createSignature(cs, getSecretAccessKeyValidate(), "iso-8859-1");
Header[] headers = new Header[]{
header("Content-Type",re.contentType()),
header("Content-Length",Long.toString(re.contentLength())),
header("Date",dateTimeString),
header("x-amz-acl",toStringACL(acl)),
header("Authorization","AWS "+getAccessKeyIdValidate()+":"+signature),
};
String strUrl="http://"+bucketName+"."+host+"/"+objectName;
if(Util.hasUpperCase(bucketName))strUrl="http://"+host+"/"+bucketName+"/"+objectName;
HTTPResponse method = HTTPEngine3Impl.put(new URL(strUrl), null, null, -1,MAX_REDIRECT, null,
"Railo", null,headers,re);
if(method.getStatusCode()!=200){
new ErrorFactory(method.getContentAsStream());
}
}
public HttpURLConnection preput(String bucketName,String objectName,int acl, String contentType) throws IOException, InvalidKeyException, NoSuchAlgorithmException {
bucketName=checkBucket(bucketName);
objectName=checkObjectName(objectName);
String dateTimeString = Util.toHTTPTimeString();
// Create a canonical string to send based on operation requested
String cs = "PUT\n\n"+contentType+"\n"+dateTimeString+"\nx-amz-acl:"+toStringACL(acl)+"\n/"+bucketName+"/"+objectName;
String signature = createSignature(cs, getSecretAccessKeyValidate(), "iso-8859-1");
String strUrl="http://"+bucketName+"."+host+"/"+objectName;
if(Util.hasUpperCase(bucketName))strUrl="http://"+host+"/"+bucketName+"/"+objectName;
URL url = new URL(strUrl);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("PUT");
conn.setFixedLengthStreamingMode(227422142);
conn.setDoOutput(true);
conn.setUseCaches(false);
conn.setRequestProperty("CONTENT-TYPE", contentType);
conn.setRequestProperty("USER-AGENT", "S3 Resource");
//conn.setRequestProperty("Transfer-Encoding", "chunked" );
conn.setRequestProperty("Date", dateTimeString);
conn.setRequestProperty("x-amz-acl", toStringACL(acl));
conn.setRequestProperty("Authorization", "AWS "+getAccessKeyIdValidate()+":"+signature);
return conn;
}
public String getObjectLink(String bucketName,String objectName,int secondsValid) throws InvalidKeyException, NoSuchAlgorithmException, IOException {
bucketName=checkBucket(bucketName);
objectName=checkObjectName(objectName);
long epoch = (System.currentTimeMillis()/1000)+(secondsValid);
String cs = "GET\n\n\n"+epoch+"\n/"+bucketName+"/"+objectName;
String signature = createSignature(cs, getSecretAccessKeyValidate(), "iso-8859-1");
String strUrl="http://"+bucketName+"."+host+"/"+objectName;
if(Util.hasUpperCase(bucketName))strUrl="http://"+host+"/"+bucketName+"/"+objectName;
return strUrl+"?AWSAccessKeyId="+getAccessKeyIdValidate()+"&Expires="+epoch+"&Signature="+signature;
}
public InputStream getInputStream(String bucketName,String objectName) throws InvalidKeyException, NoSuchAlgorithmException, IOException, SAXException {
return getData(bucketName, objectName).getContentAsStream();
}
public Map<String, String> getMetadata(String bucketName,String objectName) throws InvalidKeyException, NoSuchAlgorithmException, IOException, SAXException {
HTTPResponse method = getData(bucketName, objectName);
Header[] headers = method.getAllHeaders();
Map<String,String> rtn=new HashMap<String, String>();
String name;
if(headers!=null)for(int i=0;i<headers.length;i++){
name=headers[i].getName();
if(name.startsWith("x-amz-meta-"))
rtn.put(name.substring(11), headers[i].getValue());
}
return rtn;
}
private HTTPResponse getData(String bucketName,String objectName) throws InvalidKeyException, NoSuchAlgorithmException, IOException, SAXException {
bucketName=checkBucket(bucketName);
objectName=checkObjectName(objectName);
String dateTimeString = Util.toHTTPTimeString();
//long epoch = (System.currentTimeMillis()/1000)+6000;
String cs = "GET\n\n\n"+dateTimeString+"\n/"+bucketName+"/"+objectName;
String signature = createSignature(cs, getSecretAccessKeyValidate(), "iso-8859-1");
String strUrl="http://"+bucketName+"."+host+"/"+objectName;
if(Util.hasUpperCase(bucketName))strUrl="http://"+host+"/"+bucketName+"/"+objectName;
URL url = new URL(strUrl);
HTTPResponse method = HTTPEngine3Impl.get(url, null, null, -1,MAX_REDIRECT, null, "Railo", null,
new Header[]{
header("Date",dateTimeString),
header("Host",bucketName+"."+host),
header("Authorization","AWS "+getAccessKeyIdValidate()+":"+signature)
}
);
if(method.getStatusCode()!=200)
new ErrorFactory(method.getContentAsStream());
return method;
}
public void delete(String bucketName, String objectName) throws IOException, InvalidKeyException, NoSuchAlgorithmException, SAXException {
bucketName=checkBucket(bucketName);
objectName = checkObjectName(objectName);
String dateTimeString = Util.toHTTPTimeString();
// Create a canonical string to send based on operation requested
String cs ="DELETE\n\n\n"+dateTimeString+"\n/"+bucketName+"/"+objectName;
//print.out(cs);
String signature = createSignature(cs, getSecretAccessKeyValidate(), "iso-8859-1");
Header[] headers =new Header[]{
header("Date",dateTimeString),
header("Authorization","AWS "+getAccessKeyIdValidate()+":"+signature),
};
String strUrl="http://"+bucketName+"."+host+"/"+objectName;
if(Util.hasUpperCase(bucketName))strUrl="http://"+host+"/"+bucketName+"/"+objectName;
HTTPResponse rsp = HTTPEngine3Impl.delete(new URL(strUrl), null, null, -1,MAX_REDIRECT, null, "Railo", null,headers);
if(rsp.getStatusCode()!=200)
new ErrorFactory(rsp.getContentAsStream());
}
// --------------------------------
public static String toStringACL(int acl) throws S3Exception {
switch(acl) {
case ACL_AUTH_READ:return "authenticated-read";
case ACL_PUBLIC_READ:return "public-read";
case ACL_PRIVATE:return "private";
case ACL_PUBLIC_READ_WRITE:return "public-read-write";
}
throw new S3Exception("invalid acl definition");
}
public static String toStringStorage(int storage) throws S3Exception {
String s = toStringStorage(storage, null);
if(s==null)
throw new S3Exception("invalid storage definition");
return s;
}
public static String toStringStorage(int storage, String defaultValue) {
switch(storage) {
case STORAGE_EU:return "eu";
case STORAGE_US:return "us";
case STORAGE_US_WEST:return "us-west";
}
return defaultValue;
}
public static int toIntACL(String acl) throws S3Exception {
acl=acl.toLowerCase().trim();
if("public-read".equals(acl)) return ACL_PUBLIC_READ;
if("private".equals(acl)) return ACL_PRIVATE;
if("public-read-write".equals(acl)) return ACL_PUBLIC_READ_WRITE;
if("authenticated-read".equals(acl)) return ACL_AUTH_READ;
if("public_read".equals(acl)) return ACL_PUBLIC_READ;
if("public_read_write".equals(acl)) return ACL_PUBLIC_READ_WRITE;
if("authenticated_read".equals(acl)) return ACL_AUTH_READ;
if("publicread".equals(acl)) return ACL_PUBLIC_READ;
if("publicreadwrite".equals(acl)) return ACL_PUBLIC_READ_WRITE;
if("authenticatedread".equals(acl)) return ACL_AUTH_READ;
throw new S3Exception("invalid acl value, valid values are [public-read, private, public-read-write, authenticated-read]");
}
public static int toIntStorage(String storage) throws S3Exception {
int s=toIntStorage(storage,-1);
if(s==-1)
throw new S3Exception("invalid storage value, valid values are [eu,us,us-west]");
return s;
}
public static int toIntStorage(String storage, int defaultValue) {
storage=storage.toLowerCase().trim();
if("us".equals(storage)) return STORAGE_US;
if("usa".equals(storage)) return STORAGE_US;
if("eu".equals(storage)) return STORAGE_EU;
if("u.s.".equals(storage)) return STORAGE_US;
if("u.s.a.".equals(storage)) return STORAGE_US;
if("europe.".equals(storage)) return STORAGE_EU;
if("euro.".equals(storage)) return STORAGE_EU;
if("e.u.".equals(storage)) return STORAGE_EU;
if("united states of america".equals(storage)) return STORAGE_US;
if("us-west".equals(storage)) return STORAGE_US_WEST;
if("usa-west".equals(storage)) return STORAGE_US_WEST;
return defaultValue;
}
private String checkObjectName(String objectName) throws UnsupportedEncodingException {
if(Util.isEmpty(objectName)) return "";
if(objectName.startsWith("/"))objectName=objectName.substring(1);
return encode(objectName);
}
private String checkBucket(String name) {
/*if(!Decision.isVariableName(name))
throw new S3Exception("invalid bucket name definition ["+name+"], name should only contain letters, digits, dashes and underscores");
if(name.length()<3 || name.length()>255)
throw new S3Exception("invalid bucket name definition ["+name+"], the length of a bucket name must be between 3 and 255");
*/
return encodeEL(name);
}
private String encodeEL(String name) {
try {
return encode(name);
} catch (UnsupportedEncodingException e) {
return name;
}
}
private String encode(String name) throws UnsupportedEncodingException {
return URLEncoder.encode(name,"UTF-8");
}
/**
* @param secretAccessKey the secretAccessKey to set
*/
public void setSecretAccessKey(String secretAccessKey) {
this.secretAccessKey = secretAccessKey;
}
/**
* @param accessKeyId the accessKeyId to set
*/
public void setAccessKeyId(String accessKeyId) {
this.accessKeyId = accessKeyId;
}
/**
* @param url the url to set
*/
public void setHost(String host) {
this.host=host;
}
public String getHost() {
return host;
}
public S3Info getInfo(String path) {
return infos.get(toKey(path));
}
public void setInfo(String path,S3Info info) {
infos.put(toKey(path),info);
}
public AccessControlPolicy getACP(String path) {
return acps.get(toKey(path));
}
public void setACP(String path,AccessControlPolicy acp) {
acps.put(toKey(path),acp);
}
public void releaseCache(String path) {
Object k = toKey(path);
infos.remove(k);
acps.remove(k);
}
private String toKey(String path) {
return toString()+":"+path.toLowerCase();
}
private Header header(String name, String value) {
return HTTPEngine3Impl.header(name,value);
}
public static DateTime toDate(String strDate, TimeZone tz) throws PageException {
if(strDate.endsWith("Z"))
strDate=strDate.substring(0,strDate.length()-1);
return Caster.toDate(strDate, tz);
}
}