正确关闭SSLSocket

我想在Java中实现SSL代理。 我基本上打开了两个套接字browser-proxyproxy-server ,并运行两个线程,这些线程将写入proxy-server ,它们从browser-proxy读取的内容,反之亦然。 每个线程看起来像这样:

 while (true) { nr = in.read(buffer); if (nr == -1) System.out.println(sockin.toString()+" EOF "+nr); if (nr == -1) break; out.write(buffer, 0, nr); } sockin.shutdownInput(); sockout.shutdownOutput(); // now the second thread will receive -1 on read 

每个线程只关闭输入套接字,以便最终关闭两个套接字。

但是如果我想使用SSLSocket怎么办? 似乎那里不支持shutdownOutput/Input方法。 这是我得到的例外。

 Exception in thread "Thread-35" java.lang.UnsupportedOperationException: \ The method shutdownInput() is not supported in SSLSocket at com.sun.net.ssl.internal.ssl.BaseSSLSocketImpl.shutdownInput(Unknown Source) 

我想出的是:

 try { while (true) { nr = in.read(buffer); if (nr == -1) System.out.println(sockin.toString()+" EOF "+nr); if (nr == -1) break; out.write(buffer, 0, nr); } // possible race condition if by some mysterious way both threads would get EOF if (!sockin.isClosed()) sockin.close(); if (!sockout.isClosed()) sockout.close(); } catch (SocketException e) {/*ignore expected "socket closed" exception, */} 

每次我的套接字结束时,我必须捕获并忽略套接字exception的结束。

我的问题是:

  1. 如果不支持shutdownInput() ,我怎么从SSLSocket得到-1答案。
  2. 对我的痛苦有更好的解决方案吗? 我认为没有任何理智,因为当另一个线程标记他“我已经完成”时,其中一个线程可能已经在阻塞read方法中。 我看到将他踢出阻塞线程的唯一方法是关闭套接字并引发“关闭套接字”exception。

    (你可以猜测使用multithreading消息传递和异步数据读取的一些邪恶组合,以便向你的工作完成其他线程发出信号,但这太可怕了,我担心IntelliJ的风格警察只会因为思考而来它…)

澄清:我知道shutdownInputshutdownOutput没有意义,因为根据规范你不能有半双工TLS连接。 但鉴于此,如何在没有exception的情况下结束Java中的TLS连接?

请不要告诉我shutdownIput对TLS没有意义。 我知道, 这不是我要问的 。 我问我还能使用什么,以便正确关闭SSLSocket (因此标题为“正确关闭SSLSocket”。

这是我最初的(暂时删除的)答案,但它显然不够好,因为它很快就被激活了……我想我应该添加更多解释。

无法关闭SSL / TLS套接字的一半连接以符合TLS协议,如关闭警报一节中所述。

客户端和服务器必须共享连接结束的知识,以避免截断攻击。 任何一方都可以发起关闭消息的交换。

close_notify此消息通知收件人发件人不会再在此连接上发送任何消息。 如果在没有正确级别等于警告的close_notify消息的情况下终止任何连接,则会话将变得不可恢复。

任何一方都可以通过发送close_notify警报来发起关闭。 关闭警报后收到的任何数据都将被忽略。

在关闭连接的写入端之前,每一方都需要发送close_notify警报。 要求另一方使用自己的close_notify警报进行响应,并立即关闭连接,丢弃任何挂起的写入。 在关闭连接的读取端之前,close的发起者不需要等待响应的close_notify警报。

虽然您可能只想关闭连接的一半(通过shutdownInput() / shutdownOutput()输入或输出),但底层TLS层仍然需要在真正关闭通信之前发送此close_notify数据包,否则,它将被视为截断攻击。

修复非常简单:只在SSLSocket上使用close() :它不仅像普通的TCP套接字那样关闭连接,而是根据SSL / TLS规范干净地完成。

编辑补充说明

简短的回答仍然是:使用close() ,您可能还需要从SSL / TLS上的协议中找出何时适合这样做。

(只是为了澄清,你要做的实际上是一个“中间人”(MITM)代理。你需要将客户端配置为信任代理的证书,可能就像它是服务器一样证书,如果这是透明的。HTTPS连接如何通过HTTP代理工作是一个不同的问题 。)

在您对其他相关问题的一些评论中,我得到的印象是您认为不返回-1是“打破TLS协议”。 这不是关于协议,而是关于API:编程结构(类,方法,函数,…)的方式被提供给TLS堆栈的(程序员)用户以便能够使用TLS。 SSLSocket只是API的一部分,它为程序员提供了以类似于普通Socket的方式使用SSL / TLS的能力。 TLS规范根本不讨论套接字。 虽然构建TLS(及其前身,SSL)的目的是为了能够提供这种抽象,但在一天结束时, SSLSocket就是:抽象。 根据OOP设计原则, SSLSocketinheritance了Socket并试图尽可能地接近Socket的行为。 但是,由于SSL / TLS的工作方式(并且因为SSLSocket实际上位于普通Socket之上),所以不能精确映射所有function。

特别是,从普通TCP套接字到SSL / TLS套接字的转换区域不能完全透明地建模。 在设计SSLSocket类时必须做出一些任意选择(例如如何进行握手):它是在(a)将太多的TLS特性暴露给SSLSocket用户之间的折衷,(b)使TLS机制工作(c)将所有这些映射到超类( Socket )提供的现有接口。 这些选择不可避免地对SSLSocket的function产生一些影响,这就是为什么它的Javadoc API页面具有相对大量的文本。 就API而言, SSLSocket.close()必须遵守Socket.close()的描述。 从SSLSocket获取的InputStream / OutputStream与底层普通Socket中的那个不同,后者(除非你明确转换它 )你可能看不到。

SSLSocket被设计为尽可能紧密地使用,就好像它是一个普通的Socket ,并且你得到的InputStream被设计成尽可能地与基于文件的输入流一样。 顺便说一下,这不是特定于Java的。 甚至C中的Unix套接字也与文件描述符和read() 。 这些抽象很方便,大部分时间都可以工作,只要你知道它们的局限性。

说到read() ,即使在C语言中, EOF的概念来自文件结束终止,但你实际上并没有处理文件。 TCP套接字的问题在于,虽然您可以检测到远程方在发送FIN时选择关闭连接的时间,但如果它没有发送任何内容,则无法检测到它没有。 当从套接字中读取任何内容时,无法区分非活动连接和断开连接之间的区别。 因此,当谈到网络编程时,依赖于从InputStream读取-1 ,如果没有问题,但是你永远不会完全依赖它,否则你可能最终得到未释放的死连接。 虽然对于一方正确关闭半TCP连接(以某种方式,例如远程方在其InputStream上读取-1或等效的方式,如Java中的Orderly Versus Abortive Connection Release中所述 ),这是“礼貌的”,处理此案例不是不够。 这就是为什么TCP之上的协议通常用于指示它们何时完成发送数据 :例如,SMTP发送QUIT (并具有特定的终结符),HTTP 1.1使用Content-Length或chunked编码。

(顺便说一句,如果您对SSLSocket提供的抽象不满意,请尝试直接使用SSLEngine 。它可能更难,并且它不会改变TLS规范关于发送close_notify 。)

通常使用TCP,您应该知道在应用程序协议级别何时停止发送和接收数据(以避免未发布的连接和/或依赖于超时错误)。 TLS规范中的这句话使得这种通常良好的做法在TLS方面具有更强的要求:“ 要求对方以自己的close_notify警报响应并关闭连接,立即丢弃任何待处理的写入。 ”你将必须通过应用程序协议以某种方式进行同步,或者远程方可能仍然需要编写内容(特别是如果您允许流水线请求/响应)。

如果您正在编写的代理用于拦截HTTPS连接(作为MITM),则可以查看请求的内容。 即使HTTP请求是流水线的,您也可以计算目标服务器发送的响应数(并且通过Content-Length或chunks分隔符来预期它们何时结束)。

但是,您将无法拥有纯粹透明的通用TLS“拦截器”。 TLS旨在确保双方之间的运输,而不是中间的一方。 虽然实现这种保护(避免使用MITM)的大多数机制都是通过服务器证书完成的,但其他一些TLS机制会受到阻碍(关闭警报就是其中之一)。 请记住,您正在有效地创建两个不同的TLS连接,考虑到TLS的目的,它们很难合并,因为它不应该令人惊讶。

SSLSocket.close()是关闭SSLSocket的正确方法,但应用程序协议也将帮助您确定何时适合这样做。

用于关闭连接的SSL语义与TCP不同,因此shutdownInput和shutdownOutput实际上没有意义,至少在SSLSocket中可用的控制级别。 SSLSocket基本上减轻了SSL记录处理和SSL握手消息处理的负担。 它还处理SSL警报处理,其中一个警报是close_notify警报。 以下是RFC 2246中有关此警报含义的文本。

 "...Either party may initiate a close by sending a close_notify alert. Any data received after a closure alert is ignored. Each party is required to send a close_notify alert before closing the write side of the connection. It is required that the other party respond with a close_notify alert of its own and close down the connection immediately, discarding any pending writes. It is not required for the initiator of the close to wait for the responding close_notify alert before closing the read side of the connection...." 

Java确实为自杀者提供了另一个API,即SSLEngine API。 不要碰它,它会伤到你。

为了解决您的问题,您必须执行以下操作:

 OutputStream sockOutOStream = sockout.getOutputStream(); sockOutOStream.write(new byte[0]); sockOutOStream.flush(); sockout.close(); 

另一方面,从套接字读取的一方将收到-1-length消息,而不是抛出SocketException。