在Android设备上使用NanoHttpd创建HTTPS服务器

11

我正在尝试使用NanoHttpd在Android设备上运行HTTPS服务器(我的最终目标是在Android上运行WSS服务器)。我已经成功地在Android上使用NanoHttpd运行了HTTP服务器和WebSocket。我使用以下命令在Mac上生成密钥,并将其复制到我的设备上:

keytool -genkey -keystore key.keystore -storepass keypass -storetype BKS -provider org.bouncycastle.jce.provider.BouncyCastleProvider

我编写了以下代码:

keyStore = KeyStore.getInstance("BKS");
keyStore.load(stream, keyStorePwd.toCharArray());
keyManagerFactory = KeyManagerFactory
           .getInstance(KeyManagerFactory.getDefaultAlgorithm());
keyManagerFactory.init(keyStore, keyStorePwd.toCharArray());
SSLContext sc = SSLContext.getInstance("TLS");
sc.init(keyManagerFactory.getKeyManagers(), null, null);
server.makeSecure(sc.getServerSocketFactory());
server.start();

我在Chrome 38和42上测试了启用了“Minimum SSL/TLS”标志的“SSLv3”。但当我想连接服务器时,我一直收到“ERR_SSL_VERSION_OR_CIPHER_MISMATCH”错误。

我尝试了不同的协议实例(SSL/TLS),在多台机器和浏览器上进行了尝试。我尝试了NanoHttpd SSLServerSocketFactory方法,但是错误仍然存在。

我已经查看了一些示例,包括: https://github.com/NanoHttpd/nanohttpd/issues/139

有人对此有任何评论吗?


你在哪个Android版本上执行服务器? - Robert
这是一部LG VS985手机,搭载Android 5.0.1版本。 - user3183066
你搞定了吗? - KVISH
4个回答

10

经过几个小时的辛勤劳作,我终于搞定了!

这是我的(可用的)代码:

// I placed this block right below my class declaration so it runs
// as soon as the class is defined. (this is for localhost testing ONLY!!!!)    
static {
    //for localhost testing only
    javax.net.ssl.HttpsURLConnection.setDefaultHostnameVerifier(
    new javax.net.ssl.HostnameVerifier(){

        public boolean verify(String hostname,
                javax.net.ssl.SSLSession sslSession) {
            if (hostname.equals("localhost")) {
                return true;
            }
            return false;
        }
    });
}

// then in an init function, I set it all up here
this.secureAppServer = new NanoHTTPD(9043);
File f =new File("src/main/resources/key001.jks");
System.setProperty("javax.net.ssl.trustStore", f.getAbsolutePath());
this.secureAppServer.setServerSocketFactory(new SecureServerSocketFactory(NanoHTTPD.makeSSLSocketFactory("/" +f.getName(), "myawesomepassword".toCharArray()), null));

this.secureAppServer.start();

这里是实际的NanoHttpd测试用例,它精确地说明了如何使用Nano风格完成。

package fi.iki.elonen;

import java.io.File;

/*
 * #%L
 * NanoHttpd-Core
 * %%
 * Copyright (C) 2012 - 2015 nanohttpd
 * %%
 * Redistribution and use in source and binary forms, with or without modification,
 * are permitted provided that the following conditions are met:
 * 
 * 1. Redistributions of source code must retain the above copyright notice, this
 *    list of conditions and the following disclaimer.
 * 
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 * 
 * 3. Neither the name of the nanohttpd nor the names of its contributors
 *    may be used to endorse or promote products derived from this software without
 *    specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
 * OF THE POSSIBILITY OF SUCH DAMAGE.
 * #L%
 */

import java.io.IOException;
import java.util.Arrays;

import javax.net.ssl.SSLServerSocket;

import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.methods.HttpTrace;
import org.apache.http.impl.client.DefaultHttpClient;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;

import fi.iki.elonen.NanoHTTPD.SecureServerSocketFactory;

public class SSLServerSocketFactoryTest extends HttpServerTest {

    @Test
    public void testSSLConnection() throws ClientProtocolException, IOException {
        DefaultHttpClient httpclient = new DefaultHttpClient();
        HttpTrace httphead = new HttpTrace("https://localhost:9043/index.html");
        HttpResponse response = httpclient.execute(httphead);
        HttpEntity entity = response.getEntity();
        Assert.assertEquals(200, response.getStatusLine().getStatusCode());

        Assert.assertEquals(9043, this.testServer.getListeningPort());
        Assert.assertTrue(this.testServer.isAlive());
    }

    @Test
    public void testCreatePassesTheProtocolsToServerSocket() throws IOException {
        // first find the supported protocols
        SecureServerSocketFactory secureServerSocketFactory = new SecureServerSocketFactory(NanoHTTPD.makeSSLSocketFactory("/keystore.jks", "password".toCharArray()), null);
        SSLServerSocket socket = (SSLServerSocket) secureServerSocketFactory.create();
        String[] protocols = socket.getSupportedProtocols();

        // remove one element from supported protocols
        if (protocols.length > 0) {
            protocols = Arrays.copyOfRange(protocols, 0, protocols.length - 1);
        }

        // test
        secureServerSocketFactory = new SecureServerSocketFactory(NanoHTTPD.makeSSLSocketFactory("/keystore.jks", "password".toCharArray()), protocols);
        socket = (SSLServerSocket) secureServerSocketFactory.create();
        Assert.assertArrayEquals("Enabled protocols specified in the factory were not set to the socket.", protocols, socket.getEnabledProtocols());
    }

