2011-06-21 122 views
7

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

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, */} 

我必须赶上,而忽略每一个我的插座结束时间插座异常结束。

我的问题是:

  1. 如果shutdownInput()不支持,怎么来的,我从SSLSocket得到一个-1答案。
  2. 对我的痛苦有更好的解决方案吗?我不认为有什么理智的,因为其中一个线程可能已经在阻止read方法,当另一个线程标记他“我完成了”。我看到将他踢出阻塞线程的唯一方法是关闭套接字并引发“闭合套接字”异常。我猜你可能会使用一些邪恶的多线程消息传递和异步数据读取的组合,以便向另一个线程表明你的工作已完成,但这太可怕了,我担心IntelliJ的风格警察会跟着我只是想这件事。)

澄清:我知道shutdownInputshutdownOutput没有意义的,因为你不能有一个半双工TLS连接,按规格。但是,鉴于此,如何在Java中结束TLS连接而不发生异常?

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

+0

当你说shutdownInput/Output不被支持时,你是什么意思?该方法是否会抛出异常?如果是这样,什么例外? – Pace

+0

是的,它确实会抛出一个异常'线程中的异常“Thread-4”java.lang.UnsupportedOperationException:SSLSocket'中不支持shutdownInput()方法。我认为有足够知识回答我的问题的人已经知道,他们不支持SSL。 –

+0

您能澄清为什么“现在第二个线程将在sockout.shutdownOutput()'上读取-1”吗?在你的例子中,推测,'in = sockin.getInputStream()'。 – Bruno

回答

17

这是我最初的(暂时删除)的答案,但它显然不够好,因为它-2'相当快...我想我应该添加更多解释

无法关闭SSL/TLS套接字的一半以符合TLS协议,如section on closure alerts

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

close_notify此邮件通知收件人发件人不会 在此连接上发送更多邮件。如果没有正确的close_notify消息且级别等于警告,任何连接终止,会话将变为不可消耗。

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

每一方都需要发送 close_notify警报,然后关闭 连接的写入端。它需要 对方 立即响应其closeleotify警报 并立即关闭 连接,丢弃任何 等待写入。对于响应close_notify 警报在关闭 连接的读取端之前接近等待 的发起者并不要求 。

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

修复的方法是相当简单:只在SSLSocket的使用close():它不只是关闭连接,因为它会为正常的TCP套接字,但它干净根据SSL/TLS规范做的。

编辑附加说明

简短的回答仍然是:使用close(),您可能还需要了解何时适合从SSL/TLS之上的协议进行此操作。

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

在你的一些评论您other related question,我得到了,你以为不返回-1印象是“打破TLS协议”。这不是关于协议,而是关于API:编程结构(类,方法,函数,...)被提供给(程序员)TLS堆栈的用户以便能够使用TLS的方式。 SSLSocket仅仅是为程序员提供类似于普通Socket的方式使用SSL/TLS的API的一部分。 TLS规范根本不谈论套接字。尽管TLS(及其前身SSL)的建立旨在使提供这种抽象成为可能,但在当天结束时SSLSocket只是一个抽象概念。 符合OOP设计原则,SSLSocket继承Socket,并尽可能贴近Socket的行为。但是,由于SSL/TLS的工作方式(并且由于SSLSocket实际位于正常的Socket之上),因此无法精确映射所有功能。

