├── 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 |
  1. Enable the extension in order to add a tab titled {extension_name} to the Burp Suite user interface.
  2. 12 |
  3. 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.
  4. 13 |
  5. 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.
  6. 14 |
  7. Choose how many requests to make for each payload.
  8. 15 |
  9. Type or copy and paste the payloads into the "Payloads" text area. Place a single payload on each new line.
  10. 16 |
  11. 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.
  12. 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 | 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 | --------------------------------------------------------------------------------