-
Notifications
You must be signed in to change notification settings - Fork 2
InternalWebSockets
The WebSockets
extension was developed within the Google Summer of Code 2012. This page should give developers an insight how it is structured.
The extension is built upon RFC6455, featuring version 13 of the WebSocket
-protocol. The focus of this implementation lies on the payloads. As a result the user interface is transparent regarding WebSocket
-frames.
Originally I wanted to build the extension upon Java's NIO features, that allows non-blocking reads. It worked fine for non-SSL connections, but I was not able to transform the javax.net.ssl.SSLSocket
, that comes out of the modified commons-httpclient-3.1.jar
library to a java.nio.channels.SocketChannel
with some instance of javax.net.ssl.SSLEngine
, as this would have been the way to go with NIO.
As a result each WebSocket
channel consists of two threads:
- one listener on the outgoing connection from your browser to ZAP
- another listener on the incoming connection from the web server to ZAP
Messages received are stored into the database. There are 3 tables so far:
- websocket_channel: stores information about each connection
- websocket_message: contains information about each message received & sent
-
websocket_message_fuzz: if
WebSocket
messages are issued with the fuzz-extension, additional information is stored here
CREATE CACHED TABLE websocket_channel (
channel_id BIGINT PRIMARY KEY,
host VARCHAR(255) NOT NULL,
port INTEGER NOT NULL,
url VARCHAR(255) NOT NULL,
start_timestamp TIMESTAMP NOT NULL,
end_timestamp TIMESTAMP NULL,
history_id INTEGER NULL,
FOREIGN KEY (history_id) REFERENCES HISTORY(HISTORYID) ON DELETE SET NULL ON UPDATE SET NULL
);
CREATE CACHED TABLE websocket_message (
message_id BIGINT NOT NULL,
channel_id BIGINT NOT NULL,
timestamp TIMESTAMP NOT NULL,
opcode TINYINT NOT NULL,
payload_utf8 CLOB NULL,
payload_bytes BLOB NULL,
payload_length BIGINT NOT NULL,
is_outgoing BOOLEAN NOT NULL,
PRIMARY KEY (message_id, channel_id),
FOREIGN KEY (channel_id) REFERENCES websocket_channel(channel_id)
);
ALTER TABLE websocket_message ADD CONSTRAINT websocket_message_payload CHECK (
payload_utf8 IS NOT NULL
OR
payload_bytes IS NOT NULL
);
CREATE CACHED TABLE websocket_message_fuzz (
fuzz_id BIGINT NOT NULL,
message_id BIGINT NOT NULL,
channel_id BIGINT NOT NULL,
state VARCHAR(50) NOT NULL,
fuzz LONGVARCHAR NOT NULL,
PRIMARY KEY (fuzz_id, message_id, channel_id),
FOREIGN KEY (message_id, channel_id) REFERENCES websocket_message(message_id, channel_id) ON DELETE CASCADE
);
These tables are created in the class TableWebSocket
, if not existed before.
Things to note:
- Primary key values are created within the application with instances of
java.util.concurrent.atomic.AtomicInteger
, seeWebSocketProxy.channelIdGenerator
,WebSocketProxy.messageIdGenerator
&WebSocketFuzzableTextMessage.fuzzIdGenerator
. -
websocket_channel.history_id
may link to the HTTP message of theWebSocket
handshake. -
websocket_channel.host
andwebsocket_channel.url
are not the same. The first field contains the result ofSocket.getInetAddress().getHostName()
, while the latter contains the requested URL of theWebSocket
handshake. -
websocket_message
contains two columns for payloads, namelypayload_utf8
andpayload_bytes
. For binary-opcode messages the columnpayload_bytes
is filled. For all other types of messages, the columnpayload_utf8
is set with the readable representation. This way, integration into the search-extension should be easier. The constraintwebsocket_message_payload
ensures that at least one of these columns is set. An upgrade from HSQLDB version 1.8.0 to 2.2.9 was made to take advantage of the CLOB/BLOB fields:- Only a reference to the large object's content is returned, allowing you to retrieve only a substring, respectively only some bytes. This is used in the payload preview of the
WebSockets
-tab.
- Only a reference to the large object's content is returned, allowing you to retrieve only a substring, respectively only some bytes. This is used in the payload preview of the
The first class diagram contains the core part without integration into brk- or fuzz-extension nor with UI classes.
Let us start with ExtensionWebSocket
, which is the starting point of my contribution. It initializes all components and hooks them into ZAP. When a new WebSocket
-connection is detected in the ProxyThread
class, the following call takes place:
ExtensionWebSocket extWs = (ExtensionWebSocket) Control.getSingleton().getExtensionLoader().getExtension(ExtensionWebSocket.NAME);
extWs.addWebSocketsChannel(msg, inSocket, outSocket, outReader);
It takes the incoming & outgoing socket, the HttpMessage
of the WebSocket
-handshake and the current InputStream
of the outgoing connection, which was used to read the HTTP response. This is of importance, as first WebSocket
-messages are allowed to appear in the same TCP packet after the HTTP response. As it may buffer bytes, first messages would be lost if opening another InputStream
on outSocket
.
The ExtensionWebSocket
creates a new instance of WebSocketProxy
via the factory method WebSocketProxy.create(...)
, that returns a version specific WebSocketProxy
instance. For now WebSocketProxyV13
is the only implementation of the abstract class WebSocketProxy
. It contains an inner class WebSocketMessageV13
extending the abstract class WebSocketMessage
.
Each WebSocketProxy
instance creates two instances of WebSocketListener
. These instances are threads listening to one of the given Socket
's. If the first byte arrives, it calls WebSocketProxy.processRead(...)
that handles the received WebSocket
frame.
The WebSocketProxy
class implements the Observer-pattern, allowing instances of WebSocketObserver
to get notified about new frames or a change of the WebSocketProxy
's state. The following observers are used so far (with order value in parenthesis):
-
ExtensionFilter
(0): Calls all enabledFilter
instances, allowing them to change e.g.: the payload. There is aWebSocket
-specific filter calledFilterWebSocketPayload
, that is added to theFilter
-extension in theExtensionWebSocket.hook(...)
method. -
WebSocketProxyListenerBreak
(95): Halts if a breakpoint applies and possibly changes payload. -
WebSocketStorage
(100): UtilizesTableWebSocket
to store channels and messages into the database. -
WebSocketPanel
(105): Shows channels and their messages in the user interface under theWebSockets
-tab. *WebSocketFuzzerHandler
(110): Shows fuzzed messages in the user interface under the_fuzz-tab._
As you can see, this mechanism is a very powerful way to get informed about what is going on. In the class diagram you can see that each instance of WebSocketProxy
has got its own observerList
. If you want to observe all instances you have to add your WebSocketObserver
implementation to the ExtensionWebSocket.allChannelObservers
list. Do the following in your Extension*
class:
@Override
public void hook(ExtensionHook extensionHook) {
// 'this' implements WebSocketObserver
extensionHook.addWebSocketObserver(this);
}
With the first WebSocket
-connection arriving, the hooked observers are added to the ExtensionWebSocket.allChannelObservers
list. Each time a new WebSocketProxy
instance is created, every observer from this list is added to the WebSocketProxy.observerList
.
WebSocket
messages are processed in WebSocketProxy.processRead(...)
as mentioned before. There are several types of messages, which is specified by the 4-bits opcode header:
- non-control frames
- binary
- text
- control frames
- close
- ping
- pong
A non-control message may be split up across several frames. For this purpose a continuation frame is sent, resuming the last binary- or text-frame. In between arbitrary control frames are allowed to occur.
To achieve some loose coupling, I have introduced the *DTO
classes, namely WebSocketChannelDTO
& WebSocketMessageDTO
. DTO stands for Data Transfer Object. They can be retrieved via:
public WebSocketMessageDTO WebSocketMessage.getDTO();
public WebSocketChannelDTO WebSocketProxy.getDTO();
- with various methods from the
TableWebSocket
These DTO-objects are in use across theWebSocket
-extension.
The main class is WebSocketPanel
, which represents the WebSockets
-tab. It contains all the UI elements visible there. The most important ones are:
-
WebSocketPanel.channelSelectModel
which is filled with allWebSocket
channels. ViaWebSocketPanel.getChannelComboBoxModel()
you can retrieve an instance ofClonedComboBoxModel
, whose items are backed by the original model, i.e. if the originalComboBox
changes, also the cloned version changes. TheClonedComboBoxModel
is used for various dialogues. -
handshakeButton
: When a channel is selected in theComboBox
, this button is enabled. It allows theHttpMessage
from the handshake to be shown in Request/Response tab. -
brkButton
: See _brk_-extension integration for more information. -
filterButton
: Opens up thefilter.FilterWebSocketReplaceDialog
allowing to change the type of messages shown in theWebSockets
-tab. -
optionsButton
: Opens up the options dialogue defined byOptionsWebSocketPanel
. It is backed by theOptionsParamWebSocket
, which is the interface to the saved settings. -
messagesView
: This instance ofWebSocketMessagesView
wraps aJTable
containing allWebSocketMessages
. The model behind theJTable
is given by an instance ofWebSocketMessagesViewModel
.
WebSocketMessagesViewModel
extends the PagingTableModel
, which holds only PagingTableModel.MAX_PAGE_SIZE
entries in cache at any point in time, but the row count returns the total number of messages to be shown, resulting in a scrollbar that reflects a table containing all entries. When scrolling, or when new messages arrive, a new page is loaded from database. While in load, place-holder values are shown in the rows. In WebSocketMessagesViewModel.getRowCount()
the number of rows is cached to save some queries.
The WebSocketFuzzMessagesViewModel
does also extend WebSocketMessagesViewModel
as its entries are also stored in the database. See the _fuzz_-extension integration for more information.
There is another useful helper class, named WebSocketUiHelper
. It has got methods that create UI elements for selecting channels, opcodes and direction. It is used by various dialogues and came into existence to bring up more consistency across dialogues:
-
WebSocketBreakDialog
: specify custom conditions for breakpoints -
FilterWebSocketReplaceDialog
: allows to replaceWebSocket
payload using defined pattern -
WebSocketMessagesViewFilterDialog
: restrict types of messages to be shown in theWebSockets
tab
The FilterWebSocketPayload
class allows for modification of WebSocket
-payloads on specific messages. It is set up in the ExtensionWebSocket.hook(...)
method. It overwrites the method onWebSocketPayload(...)
and modifies a messages' payload if criteria are met. The ExtensionFilter
implements WebSocketObserver
and calls onWebSocketPayload(...)
when a message arrives.
There are several options for the break-behaviour of WebSocket
messages. These options are enforced in the WebSocketBreakpointMessageHandler
class. The decision if ZAP should hold on the arrival of a specific message, i.e. if a breakpoint applies, is reached in WebSocketBreakpointMessage.match(...)
. Beforehand WebSocketProxyListenerBreak.onMessageFrame(...)
does some initial checks before passing on the power of decision.
The Fuzzer-tab is able to show a messages view that inherits from the view in the WebSockets
-tab. The correspondent classes are WebSocketFuzzMessagesView
with its model class WebSocketFuzzMessagesViewModel
. The view model is also backed by the database. The table websocket_message_fuzz
is used to provide more information on the fuzzed messages. Unsuccessful fuzzed messages do not pass the WebSocketStorage
class, that is responsible for saving messages into database. As a result there is an extra list for failed messages in WebSocketFuzzMessagesViewModel.erroneousMessages
. A reason for unsuccessful fuzzing attempts may be closed WebSocket
-channels.
WebSocketFuzzMessageDTO
extends WebSocketMessageDTO
and holds additional information on the fuzzing process. When an instance of WebSocketFuzzMessageDTO
arrives at the WebSocketStorage
class, additional information is saved to the websocket_message_fuzz
-table.
You can not only retrieve a DTO-object from a WebSocketMessage
, but also create a WebSocketMessage
from a WebSocketMessageDTO
. The given DTO-object is saved as base-DTO in the WebSocketMessage
. When you retrieve the DTO-object from a WebSocketMessage
no new WebSocketMessageDTO
instance is created, but the base-DTO is returned with current values. This mechanism is used to integrate the fuzzing of WebSocket
messages.