特别是,从正常的TCP套接字到SSL/TLS插座的过渡区域不能完全被透明地建模。 在设计SSLSocket类时必须进行一些任意选择(例如握手如何发生): 它不是(a)将TLS的特定性暴露给SSLSocket用户,(b)使TLS机制工作和(c)将所有这些映射到由超类提供的现有接口(Socket)。 这些选择不可避免地对SSLSocket的功能产生一定的影响,这就是为什么它Javadoc API page具有相对大量的文本。根据API, SSLSocket.close()必须遵守Socket.close()的描述。 从SSLSocket获得的InputStream/OutputStream与底层的简单Socket中的InputStream/OutputStream不同,您可能看不到它(除非you've converted it explicitly)。

SSLSocket被设计成可用尽可能靠近可能,如果它是一个普通的Socket,你会得到的InputStream旨在尽可能接近可能表现为基于文件的输入流。顺便说一句,这不是特定于Java的。即使C中的Unix套接字也与文件描述符和read()一起使用。 只要你知道它们的局限性,这些抽象很方便,并且大部分时间都可以工作。

说到read(),即使在C中,EOF的概念来自文件结束终止,但实际上并没有处理文件。 TCP套接字的问题在于,虽然您可以检测到远程方在发送FIN时选择关闭连接的时间,但如果它不发送任何内容,则无法检测到它没有。 当从套接字中读取任何内容时,无法辨别非活动连接和断开连接之间的区别。 因此,当涉及到网络编程,依靠从InputStream如果细阅读-1,但你永远不单靠依赖,否则你可能会未发布,死连接结束。 虽然这是一个党“礼貌”正确地关闭TCP半连接(的方式,如对方在其InputStream或等价的东西读-1,如Orderly Versus Abortive Connection Release in Java描述),处理这种情况下是不够的。 这就是为什么TCP上的协议通常是designed to give an indication as to when they're done sending data:例如, SMTP发送QUIT(并且具有特定的终止符),HTTP 1.1使用Content-Length或分块编码。

(顺便说一句,如果你不满意SSLSocket提供的抽象,尝试直接使用SSLEngine。它可能更难,它不会改变什么TLS规范说要送close_notify)。

通常与TCP,应该知道,在应用协议级,当停止发送和接收数据(以避免未释放的连接和/或依赖于超时错误)。 这句话在TLS规范使得这总体上是好的做法更强的要求,当谈到TLS: “,要求对方与自己的close_notify消息响应警报,并关闭立即丢弃任何未决写入连接。“ 您将不得不通过应用程序协议以某种方式进行同步,否则远程参与方可能仍有要写入的内容(特别是如果您允许流水线请求/响应)。

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

但是,您将无法拥有纯粹透明的通用TLS“拦截器”。 TLS旨在保证双方之间的交通运输安全,而不是在中间有一个交通工具。 尽管通过服务器证书完成实现该保护(避免MITM)的大部分机制,但其他一些TLS机制也会受阻碍(闭包警报就是其中之一)。 请记住,您正在有效地创建两个不同的TLS连接,考虑到TLS的目的,它们很难合并,因为不应该让人感到意外。

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

+0

你见过我提出的解决方案吗?我想我提出你在说什么,我说得对吗?我使用'close',但另一端抛出异常,而不是在读取时干净地返回'-1'。关闭TLS连接后,是否有可能让Java返回'-1'?是的,当我联系非Java实体时它是这样做的。我怎样才能模仿这种行为? –

+1

我刚刚意识到[RFC 2818](http://tools.ietf.org/html/rfc2818#section-2.2)的这一部分也应该是相关的:*“由于TLS不理会HTTP请求/响应边界,有必要检查HTTP数据本身(特别是Content-Length头部),以确定截断是发生在消息内部还是消息之间。“* – Bruno

+0

嗨,Bruno,你知道一些现成的SSL引擎解决方案吗? –

0

用于关闭连接的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。不要触摸它,它会伤害你。

+0

我明白这一点,但关闭连接会在另一侧抛出异常,这仍然没有意义。我会澄清一点,我知道在评论中。我应该怎么做,而不是'shutdownInput'和'shutdownOutput'来确保当他试图读取流而不是异常时,另一侧会得到'-1'? –

+2

@Elazar Leibovich你应该调用SSLSocket.close()。我仍然不清楚你在哪里得到例外。 –

-3

为了解决你的问题,你必须做到以下几点:

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

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

+0

废话。这个零长度的写不会发送任何东西,更不用说'-1长度的消息'。不存在'-1长度消息'这样的事情。 read()返回的-1是一个带外*值,意味着流的结束,它是由发送端的close()引起的,而不是由你写的任何东西引起的。 flush()在这里也是多余的。唯一需要的是close()。 – EJP