    @Before
    public void setUp() throws Exception {
        System.setProperty("javax.net.ssl.trustStore", new File("src/test/resources/keystore.jks").getAbsolutePath());
        this.testServer = new TestServer(9043);
        this.testServer.setServerSocketFactory(new SecureServerSocketFactory(NanoHTTPD.makeSSLSocketFactory("/keystore.jks", "password".toCharArray()), null));
        this.tempFileManager = new TestTempFileManager();
        this.testServer.start();
        try {
            long start = System.currentTimeMillis();
            Thread.sleep(100L);
            while (!this.testServer.wasStarted()) {
                Thread.sleep(100L);
                if (System.currentTimeMillis() - start > 2000) {
                    Assert.fail("could not start server");
                }
            }
        } catch (InterruptedException e) {
        }
    }

    @After
    public void tearDown() {
        this.testServer.stop();
    }
}

1
注意:Nano尝试将密钥库作为资源流加载,因此KeyStore必须驻留在src/main/resources中(这就是我将其设置为硬路径的原因)。看起来这是一个安全特性 ;)。 - Decoded
2
注意不要公开发布你的密码。 - Denys Vitali
1
什么?src/main/resources/key001.jks在构建时是有效的,但在运行时无效。 - Jeffrey Blattman
1
谢谢你的提醒 @DenysVitali,幸运的是这些不是真正的生产密码 ;),只是为了测试目的。 - Decoded
@JeffreyBlattman,你是否对Java安装进行了任何更改(例如关于安全JAR文件的更改),并且是否已正确安装了您的证书在您的rooted Android设备上? - Decoded
显示剩余3条评论

4
其他答案对我没有用。我必须使用KeyStore Explorer创建一个BKS-V1密钥库,并将其保存到Android资产文件夹中,命名为“ keystore.bks”。或者,您也可以使用以下代码创建KeyStore文件,然后仅打开它并将其类型更改为BKS-V1,使用KeyStore Explorer
keytool -genkey -keyalg RSA -alias selfsigned -keystore keystore.bks -storepass myKeyStorePass -validity 360 -keysize 2048 -ext SAN=DNS:localhost,IP:127.0.0.1  -validity 9999 

我使用了下面的代码使其工作。
package com.example.myappname

import java.io.IOException;
import java.io.InputStream;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;

import javax.net.ssl.KeyManagerFactory;

import fi.iki.elonen.NanoHTTPD;

public class Server extends NanoHTTPD {
    public Server(int port) throws IOException, KeyStoreException, CertificateException, NoSuchAlgorithmException, UnrecoverableKeyException {
        super(port);
        KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
        InputStream keyStoreStream = context.get().getAssets().open("keystore.bks");
        keyStore.load(keyStoreStream, "myKeyStorePass".toCharArray());
        KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
        keyManagerFactory.init(keyStore, "myCertificatePass".toCharArray());
        makeSecure(NanoHTTPD.makeSSLSocketFactory(keyStore, keyManagerFactory), null);
    }

    @Override
    public Response serve(IHTTPSession session) {
    }
}

只需编写以下代码,您的Android设备就可以运行HTTPS服务器。

Server server = new Server(8080);
server.start(NanoHTTPD.SOCKET_READ_TIMEOUT, false);

这段代码得益于以下Github问题评论中提供的示例。 https://github.com/NanoHttpd/nanohttpd/issues/535#issuecomment-479269044

Ravi Patel,我使用这个来创建https服务器,但是当我从Web应用程序中请求表单时,我遇到了握手失败的问题,因为Web应用程序没有客户端证书,只有单向认证。我们如何只使用服务器端证书实现https? - wasim
@wasim 你可以尝试在客户端禁用证书验证。这可能会解决问题。 - Ravi Patel
客户端Web应用程序服务器证书已经验证。因此,我们只有服务器证书。这是因为我使用的证书是自签名的吗? - wasim
是的,如果您正在使用自签名证书,这种情况可能会发生。 - Ravi Patel
@RaviPatel 当我将密钥类型更改为BKS-V1时,它可以工作。但是我遇到了另一个问题:javax.net.ssl.SSLHandshakeException: 握手失败 at com.android.org.conscrypt.ConscryptFileDescriptorSocket.startHandshake(ConscryptFileDescriptorSocket.java:286) 请问你能告诉我如何解决吗? - kollein
显示剩余4条评论

0

由于我无法使用 JKS 类型的密钥库,因此对 Decoded 的解决方案进行了修改。

相反,我使用 Keystore Explorer 生成了一个 BKS 密钥。选择 BKS-V1 作为新 KeyStore 的类型,然后在启动之前设置 NanoHTTPD 服务器:

androidWebServer = new AndroidWebServer(port);

File f = new File("src/main/resources/localkey.bks");
System.setProperty("javax.net.ssl.trustStore", f.getAbsolutePath());
androidWebServer.setServerSocketFactory(new AndroidWebServer.SecureServerSocketFactory(AndroidWebServer.makeSSLSocketFactory("/" + f.getName(), "yourKeyStorePass".toCharArray()), null));

androidWebServer.start(); 

0
此链接有解决方案:https://www.baeldung.com/nanohttpd#https 使用keytool生成jks。
keytool -genkey -keyalg RSA -alias selfsigned
  -keystore keystore.jks -storepass your_password -validity 360
  -keysize 2048 -ext SAN=DNS:localhost,IP:127.0.0.1  -validity 9999

将keystore.jks移动到项目src/main/resources中

并使用以下代码:

public class HttpsExample  extends NanoHTTPD {
 
    public HttpsExample() throws IOException {
        super(8080);
        makeSecure(NanoHTTPD.makeSSLSocketFactory(
          "/keystore.jks", "password".toCharArray()), null);
        start(NanoHTTPD.SOCKET_READ_TIMEOUT, false);
    }
 
    // main and serve methods
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接