Amazon S3 上传文件 SSL23_GET_SERVER_HELLO握手错误
题外话:今天偶尔来逛逛,发现我真是懒到家了。居然有半年前的留言我都没有来看过,真对不起留言的同学,希望他的问题已经解决了。
这两三天一直被亚马逊S3上传文件的问题困扰着,直到昨天晚上终于搞定了,工作群里一片欢腾,从客户端到服务器数位工程师卡在这个问题上抓耳挠腮了好几天,终于解决了,这就是所谓“光明总出现在最黑暗的时刻”吧,嘿嘿,非常开心,程序员真是容易满足啊。
途中搜索了很多互联上的方案,最终在stackoverflow的一个帖子里找到有价值的参考。而我们国内的站点上关于这方面的信息非常少,所以我来写这个随笔,希望为大家增加一点参考吧。
------------------------------------------------------------正式开始的分割线----------------------------------------------------------------------------
一开始我们的代码是这样,上传一个测试的mp4,我们的需求是文件size不超过10M:
1 File file = new File(Environment.getExternalStorageDirectory().getPath() + "/0.mp4"); 2 3 if(!file.exists()) 4 return; 5 if(!file.isFile()) 6 return; 7 try { 8 URL url = new URL("https://secv.s3.amazonaws.com/familytest10099/DFG092833/2016/01/27/10-38-07/0.mp4?AWSAccessKeyId=AKIAIGMMMZARXIK3ZDBA&Expires=1453948689&Signature=uJvHirNhOlCuB5z20NPkYc73qV8%3D");//从自家server签出的S3 地址 9 HttpsURLConnection connection = (HttpsURLConnection) url.openConnection(); 10 connection.setDoOutput(true); 11 connection.setRequestMethod("PUT"); 12 BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(connection.getOutputStream()); 13 FileInputStream fileInputStream = new FileInputStream(file);// 读取文件的数据。 14 byte[] bufer = new byte[1024]; 15 int len = 0, i = 0; 16 while ((len = fileInputStream.read(bufer)) != -1) { 17 bufferedOutputStream.write(bufer, 0, len); 18 } 19 20 bufferedOutputStream.flush(); 21 bufferedOutputStream.close(); 22 23 fileInputStream.close(); 24 25 int responseCode = connection.getResponseCode(); 26 Log.d("s3_ssltest","Service returned response code " + responseCode); 27 } catch (MalformedURLException e) { 28 // TODO Auto-generated catch block 29 e.printStackTrace(); 30 } catch (IOException e) { 31 // TODO Auto-generated catch block 32 e.printStackTrace(); 33 }
运行结果抛出异常:
javax.net.ssl.SSLHandshakeException: javax.net.ssl.SSLProtocolException: SSL handshake aborted: ssl=0x54b61708: Failure in SSL library, usually a protocol error
error:14077410:SSL routines:SSL23_GET_SERVER_HELLO:sslv3 alert handshake failure (external/openssl/ssl/s23_clnt.c:741 0x52732cfc:0x00000000)
at com.android.org.conscrypt.OpenSSLSocketImpl.startHandshake(OpenSSLSocketImpl.java:448)
at com.android.okhttp.Connection.upgradeToTls(Connection.java:146)
at com.android.okhttp.Connection.connect(Connection.java:107)
at com.android.okhttp.internal.http.HttpEngine.connect(HttpEngine.java:294)
at com.android.okhttp.internal.http.HttpEngine.sendSocketRequest(HttpEngine.java:255)
at com.android.okhttp.internal.http.HttpEngine.sendRequest(HttpEngine.java:206)
at com.android.okhttp.internal.http.HttpURLConnectionImpl.execute(HttpURLConnectionImpl.java:345)
at com.android.okhttp.internal.http.HttpURLConnectionImpl.getResponse(HttpURLConnectionImpl.java:296)
at com.android.okhttp.internal.http.HttpURLConnectionImpl.getResponseCode(HttpURLConnectionImpl.java:503)
at com.android.okhttp.internal.http.HttpsURLConnectionImpl.getResponseCode(HttpsURLConnectionImpl.java:136)
at com.spde.switchbox.manager.secu.SecuThread.a(Unknown Source)
at com.spde.switchbox.manager.secu.SecuThread.run(Unknown Source)
Caused by: javax.net.ssl.SSLProtocolException: SSL handshake aborted: ssl=0x54b61708: Failure in SSL library, usually a protocol error
error:14077410:SSL routines:SSL23_GET_SERVER_HELLO:sslv3 alert handshake failure (external/openssl/ssl/s23_clnt.c:741 0x52732cfc:0x00000000)
at com.android.org.conscrypt.NativeCrypto.SSL_do_handshake(Native Method)
at com.android.org.conscrypt.OpenSSLSocketImpl.startHandshake(OpenSSLSocketImpl.java:405)
... 11 more
出错原因是亚马逊s3 服务器端禁用了SSLv3,解决方法是自己extends一个SSLSocketFactory把SSLv3从默认的protocol中remove掉。重写的SSLSocketFactory见附件NoSSLv3Factory.java。
在创建connection之前先调用:
HttpsURLConnection.setDefaultSSLSocketFactory(new NoSSLv3Factory());
这样修改之后SSL的问题解决了,但又抛出一个异常
javax.net.ssl.SSLException: Write error: ssl=0x5ab6f5f8: I/O error during system call, Connection reset by peer
at org.apache.harmony.xnet.provider.jsse.NativeCrypto.SSL_write(Native Method)
at org.apache.harmony.xnet.provider.jsse.OpenSSLSocketImpl$SSLOutputStream.write(OpenSSLSocketImpl.java:693)
at java.io.ByteArrayOutputStream.writeTo(ByteArrayOutputStream.java:231)
at libcore.net.http.RetryableOutputStream.writeToSocket(RetryableOutputStream.java:70)
at libcore.net.http.HttpEngine.readResponse(HttpEngine.java:806)
at libcore.net.http.HttpURLConnectionImpl.getResponse(HttpURLConnectionImpl.java:274)
at libcore.net.http.HttpURLConnectionImpl.getResponseCode(HttpURLConnectionImpl.java:486)
at libcore.net.http.HttpsURLConnectionImpl.getResponseCode(HttpsURLConnectionImpl.java:134)
at com.example.test.MainActivity$SSLTestRunnable.run(MainActivity.java:73)
at java.lang.Thread.run(Thread.java:856)
原因是http header中没有指定content-type,传输格式是stream
connection.setRequestProperty("Content-Type", "application/octet-stream");
这个异常也解决了。访问成功,但是返回403 Forbidden,网上搜到很多解释说是客户端的时区或者系统时间不对,因为S3上签出的url是有expire date的。但我们出问题的原因是因为服务器签出这个url的时候没有指定Content-type , 又是Content-type的问题,修改签出地址的代码如下:
public static String createUploadUrl(String bucket, String objKey, Date expire){
String url = s3Client.generatePresignedUrl(new GeneratePresignedUrlRequest(bucket, objKey).
withMethod(HttpMethod.PUT).
withContentType("application/octet-stream").
withExpiration(expire)
).toString();
return url;
}
ok,这次终于收到回复200 ok了,视频也上传成功。
困扰三天的问题就这样解决了。总结一下:一个复杂迷茫各种想不通的问题,最终解决的时候往往修改不了几行代码。
额外的说明:我们出问题的平台android版本是4.4.3,openSSL lib的版本是1.0.1e。网上搜集到的信息,SSL23_GET_SERVER_HELLO:sslv3 alert handshake failure 这种问题通常出现在Android 4.x上。
最后是 NoSSLv3Factory.java 的源码