├── about.html
├── readme.md
└── timeinator.py
/about.html:
--------------------------------------------------------------------------------
1 |
2 |
{extension_name}
3 | About
4 | {extension_name} is an extension for Burp Suite that can be used to perform timing attacks over an unreliable network such as the internet.
5 |
6 | The attack mode is similar to the "sniper" mode in burp intruder, but instead of sending a single request for every payload, it is able to send multiple requests for each payload and display the minimum and maximum times taken to receive a response as well as the mean and median averages and the standard deviation.
7 |
8 | In an effort to prevent slow DNS lookups introducing delays, the domain is resolved at the start of the attack before any http requests are made. In addition, the extension sends HTTP(S) requests sequentially to reduce the chance of delays being introduced by overloading the server.
9 | Usage
10 |
11 | - Enable the extension in order to add a tab titled {extension_name} to the Burp Suite user interface.
12 | - Send a request to the extension by right clicking on a request anywhere in Burp Suite and choosing the "Send To {extension_name}" menu item. This will populate the "Attack" tab under the {extension_name} tab.
13 | - Select the section of the request that should be replaced with the payloads then click the "Add §" button. This will add a § symbol either side of the characters that should be replaced with the payload. More than one payload position can be chosen in a single request.
14 | - Choose how many requests to make for each payload.
15 | - Type or copy and paste the payloads into the "Payloads" text area. Place a single payload on each new line.
16 | - Hit the "Start Attack" button. Results will be displayed in the "Results" tab. If there is more than one payload then results will be colour coded to make timing differences easier to spot. Slower requests will be coloured red and faster requests will be coloured green.
17 |
18 | Troubleshooting
19 | This extension is wriiten in Python and therefore requires Jython.
20 | HTTP(S) requests made by this extension will not be shown in the proxy history, however extensions such as Logger++ (available from the BApp store) will do so.
21 |
22 |
23 | Credits
24 | {extension_name} is by graeme.robinson@f-secure.com
25 | Thanks to:
26 |
27 | - @rbsec for adding standard deviation to the results
28 | - Oliver Simonnet for coming up with the "Timeinator" name
29 |
30 |
31 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | Timeinator
2 | =========================
3 |
4 | About
5 | -----
6 |
7 | Timeinator is an extension for Burp Suite that can be used to perform timing attacks over an unreliable network such as the internet.
8 |
9 | The attack mode is similar to the "sniper" mode in burp intruder, but instead of sending a single request for every payload, it is able to send multiple requests for each payload and display the minimum and maximum times taken to receive a response as well as the mean and median averages and the standard deviation.
10 |
11 | In an effort to prevent slow DNS lookups introducing delays, the domain is resolved at the start of the attack before any http requests are made. In addition, the extension sends HTTP(S) requests sequentially to reduce the chance of delays being introduced by overloading the server.
12 |
13 | Usage
14 | -----
15 |
16 | 1. Enable the extension in order to add a tab titled Timeinator to the Burp Suite user interface.
17 | 2. Send a request to the extension by right clicking on a request anywhere in Burp Suite and choosing the "Send To Timeinator" menu item. This will populate the "Attack" tab under the Timeinator tab.
18 | 3. Select the section of the request that should be replaced with the payloads then click the "Add §" button. This will add a § symbol either side of the characters that should be replaced with the payload. More than one payload position can be chosen in a single request.
19 | 4. Choose how many requests to make for each payload.
20 | 5. Type or copy and paste the payloads into the "Payloads" text area. Place a single payload on each new line.
21 | 6. Hit the "Start Attack" button. Results will be displayed in the Results" tab. If there is more than one payload then results will be colour coded to make timing differences easier to spot. Slower requests will be coloured red and faster requests will be coloured green.
22 |
23 | Troubleshooting
24 | ---------------
25 |
26 | This extension is written in Python and therefore requires Jython.
27 |
28 | HTTP(S) requests made by this extension will not be shown in the proxy history, however extensions such as Logger++ (available from the BApp store) will do so.
29 |
30 | Credits
31 | -------
32 |
33 | Timeinator is by [graeme.robinson@f-secure.com](mailto:graeme.robinson@f-secure.com)
34 |
35 | Thanks to:
36 | * @rbsec for adding standard deviation to the results
37 | * Oliver Simonnet for coming up with the "Timeinator" name
38 |
--------------------------------------------------------------------------------
/timeinator.py:
--------------------------------------------------------------------------------
1 | from re import sub
2 | from socket import gethostbyname
3 | from threading import Thread
4 | from time import time
5 |
6 | from javax.swing import (JTabbedPane, JPanel, JLabel, JTextField,
7 | JTextArea, JCheckBox, JMenuItem, JButton, JTable,
8 | JScrollPane, JProgressBar)
9 | from javax.swing.table import DefaultTableModel, DefaultTableCellRenderer
10 | from java.awt import Color, GridBagLayout, GridBagConstraints, Insets
11 | import java.lang
12 |
13 | from burp import (
14 | IBurpExtender, ITab, IContextMenuFactory, IMessageEditorController)
15 |
16 | EXTENSION_NAME = "Timeinator"
17 | COLUMNS = [
18 | "Payload", "Number of Requests", "Status Code", "Length (B)", "Body (B)",
19 | "Minimum (ms)", "Maximum (ms)", "Mean (ms)", "Median (ms)", "StdDev (ms)"]
20 |
21 |
22 | def mean(values):
23 | return sum(values) / len(values)
24 |
25 |
26 | def median(values):
27 | length = len(values)
28 | values.sort()
29 | if length % 2 != 0:
30 | # Odd number of values, so chose middle one
31 | return values[length/2]
32 | else:
33 | # Even number of values, so mean of middle two
34 | return mean([values[length/2], values[(length/2)-1]])
35 |
36 | def stdDev(values):
37 | ss = sum((x - mean(values))**2 for x in values)
38 | pvar = ss/len(values)
39 | return pvar**0.5
40 |
41 | class BurpExtender(
42 | IBurpExtender, ITab, IContextMenuFactory, IMessageEditorController):
43 |
44 | # Implement IBurpExtender
45 | def registerExtenderCallbacks(self, callbacks):
46 |
47 | callbacks.registerContextMenuFactory(self)
48 |
49 | self._callbacks = callbacks
50 | self._helpers = callbacks.getHelpers()
51 |
52 | callbacks.setExtensionName(EXTENSION_NAME)
53 |
54 | # Construct UI
55 | insets = Insets(3, 3, 3, 3)
56 | self._messageEditor = callbacks.createMessageEditor(self, True)
57 | attackPanel = self._constructAttackPanel(
58 | insets, self._messageEditor.getComponent())
59 | resultsPanel = self._constructResultsPanel(insets)
60 | aboutPanel = self._constructAboutPanel(insets)
61 | self._tabbedPane = JTabbedPane()
62 | self._tabbedPane.addTab("Attack", attackPanel)
63 | self._tabbedPane.addTab("Results", resultsPanel)
64 | self._tabbedPane.addTab("About", aboutPanel)
65 | callbacks.addSuiteTab(self)
66 |
67 | # Implement ITab
68 | def getTabCaption(self):
69 | return EXTENSION_NAME
70 |
71 | def getUiComponent(self):
72 | return self._tabbedPane
73 |
74 | # Implement IMessageEditorController
75 | def getHttpService(self):
76 | self._updateClassFromUI()
77 | return self._httpService
78 |
79 | def getRequest(self):
80 | self._updateClassFromUI()
81 | return self._request
82 |
83 | def getResponse(self):
84 | return None
85 |
86 | # Implement IContextMenuFactory
87 | def createMenuItems(self, contextMenuInvocation):
88 | messages = contextMenuInvocation.getSelectedMessages()
89 |
90 | # Only add menu item if a single request is selected
91 | if len(messages) == 1:
92 | self._contextMenuData = messages
93 | menu_item = JMenuItem(
94 | "Send to {}".format(EXTENSION_NAME),
95 | actionPerformed=self._contextMenuItemClicked
96 | )
97 | return [menu_item]
98 |
99 | def _contextMenuItemClicked(self, _):
100 | httpRequestResponse = self._contextMenuData[0]
101 |
102 | # Update instance variables with request data
103 | self._httpService = httpRequestResponse.getHttpService()
104 | self._request = httpRequestResponse.getRequest()
105 |
106 | # Update fields in tab
107 | self._hostTextField.setText(self._httpService.getHost())
108 | self._portTextField.setText(str(self._httpService.getPort()))
109 | self._protocolCheckBox.setSelected(
110 | True if self._httpService.getProtocol() == "https" else False)
111 | self._messageEditor.setMessage(self._request, True)
112 |
113 | def _startAttack(self, _):
114 |
115 | # Switch to results tab
116 | self._tabbedPane.setSelectedIndex(1)
117 |
118 | # Clear results table
119 | self._resultsTableModel.setRowCount(0)
120 |
121 | # Set progress bar to 0%
122 | self._progressBar.setValue(0)
123 |
124 | Thread(target=self._makeHttpRequests).start()
125 |
126 | def _makeHttpRequests(self):
127 |
128 | # Set class variables from values in UI
129 | self._updateClassFromUI()
130 |
131 | self._responses = {}
132 |
133 | # Set progress bar max to number of requests
134 | self._progressBar.setMaximum(len(self._payloads) * self._numReq)
135 |
136 | for payload in self._payloads:
137 | self._responses[payload] = []
138 | # Stick payload into request at specified position
139 | # Use lambda function for replacement string to stop slashes being
140 | # escaped
141 | request = sub("\xa7[^\xa7]*\xa7", lambda x: payload, self._request)
142 | request = self._updateContentLength(request)
143 | for _ in xrange(self._numReq):
144 | # Make request and work out how long it took in ms. This method
145 | # is crude, but it's as good as we can get with current Burp
146 | # APIs.
147 | # See https://support.portswigger.net/customer/portal/questions/16227838-request-response-timing # noqa: E501
148 | startTime = time()
149 | response = self._callbacks.makeHttpRequest(
150 | self._httpService, request)
151 | endTime = time()
152 | duration = (endTime - startTime) * 1000
153 |
154 | self._progressBar.setValue(self._progressBar.getValue() + 1)
155 |
156 | self._responses[payload].append(duration)
157 |
158 | # If all responses for this payload have
159 | # been added to array, add to results table.
160 | results = self._responses[payload]
161 | numReqs = self._numReq
162 | statusCode = response.getStatusCode()
163 | analysis = self._helpers.analyzeResponse(
164 | response.getResponse())
165 | for header in analysis.getHeaders():
166 | if header.lower().startswith("content-length"):
167 | content_length = int(header.split(": ")[1])
168 | meanTime = round(mean(results), 3)
169 | medianTime = round(median(results), 3)
170 | stdDevTime = round(stdDev(results), 3)
171 | minTime = int(min(results))
172 | maxTime = int(max(results))
173 | rowData = [
174 | payload, numReqs, statusCode,
175 | len(response.getResponse()), content_length, minTime,
176 | maxTime, meanTime, medianTime, stdDevTime]
177 | self._resultsTableModel.addRow(rowData)
178 |
179 | def _updateClassFromUI(self):
180 | host = self._hostTextField.text
181 | port = int(self._portTextField.text)
182 | protocol = "https" if self._protocolCheckBox.isSelected() else "http"
183 | # In an effort to prevent DNS queries introducing a delay, an attempt
184 | # was made to use the IP address of the destination web server instead
185 | # of the hostname when building the HttpService. Unfortunately it
186 | # caused issues with HTTPS requests, probably because of SNIs. As an
187 | # alternative, the hostname is resolved in the next line and hopefully
188 | # it will be cached at that point.
189 | gethostbyname(host)
190 |
191 | self._httpService = self._helpers.buildHttpService(
192 | host, port, protocol)
193 | self._request = self._messageEditor.getMessage()
194 | self._numReq = int(self._requestsNumTextField.text)
195 | self._payloads = set(self._payloadTextArea.text.split("\n"))
196 |
197 | def _addPayload(self, _):
198 | request = self._messageEditor.getMessage()
199 | selection = self._messageEditor.getSelectionBounds()
200 | if selection[0] == selection[1]:
201 | # No text selected so in/out points are same
202 | request.insert(selection[0], 0xa7)
203 | request.insert(selection[1], 0xa7)
204 | else:
205 | request.insert(selection[0], 0xa7)
206 | request.insert(selection[1]+1, 0xa7)
207 | self._messageEditor.setMessage(request, True)
208 |
209 | def _clearPayloads(self, _):
210 | request = self._messageEditor.getMessage()
211 | request = self._helpers.bytesToString(request).replace("\xa7", "")
212 | self._messageEditor.setMessage(request, True)
213 |
214 | def _updateContentLength(self, request):
215 | messageSize = len(request)
216 | bodyOffset = self._helpers.analyzeRequest(request).getBodyOffset()
217 | contentLength = messageSize - bodyOffset
218 | contentLengthHeader = "Content-Length: {}".format(contentLength)
219 | request = sub("Content-Length: \\d+", contentLengthHeader, request)
220 | return request
221 |
222 | def _constructAttackPanel(self, insets, messageEditorComponent):
223 | attackPanel = JPanel(GridBagLayout())
224 |
225 | targetHeadingLabel = JLabel("Target")
226 | targetHeadingLabelConstraints = GridBagConstraints()
227 | targetHeadingLabelConstraints.gridx = 0
228 | targetHeadingLabelConstraints.gridy = 0
229 | targetHeadingLabelConstraints.gridwidth = 4
230 | targetHeadingLabelConstraints.anchor = GridBagConstraints.LINE_START
231 | targetHeadingLabelConstraints.insets = insets
232 | attackPanel.add(targetHeadingLabel, targetHeadingLabelConstraints)
233 |
234 | startAttackButton = JButton("Start Attack",
235 | actionPerformed=self._startAttack)
236 | startAttackButtonConstraints = GridBagConstraints()
237 | startAttackButtonConstraints.gridx = 4
238 | startAttackButtonConstraints.gridy = 0
239 | startAttackButtonConstraints.insets = insets
240 | attackPanel.add(startAttackButton, startAttackButtonConstraints)
241 |
242 | hostLabel = JLabel("Host:")
243 | hostLabelConstraints = GridBagConstraints()
244 | hostLabelConstraints.gridx = 0
245 | hostLabelConstraints.gridy = 1
246 | hostLabelConstraints.anchor = GridBagConstraints.LINE_START
247 | hostLabelConstraints.insets = insets
248 | attackPanel.add(hostLabel, hostLabelConstraints)
249 |
250 | self._hostTextField = JTextField(25)
251 | self._hostTextField.setMinimumSize(
252 | self._hostTextField.getPreferredSize())
253 | hostTextFieldConstraints = GridBagConstraints()
254 | hostTextFieldConstraints.gridx = 1
255 | hostTextFieldConstraints.gridy = 1
256 | hostTextFieldConstraints.weightx = 1
257 | hostTextFieldConstraints.gridwidth = 2
258 | hostTextFieldConstraints.anchor = GridBagConstraints.LINE_START
259 | hostTextFieldConstraints.insets = insets
260 | attackPanel.add(self._hostTextField, hostTextFieldConstraints)
261 |
262 | portLabel = JLabel("Port:")
263 | portLabelConstraints = GridBagConstraints()
264 | portLabelConstraints.gridx = 0
265 | portLabelConstraints.gridy = 2
266 | portLabelConstraints.anchor = GridBagConstraints.LINE_START
267 | portLabelConstraints.insets = insets
268 | attackPanel.add(portLabel, portLabelConstraints)
269 |
270 | self._portTextField = JTextField(5)
271 | self._portTextField.setMinimumSize(
272 | self._portTextField.getPreferredSize())
273 | portTextFieldConstraints = GridBagConstraints()
274 | portTextFieldConstraints.gridx = 1
275 | portTextFieldConstraints.gridy = 2
276 | portTextFieldConstraints.gridwidth = 2
277 | portTextFieldConstraints.anchor = GridBagConstraints.LINE_START
278 | portTextFieldConstraints.insets = insets
279 | attackPanel.add(self._portTextField, portTextFieldConstraints)
280 |
281 | self._protocolCheckBox = JCheckBox("Use HTTPS")
282 | protocolCheckBoxConstraints = GridBagConstraints()
283 | protocolCheckBoxConstraints.gridx = 0
284 | protocolCheckBoxConstraints.gridy = 3
285 | protocolCheckBoxConstraints.gridwidth = 3
286 | protocolCheckBoxConstraints.anchor = GridBagConstraints.LINE_START
287 | protocolCheckBoxConstraints.insets = insets
288 | attackPanel.add(self._protocolCheckBox, protocolCheckBoxConstraints)
289 |
290 | requestHeadingLabel = JLabel("Request")
291 | requestHeadingLabelConstraints = GridBagConstraints()
292 | requestHeadingLabelConstraints.gridx = 0
293 | requestHeadingLabelConstraints.gridy = 4
294 | requestHeadingLabelConstraints.gridwidth = 4
295 | requestHeadingLabelConstraints.anchor = GridBagConstraints.LINE_START
296 | requestHeadingLabelConstraints.insets = insets
297 | attackPanel.add(requestHeadingLabel, requestHeadingLabelConstraints)
298 |
299 | messageEditorComponentConstraints = GridBagConstraints()
300 | messageEditorComponentConstraints.gridx = 0
301 | messageEditorComponentConstraints.gridy = 5
302 | messageEditorComponentConstraints.weightx = 1
303 | messageEditorComponentConstraints.weighty = .75
304 | messageEditorComponentConstraints.gridwidth = 4
305 | messageEditorComponentConstraints.gridheight = 2
306 | messageEditorComponentConstraints.fill = GridBagConstraints.BOTH
307 | messageEditorComponentConstraints.insets = insets
308 | attackPanel.add(
309 | messageEditorComponent, messageEditorComponentConstraints)
310 |
311 | addPayloadButton = JButton(
312 | "Add \xa7", actionPerformed=self._addPayload)
313 | addPayloadButtonConstraints = GridBagConstraints()
314 | addPayloadButtonConstraints.gridx = 4
315 | addPayloadButtonConstraints.gridy = 5
316 | addPayloadButtonConstraints.fill = GridBagConstraints.HORIZONTAL
317 | addPayloadButtonConstraints.insets = insets
318 | attackPanel.add(addPayloadButton, addPayloadButtonConstraints)
319 |
320 | clearPayloadButton = JButton(
321 | "Clear \xa7", actionPerformed=self._clearPayloads)
322 | clearPayloadButtonConstraints = GridBagConstraints()
323 | clearPayloadButtonConstraints.gridx = 4
324 | clearPayloadButtonConstraints.gridy = 6
325 | clearPayloadButtonConstraints.anchor = GridBagConstraints.PAGE_START
326 | clearPayloadButtonConstraints.fill = GridBagConstraints.HORIZONTAL
327 | clearPayloadButtonConstraints.insets = insets
328 | attackPanel.add(clearPayloadButton, clearPayloadButtonConstraints)
329 |
330 | payloadHeadingLabel = JLabel("Payloads")
331 | payloadHeadingLabelConstraints = GridBagConstraints()
332 | payloadHeadingLabelConstraints.gridx = 0
333 | payloadHeadingLabelConstraints.gridy = 7
334 | payloadHeadingLabelConstraints.gridwidth = 4
335 | payloadHeadingLabelConstraints.anchor = GridBagConstraints.LINE_START
336 | payloadHeadingLabelConstraints.insets = insets
337 | attackPanel.add(payloadHeadingLabel, payloadHeadingLabelConstraints)
338 |
339 | self._payloadTextArea = JTextArea()
340 | payloadScrollPane = JScrollPane(self._payloadTextArea)
341 | payloadScrollPaneConstraints = GridBagConstraints()
342 | payloadScrollPaneConstraints.gridx = 0
343 | payloadScrollPaneConstraints.gridy = 8
344 | payloadScrollPaneConstraints.weighty = .25
345 | payloadScrollPaneConstraints.gridwidth = 3
346 | payloadScrollPaneConstraints.fill = GridBagConstraints.BOTH
347 | payloadScrollPaneConstraints.insets = insets
348 | attackPanel.add(payloadScrollPane, payloadScrollPaneConstraints)
349 |
350 | requestsNumLabel = JLabel("Number of requests for each payload:")
351 | requestsNumLabelConstraints = GridBagConstraints()
352 | requestsNumLabelConstraints.gridx = 0
353 | requestsNumLabelConstraints.gridy = 9
354 | requestsNumLabelConstraints.gridwidth = 2
355 | requestsNumLabelConstraints.anchor = GridBagConstraints.LINE_START
356 | requestsNumLabelConstraints.insets = insets
357 | attackPanel.add(requestsNumLabel, requestsNumLabelConstraints)
358 |
359 | self._requestsNumTextField = JTextField("100", 4)
360 | self._requestsNumTextField.setMinimumSize(
361 | self._requestsNumTextField.getPreferredSize())
362 | requestsNumTextFieldConstraints = GridBagConstraints()
363 | requestsNumTextFieldConstraints.gridx = 2
364 | requestsNumTextFieldConstraints.gridy = 9
365 | requestsNumTextFieldConstraints.anchor = GridBagConstraints.LINE_START
366 | requestsNumTextFieldConstraints.insets = insets
367 | attackPanel.add(
368 | self._requestsNumTextField, requestsNumTextFieldConstraints)
369 |
370 | return attackPanel
371 |
372 | def _constructResultsPanel(self, insets):
373 | resultsPanel = JPanel(GridBagLayout())
374 |
375 | self._progressBar = JProgressBar()
376 | self._progressBar.setStringPainted(True)
377 | self._progressBar.setMinimum(0)
378 | progressBarContraints = GridBagConstraints()
379 | progressBarContraints.gridx = 0
380 | progressBarContraints.gridy = 0
381 | progressBarContraints.fill = GridBagConstraints.HORIZONTAL
382 |
383 | resultsPanel.add(self._progressBar, progressBarContraints)
384 |
385 | self._resultsTableModel = ResultsTableModel(COLUMNS, 0)
386 | resultsTable = JTable(self._resultsTableModel)
387 | resultsTable.setAutoCreateRowSorter(True)
388 | cellRenderer = ColoredTableCellRenderer()
389 | for index in [5, 6, 7, 8, 9]:
390 | column = resultsTable.columnModel.getColumn(index)
391 | column.cellRenderer = cellRenderer
392 | resultsTable.getColumnModel().getColumn(0).setPreferredWidth(99999999)
393 | resultsTable.getColumnModel().getColumn(1).setMinWidth(160)
394 | resultsTable.getColumnModel().getColumn(2).setMinWidth(100)
395 | resultsTable.getColumnModel().getColumn(3).setMinWidth(80)
396 | resultsTable.getColumnModel().getColumn(4).setMinWidth(80)
397 | resultsTable.getColumnModel().getColumn(5).setMinWidth(110)
398 | resultsTable.getColumnModel().getColumn(6).setMinWidth(110)
399 | resultsTable.getColumnModel().getColumn(7).setMinWidth(90)
400 | resultsTable.getColumnModel().getColumn(8).setMinWidth(110)
401 | resultsTable.getColumnModel().getColumn(9).setMinWidth(110)
402 | resultsTable.setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS)
403 | resultsScrollPane = JScrollPane(resultsTable)
404 | resultsScrollPaneConstraints = GridBagConstraints()
405 | resultsScrollPaneConstraints.gridx = 0
406 | resultsScrollPaneConstraints.gridy = 1
407 | resultsScrollPaneConstraints.weightx = 1
408 | resultsScrollPaneConstraints.weighty = 1
409 | resultsScrollPaneConstraints.fill = GridBagConstraints.BOTH
410 | resultsPanel.add(resultsScrollPane, resultsScrollPaneConstraints)
411 |
412 | return resultsPanel
413 |
414 | def _constructAboutPanel(self, insets):
415 | aboutPanel = JPanel(GridBagLayout())
416 | with open("about.html") as file:
417 | aboutBody = file.read()
418 | aboutLabel = JLabel(
419 | aboutBody.format(extension_name=EXTENSION_NAME))
420 | aboutLabelConstraints = GridBagConstraints()
421 | aboutLabelConstraints.weightx = 1
422 | aboutLabelConstraints.weighty = 1
423 | aboutLabelConstraints.insets = insets
424 | aboutLabelConstraints.fill = GridBagConstraints.HORIZONTAL
425 | aboutLabelConstraints.anchor = GridBagConstraints.PAGE_START
426 | aboutPanel.add(aboutLabel, aboutLabelConstraints)
427 |
428 | return aboutPanel
429 |
430 |
431 | # Required for coloured cells
432 | class ColoredTableCellRenderer(DefaultTableCellRenderer):
433 | def getTableCellRendererComponent(
434 | self, table, value, isSelected, hasFocus, row, column):
435 |
436 | renderer = DefaultTableCellRenderer.getTableCellRendererComponent(
437 | self, table, value, isSelected, hasFocus, row, column)
438 |
439 | value = table.getValueAt(row, column)
440 | model = table.getModel()
441 | rowsCount = model.getRowCount()
442 | if rowsCount == 1:
443 | renderer.setBackground(table.getBackground())
444 | renderer.setForeground(table.getForeground())
445 | else:
446 | colValues = []
447 | for index in xrange(rowsCount):
448 | valueAtIndex = model.getValueAt(index, column)
449 | colValues.append(valueAtIndex)
450 | minBound = min(colValues)
451 | maxBound = max(colValues)
452 | if minBound != maxBound:
453 | valueAsFraction = (
454 | float(value - minBound) / (maxBound - minBound))
455 | if valueAsFraction > 0.75:
456 | renderer.setForeground(Color.WHITE)
457 | else:
458 | renderer.setForeground(Color.BLACK)
459 | if valueAsFraction > 0.5:
460 | red = 1.0
461 | else:
462 | red = (valueAsFraction * 2.0)
463 | if valueAsFraction < 0.5:
464 | green = 1.0
465 | else:
466 | green = 2 - (valueAsFraction * 2.0)
467 | blue = 111/256.0
468 |
469 | if isSelected:
470 | red = max(0.0, red-0.25)
471 | green = max(0.0, green-0.25)
472 | blue = max(0.0, blue-0.25)
473 |
474 | renderer.setBackground(Color(red, green, blue))
475 | return renderer
476 |
477 |
478 | # Required for proper sorting
479 | class ResultsTableModel(DefaultTableModel):
480 |
481 | # Native java types are required here for proper sorting
482 | _types = [
483 | java.lang.String,
484 | java.lang.Integer,
485 | java.lang.Integer,
486 | java.lang.Integer,
487 | java.lang.Integer,
488 | java.lang.Integer,
489 | java.lang.Integer,
490 | java.lang.Float,
491 | java.lang.Float,
492 | java.lang.Float]
493 |
494 | def getColumnClass(self, column):
495 | return self._types[column]
496 |
--------------------------------------------------------------------------------