前一阵子我自己架设了一个tinode的IM服务器,
web直接可以运行
但是安卓版本的一直报错,
具体信息为:
No subjectAltNames on the certificate match
问了作者,作者竟然把我的问题直接删除了,还是自己调试代码吧。毕竟源码面前,了无秘密;
一、代码地址
GitHub - tinode/tindroid: Tinode chat client application for Android
我从release部分下载了0.20.9版本源码
二、更改源码配置
1)根目录下的build.gradle有2处需要更改,主要是版本信息,非git版本无从提取,随便设置一下
static def gitVersionCode() {
// If you are not compiling in a git directory and getting an error like
// [A problem occurred evaluating root project 'master'. For input string: ""]
// then just return your manually assigned error code like this:
// return 12345
def process = "git rev-list --count HEAD".execute()
return 12345
}
// Use current git tag as a version name.
// For example, if the git tag is 'v0.20.0-rc1' then the version name will be '0.20.0-rc1'.
static def gitVersionName() {
// If you are not compiling in a git directory, you should manually assign version name:
// return "MyVersionName"
def process = "git describe --tags".execute()
// Remove trailing CR and remove leading 'v' as in 'v1.2.3'
return "1.2.3"
}
2)app下面的build.gradle有3处需要修改
2.1)程序使用googleService,需要去官网注册一下相关的资料,自己注册一个新的应用,下载得到google-services.json,这个文件放置于app目录;
2.2)google-services.json中我们注册了一个应用的名字,这文件中有个package_name替换原来的应用ID,否则编译不过
applicationId "com.birdschat.cn"
2.3)创建证书,文件放置于源码同级目录,比如我的:
../robinkeys/key.keystore
在根目录下添加一个配置文件,叫keystore.properties,内容大概如下:
keystoreFile=../robin_keys/key.keystore
keystoreAlias=key.keystore
keystorePassword=123456
keyPassword=123456
并根据自己配置文件中的参数名,设置一下build.gradle:
signingConfigs {
release {
storeFile file(keystoreProperties['keystoreFile'])
storePassword keystoreProperties['keystorePassword']
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
}
}
这样应该就可以编译了!!
3)取消客户端WebSocket 的SSL双向认证
但是运行后,设置了自己的服务器,以及使用加密模式,无法注册或者登录,
主要是我们的证书需要有域名,并且是申请来的,也就是有CA认证的,而不是自己生成的,不然无法实现双向验证,这主要是为了防止中间人攻击;
但是我们往往就是自己内部试用,不需要这么麻烦,
需要对SDK部分代码进行更该,参考:java websocket及忽略证书_nell_lee的博客-CSDN博客_websocket 忽略证书
更改后的代码如下:Connection.java 全文
package co.tinode.tinodesdk;
import android.util.Log;
import org.java_websocket.client.WebSocketClient;
import org.java_websocket.drafts.Draft_6455;
import org.java_websocket.handshake.ServerHandshake;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLHandshakeException;
import javax.net.ssl.*;
import java.net.Socket;
import java.net.URI;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
/**
* A thinly wrapped websocket connection.
*/
public class Connection extends WebSocketClient {
private static final String TAG = "Connection";
private static final int CONNECTION_TIMEOUT = 3000; // in milliseconds
// Connection states
// TODO: consider extending ReadyState
private enum State {
// Created. No attempts were made to reconnect.
NEW,
// Created, in process of creating or restoring connection.
CONNECTING,
// Connected.
CONNECTED,
// Disconnected. A thread is waiting to reconnect again.
WAITING_TO_RECONNECT,
// Disconnected. Not waiting to reconnect.
CLOSED
}
private final WsListener mListener;
// Connection status
private State mStatus;
// If connection should try to reconnect automatically.
private boolean mAutoreconnect;
// This connection is a background connection.
// The value is reset when the connection is successful.
private boolean mBackground;
// Exponential backoff/reconnecting
final private ExpBackoff backoff = new ExpBackoff();
@SuppressWarnings("WeakerAccess")
protected Connection(URI endpoint, String apikey, WsListener listener) {
super(normalizeEndpoint(endpoint), new Draft_6455(), wrapApiKey(apikey), CONNECTION_TIMEOUT);
setReuseAddr(true);
mListener = listener;
mStatus = State.NEW;
mAutoreconnect = false;
mBackground = false;
if(endpoint.toString().contains("wss://")){
trustAllHosts(this);
}
}
// robin add here
/**
*忽略证书
*@paramclient
*/
void trustAllHosts(Connection client) {
TrustManager[] trustAllCerts = new TrustManager[]{new X509ExtendedTrustManager() {
@Override
public void checkClientTrusted(X509Certificate[] x509Certificates, String s, Socket socket) throws CertificateException {
}
@Override
public void checkServerTrusted(X509Certificate[] x509Certificates, String s, Socket socket) throws CertificateException {
}
@Override
public void checkClientTrusted(X509Certificate[] x509Certificates, String s, SSLEngine sslEngine) throws CertificateException {
}
@Override
public void checkServerTrusted(X509Certificate[] x509Certificates, String s, SSLEngine sslEngine) throws CertificateException {
}
public java.security.cert.X509Certificate[] getAcceptedIssuers() {
// return new java.security.cert.X509Certificate[]{};
// System.out.println("getAcceptedIssuers");
return null;
}
@Override
public void checkClientTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {
System.out.println("checkClientTrusted");
}
@Override
public void checkServerTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {
System.out.println("checkServerTrusted");
}
}};
try {
SSLContext ssl = SSLContext.getInstance("SSL");
ssl.init(null, trustAllCerts, new java.security.SecureRandom());
SSLSocketFactory socketFactory = ssl.getSocketFactory();
this.setSocketFactory(socketFactory);
} catch (Exception e) {
e.printStackTrace();
}
}
//————————————————
// 版权声明:本文为CSDN博主「nell_lee」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
// 原文链接:https://blog.csdn.net/qq_33529102/article/details/115763483
private static Map<String,String> wrapApiKey(String apikey) {
Map<String, String> headers = new HashMap<>();
headers.put("X-Tinode-APIKey",apikey);
return headers;
}
private static URI normalizeEndpoint(URI endpoint) {
String path = endpoint.getPath();
if (path.equals("")) {
path = "/";
} else if (path.lastIndexOf("/") != path.length() - 1) {
path += "/";
}
path += "channels"; // ws://www.example.com:12345/v0/channels
String scheme = endpoint.getScheme();
// Normalize scheme to ws or wss.
scheme = ("wss".equals(scheme) || "https".equals(scheme)) ? "wss" : "ws";
int port = endpoint.getPort();
if (port < 0) {
port = "wss".equals(scheme) ? 443 : 80;
}
try {
endpoint = new URI(scheme,
endpoint.getUserInfo(),
endpoint.getHost(),
port,
path,
endpoint.getQuery(),
endpoint.getFragment());
} catch (URISyntaxException e) {
Log.w(TAG, "Invalid endpoint URI", e);
}
return endpoint;
}
private void connectSocket(final boolean reconnect) {
new Thread(() -> {
try {
if (reconnect) {
reconnectBlocking();
} else {
connectBlocking(CONNECTION_TIMEOUT, TimeUnit.MILLISECONDS);
}
if ("wss".equals(uri.getScheme())) {
// SNI: Verify server host name.
SSLSocket ss = (SSLSocket) getSocket();
ss.setWantClientAuth(false);
SSLSession sess = ss.getSession();
String hostName = uri.getHost();
// robin add
// if (!HttpsURLConnection.getDefaultHostnameVerifier().verify(hostName, sess)) {
// close();
// throw new SSLHandshakeException("SNI verification failed. Expected: '" + uri.getHost() +
// "', actual: '" + sess.getPeerPrincipal() + "'");
// }
}
} catch (Exception ex) {
Log.i(TAG, "WS connection failed", ex);
if (mListener != null) {
mListener.onError(Connection.this, ex);
}
}
}).start();
}
/**
* Establish a connection with the server. It opens or reopens a websocket in a separate
* thread.
*
* This is a non-blocking call.
*
* @param autoReconnect if connection is dropped, reconnect automatically
*/
@SuppressWarnings("WeakerAccess")
synchronized public void connect(boolean autoReconnect, boolean background) {
mAutoreconnect = autoReconnect;
mBackground = background;
switch (mStatus) {
case CONNECTED:
case CONNECTING:
// Already connected or in process of connecting: do nothing.
break;
case WAITING_TO_RECONNECT:
backoff.wakeUp();
break;
case NEW:
mStatus = State.CONNECTING;
connectSocket(false);
break;
case CLOSED:
mStatus = State.CONNECTING;
connectSocket(true);
break;
// exhaustive, no default:
}
}
/**
* Gracefully close websocket connection. The socket will attempt
* to send a frame to the server.
*
* The call is idempotent: if connection is already closed it does nothing.
*/
@SuppressWarnings("WeakerAccess")
synchronized public void disconnect() {
boolean wakeUp = mAutoreconnect;
mAutoreconnect = false;
// Actually close the socket (non-blocking).
close();
if (wakeUp) {
// Make sure we are not waiting to reconnect
backoff.wakeUp();
}
}
/**
* Check if the socket is OPEN.
*
* @return true if the socket is OPEN, false otherwise;
*/
@SuppressWarnings("WeakerAccess")
public boolean isConnected() {
return isOpen();
}
/**
* Check if the socket is waiting to reconnect.
*
* @return true if the socket is OPEN, false otherwise;
*/
@SuppressWarnings("WeakerAccess")
public boolean isWaitingToReconnect() {
return mStatus == State.WAITING_TO_RECONNECT;
}
/**
* Reset exponential backoff counter to zero.
* If autoreconnect is true and WsListener is provided, then WsListener.onConnect must call
* this method.
*/
@SuppressWarnings("WeakerAccess")
public void backoffReset() {
backoff.reset();
}
@Override
public void onOpen(ServerHandshake handshakeData) {
synchronized (this) {
mStatus = State.CONNECTED;
}
if (mListener != null) {
boolean bkg = mBackground;
mBackground = false;
mListener.onConnect(this, bkg);
} else {
backoff.reset();
}
}
@Override
public void onMessage(String message) {
if (mListener != null) {
mListener.onMessage(this, message);
}
}
@Override
public void onMessage(ByteBuffer blob) {
// do nothing, server does not send binary frames
Log.w(TAG, "binary message received (should not happen)");
}
@Override
public void onClose(int code, String reason, boolean remote) {
// Avoid infinite recursion
synchronized (this) {
if (mStatus == State.WAITING_TO_RECONNECT) {
return;
} else if (mAutoreconnect) {
mStatus = State.WAITING_TO_RECONNECT;
} else {
mStatus = State.CLOSED;
}
}
if (mListener != null) {
mListener.onDisconnect(this, remote, code, reason);
}
if (mAutoreconnect) {
new Thread(() -> {
while (mStatus == State.WAITING_TO_RECONNECT) {
backoff.doSleep();
synchronized (Connection.this) {
// Check if we no longer need to connect.
if (mStatus != State.WAITING_TO_RECONNECT) {
break;
}
mStatus = State.CONNECTING;
}
connectSocket(true);
}
}).start();
}
}
@Override
public void onError(Exception ex) {
Log.w(TAG, "Websocket error", ex);
if (mListener != null) {
mListener.onError(this, ex);
}
}
interface WsListener {
default void onConnect(Connection conn, boolean background) {
}
default void onMessage(Connection conn, String message) {
}
default void onDisconnect(Connection conn, boolean byServer, int code, String reason) {
}
default void onError(Connection conn, Exception err) {
}
}
}
这样就OK了;
4)设置服务器默认参数
将服务器的链接参数预先设置好为我们需要的:
4.1) 地址与端口:全文搜索“:6060”字样,在资源文件res/strings.xml中更改:
<string name="emulator_host_name" translatable="false">119.0.0.1:6060</string>
同时,将build.gradle的相关位置做更改,自动生成相关的资源文件
buildTypes {
debug {
resValue "string", "default_host_name", '"119.0.0.0:6060"'
}
release {
resValue "string", "default_host_name", '"api.tinode.co"'
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.release
}
在同样在资源中更改为自己的地址和端口;
4.2)使用https,更改TindroidApp.java
将返回的默认的参数设置为true
public static boolean getDefaultTLS() {
//return !isEmulator();
return true;
}
编译好了就可以用了!
备注:编译好的apk https://download.csdn.net/download/robinfoxnan/87300700