├── .gitignore ├── Context.sublime-menu ├── Default (Linux).sublime-keymap ├── Default (OSX).sublime-keymap ├── Default (Windows).sublime-keymap ├── Default.sublime-commands ├── README.md └── http_requester.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | -------------------------------------------------------------------------------- /Context.sublime-menu: -------------------------------------------------------------------------------- 1 | [ 2 | { "command": "http_requester" } 3 | ] 4 | -------------------------------------------------------------------------------- /Default (Linux).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "keys": ["ctrl+alt+r"], 4 | "command": "http_requester" 5 | }, 6 | { 7 | "keys": ["f5"], 8 | "command": "http_requester_refresh" 9 | } 10 | ] 11 | -------------------------------------------------------------------------------- /Default (OSX).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "keys": ["super+alt+r"], 4 | "command": "http_requester" 5 | }, 6 | { 7 | "keys": ["f5"], 8 | "command": "http_requester_refresh" 9 | } 10 | ] 11 | -------------------------------------------------------------------------------- /Default (Windows).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "keys": ["ctrl+alt+r"], 4 | "command": "http_requester" 5 | }, 6 | { 7 | "keys": ["f5"], 8 | "command": "http_requester_refresh" 9 | } 10 | ] 11 | -------------------------------------------------------------------------------- /Default.sublime-commands: -------------------------------------------------------------------------------- 1 | [{ 2 | "caption": "HTTP Requester", 3 | "command": "http_requester" 4 | } 5 | ] 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | *** 2 | # SublimeHttpRequester - HTTP client plugin for Sublime Text 2 & 3 3 | *** 4 | ==================== 5 | 6 | Contact: [braindamageinc@gmail.com](mailto:braindamageinc@gmail.com) 7 | 8 | ## Summary 9 | Makes HTTP requests using the selected text as URL + headers. Useful for testing REST APIs from Sublime Text 2 editor. 10 | 11 | ## Update: Added latency and download time output. 12 | 13 | ## Usage 14 | Select the text that represents an URL. Examples of requests: 15 | 16 | http://www.google.com/search?q=test 17 | GET http://www.google.com/search?q=test 18 | www.google.com/search?q=test 19 | 20 | If you need to add extra headers just add them below the URL line, one on each line: 21 | 22 | www.google.com/search?q=test 23 | Accept: text/plain 24 | Cookie : SOME_COOKIE 25 | 26 | Use the right-click context menu command *Http Requester* or the keyboard shortcut *CTRL + ALT + R* ( *COMMAND + ALT + R* on Mac OS X ). 27 | Update: *F5* refreshes last request. 28 | 29 | ### POST/PUT usage 30 | Just add **POST_BODY:** after any extra headers and the body on the following lines: 31 | 32 | POST http://posttestserver.com/post.php 33 | POST_BODY: 34 | this is the body that will be sent via HTTP POST 35 | a second line for body message 36 | 37 | If you want to POST form variables: 38 | 39 | POST http://posttestserver.com/post.php 40 | Content-type: application/x-www-form-urlencoded 41 | POST_BODY: 42 | variable1=avalue&variable2=1234&variable3=anothervalue 43 | 44 | For PUT: 45 | 46 | PUT http://yoururl.com/puthere 47 | POST_BODY: 48 | this body will be sent via HTTP PUT 49 | 50 | ### DELETE usage 51 | Same as HTTP GET: 52 | 53 | DELETE http://yoururl.com/deletethis 54 | 55 | ### Requesting through a proxy 56 | If you need to send the request through a proxy server you can use: 57 | 58 | GET www.yourtest.com 59 | USE_PROXY: 127.0.0.1:1234 60 | 61 | Where *127.0.0.1* is the proxy server address (IP or URL) followed by the port number. **Warning** : allways append a port number, even if it's *80* 62 | 63 | ### Using client SSL certificates 64 | If you need client SSL certification you can use: 65 | 66 | GET https://yoursecureserver.com 67 | CLIENT_SSL_CERT: certif_file.pem 68 | CLIENT_SSL_KEY: key_file.key 69 | 70 | ### Using html charset 71 | If you need to make a request for a page with a specific encoding such as cyrillic you can use: 72 | 73 | GET https://yoursecureserver.com 74 | CHARSET: cp1251 75 | 76 | ### Show results in the same results tab 77 | If you wish to have all the requests responses in the same file (tab), you can use the following param: 78 | 79 | GET http://someserver.com 80 | SAME_FILE: True 81 | 82 | ### Set custom timeout 83 | For a custom request timeout value, use the following param (timeout in **seconds**): 84 | 85 | GET http://someserver.com 86 | TIMEOUT: 5 87 | 88 | 89 | ## Installation 90 | Using the Sublime Text 2/3 Package Control plugin (http://wbond.net/sublime_packages/package_control) 91 | press *CTRL + SHIFT + P* and find **Package Control: Install Package** and press *Enter*. 92 | Find this plugin in the list by name **Http Requester**. 93 | 94 | Or git clone to your Sublime Text 2/3 packages folder directly (usually located at /Sublime Text 2/Packages/ or /Sublime Text 3/Packages/). 95 | -------------------------------------------------------------------------------- /http_requester.py: -------------------------------------------------------------------------------- 1 | import httplib 2 | import sublime 3 | import sublime_plugin 4 | import socket 5 | import types 6 | import threading 7 | import time 8 | 9 | gPrevHttpRequest = "" 10 | 11 | CHECK_DOWNLOAD_THREAD_TIME_MS = 1000 12 | 13 | 14 | def monitorDownloadThread(downloadThread): 15 | if downloadThread.is_alive(): 16 | msg = downloadThread.getCurrentMessage() 17 | sublime.status_message(msg) 18 | sublime.set_timeout(lambda: monitorDownloadThread(downloadThread), CHECK_DOWNLOAD_THREAD_TIME_MS) 19 | else: 20 | downloadThread.showResultToPresenter() 21 | 22 | 23 | class HttpRequester(threading.Thread): 24 | 25 | REQUEST_TYPE_GET = "GET" 26 | REQUEST_TYPE_POST = "POST" 27 | REQUEST_TYPE_DELETE = "DELETE" 28 | REQUEST_TYPE_PUT = "PUT" 29 | 30 | httpRequestTypes = [REQUEST_TYPE_GET, REQUEST_TYPE_POST, REQUEST_TYPE_PUT, REQUEST_TYPE_DELETE] 31 | 32 | HTTP_URL = "http://" 33 | HTTPS_URL = "https://" 34 | 35 | httpProtocolTypes = [HTTP_URL, HTTPS_URL] 36 | 37 | HTTP_POST_BODY_START = "POST_BODY:" 38 | 39 | HTTP_PROXY_HEADER = "USE_PROXY" 40 | 41 | HTTPS_SSL_CLIENT_CERT = "CLIENT_SSL_CERT" 42 | HTTPS_SSL_CLIENT_KEY = "CLIENT_SSL_KEY" 43 | 44 | CONTENT_LENGTH_HEADER = "Content-lenght" 45 | 46 | MAX_BYTES_BUFFER_SIZE = 8192 47 | 48 | FILE_TYPE_HTML = "html" 49 | FILE_TYPE_JSON = "json" 50 | FILE_TYPE_XML = "xml" 51 | 52 | HTML_CHARSET_HEADER = "CHARSET" 53 | htmlCharset = "utf-8" 54 | 55 | httpContentTypes = [FILE_TYPE_HTML, FILE_TYPE_JSON, FILE_TYPE_XML] 56 | 57 | HTML_SHOW_RESULTS_SAME_FILE_HEADER = "SAME_FILE" 58 | showResultInSameFile = False 59 | 60 | DEFAULT_TIMEOUT = 10 61 | TIMEOUT_KEY = "TIMEOUT" 62 | 63 | def __init__(self, resultsPresenter): 64 | self.totalBytesDownloaded = 0 65 | self.contentLenght = 0 66 | self.resultsPresenter = resultsPresenter 67 | threading.Thread.__init__(self) 68 | 69 | def request(self, selection): 70 | self.selection = selection 71 | self.start() 72 | sublime.set_timeout(lambda: monitorDownloadThread(self), CHECK_DOWNLOAD_THREAD_TIME_MS) 73 | 74 | def run(self): 75 | FAKE_CURL_UA = "curl/7.21.0 (i486-pc-linux-gnu) libcurl/7.21.0 OpenSSL/0.9.8o zlib/1.2.3.4 libidn/1.15 libssh2/1.2.6" 76 | 77 | selection = self.selection 78 | 79 | lines = selection.split("\n") 80 | 81 | # trim any whitespaces for all lines and remove lines starting with a pound character 82 | for idx in range(len(lines) - 1, -1, -1): 83 | lines[idx] = lines[idx].lstrip() 84 | lines[idx] = lines[idx].rstrip() 85 | if (len(lines[idx]) > 0): 86 | if lines[idx][0] == "#": 87 | del lines[idx] 88 | 89 | # get request web address and req. type from the first line 90 | (url, port, request_page, requestType, httpProtocol) = self.extractRequestParams(lines[0]) 91 | 92 | print "Requesting...." 93 | print requestType, " ", httpProtocol, " HOST ", url, " PORT ", port, " PAGE: ", request_page 94 | 95 | # get request headers from the lines below the http address 96 | (extra_headers, requestPOSTBody, proxyURL, proxyPort, clientSSLCertificateFile, 97 | clientSSLKeyFile, timeoutValue) = self.extractExtraHeaders(lines) 98 | 99 | headers = {"User-Agent": FAKE_CURL_UA, "Accept": "*/*"} 100 | 101 | for key in extra_headers: 102 | headers[key] = extra_headers[key] 103 | 104 | # if valid POST body add Content-lenght header 105 | if len(requestPOSTBody) > 0: 106 | headers[self.CONTENT_LENGTH_HEADER] = len(requestPOSTBody) 107 | requestPOSTBody = requestPOSTBody.encode('utf-8') 108 | 109 | 110 | for key in headers: 111 | print "REQ HEADERS ", key, " : ", headers[key] 112 | 113 | respText = "" 114 | fileType = "" 115 | 116 | useProxy = False 117 | if len(proxyURL) > 0: 118 | useProxy = True 119 | 120 | # make http request 121 | try: 122 | if not(useProxy): 123 | if httpProtocol == self.HTTP_URL: 124 | conn = httplib.HTTPConnection(url, port, timeout=timeoutValue) 125 | else: 126 | if len(clientSSLCertificateFile) > 0 or len(clientSSLKeyFile) > 0: 127 | print "Using client SSL certificate: ", clientSSLCertificateFile 128 | print "Using client SSL key file: ", clientSSLKeyFile 129 | conn = httplib.HTTPSConnection( 130 | url, port, timeout=timeoutValue, cert_file=clientSSLCertificateFile, key_file=clientSSLKeyFile) 131 | else: 132 | conn = httplib.HTTPSConnection(url, port, timeout=timeoutValue) 133 | 134 | conn.request(requestType, request_page, requestPOSTBody, headers) 135 | else: 136 | print "Using proxy: ", proxyURL + ":" + str(proxyPort) 137 | conn = httplib.HTTPConnection(proxyURL, proxyPort, timeout=timeoutValue) 138 | conn.request(requestType, httpProtocol + url + request_page, requestPOSTBody, headers) 139 | 140 | startReqTime = time.time() 141 | resp = conn.getresponse() 142 | endReqTime = time.time() 143 | 144 | startDownloadTime = time.time() 145 | (respHeaderText, respBodyText, fileType) = self.getParsedResponse(resp) 146 | endDownloadTime = time.time() 147 | 148 | latencyTimeMilisec = int((endReqTime - startReqTime) * 1000) 149 | downloadTimeMilisec = int((endDownloadTime - startDownloadTime) * 1000) 150 | 151 | respText = self.getResponseTextForPresentation(respHeaderText, respBodyText, latencyTimeMilisec, downloadTimeMilisec) 152 | 153 | conn.close() 154 | except (socket.error, httplib.HTTPException, socket.timeout) as e: 155 | if not(isinstance(e, types.NoneType)): 156 | respText = "Error connecting: " + str(e) 157 | else: 158 | respText = "Error connecting" 159 | except AttributeError as e: 160 | print e 161 | respText = "HTTPS not supported by your Python version" 162 | 163 | self.respText = respText 164 | self.fileType = fileType 165 | 166 | def extractHttpRequestType(self, line): 167 | for type in self.httpRequestTypes: 168 | if line.find(type) == 0: 169 | return type 170 | 171 | return "" 172 | 173 | def extractWebAdressPart(self, line): 174 | webAddress = "" 175 | for protocol in self.httpProtocolTypes: 176 | requestPartions = line.partition(protocol) 177 | if requestPartions[1] == "": 178 | webAddress = requestPartions[0] 179 | else: 180 | webAddress = requestPartions[2] 181 | return (webAddress, protocol) 182 | 183 | return (webAddress, self.HTTP_URL) 184 | 185 | def extractRequestParams(self, requestLine): 186 | requestType = self.extractHttpRequestType(requestLine) 187 | if requestType == "": 188 | requestType = self.REQUEST_TYPE_GET 189 | else: 190 | partition = requestLine.partition(requestType) 191 | requestLine = partition[2].lstrip() 192 | 193 | # remove http:// or https:// from URL 194 | (webAddress, protocol) = self.extractWebAdressPart(requestLine) 195 | 196 | request_parts = webAddress.split("/") 197 | request_page = "" 198 | if len(request_parts) > 1: 199 | for idx in range(1, len(request_parts)): 200 | request_page = request_page + "/" + request_parts[idx] 201 | else: 202 | request_page = "/" 203 | 204 | url_parts = request_parts[0].split(":") 205 | 206 | url_idx = 0 207 | url = url_parts[url_idx] 208 | 209 | if protocol == self.HTTP_URL: 210 | port = httplib.HTTP_PORT 211 | else: 212 | port = httplib.HTTPS_PORT 213 | 214 | if len(url_parts) > url_idx + 1: 215 | port = int(url_parts[url_idx + 1]) 216 | 217 | # convert requested page to utf-8 and replace spaces with + 218 | request_page = request_page.encode('utf-8') 219 | request_page = request_page.replace(' ', '+') 220 | 221 | return (url, port, request_page, requestType, protocol) 222 | 223 | def getHeaderNameAndValueFromLine(self, line): 224 | readingPOSTBody = False 225 | 226 | line = line.lstrip() 227 | line = line.rstrip() 228 | 229 | if line == self.HTTP_POST_BODY_START: 230 | readingPOSTBody = True 231 | else: 232 | header_parts = line.split(":") 233 | if len(header_parts) == 2: 234 | header_name = header_parts[0].rstrip() 235 | header_value = header_parts[1].lstrip() 236 | return (header_name, header_value, readingPOSTBody) 237 | else: 238 | # may be proxy address URL:port 239 | if len(header_parts) > 2: 240 | header_name = header_parts[0].rstrip() 241 | header_value = header_parts[1] 242 | header_value = header_value.lstrip() 243 | header_value = header_value.rstrip() 244 | for idx in range(2, len(header_parts)): 245 | currentValue = header_parts[idx] 246 | currentValue = currentValue.lstrip() 247 | currentValue = currentValue.rstrip() 248 | header_value = header_value + ":" + currentValue 249 | 250 | return (header_name, header_value, readingPOSTBody) 251 | 252 | return (None, None, readingPOSTBody) 253 | 254 | def extractExtraHeaders(self, headerLines): 255 | requestPOSTBody = "" 256 | readingPOSTBody = False 257 | lastLine = False 258 | numLines = len(headerLines) 259 | 260 | proxyURL = "" 261 | proxyPort = 0 262 | 263 | clientSSLCertificateFile = "" 264 | clientSSLKeyFile = "" 265 | 266 | timeoutValue = self.DEFAULT_TIMEOUT 267 | 268 | extra_headers = {} 269 | 270 | if len(headerLines) > 1: 271 | for i in range(1, numLines): 272 | lastLine = (i == numLines - 1) 273 | if not(readingPOSTBody): 274 | (header_name, header_value, readingPOSTBody) = self.getHeaderNameAndValueFromLine(headerLines[i]) 275 | if header_name is not None: 276 | if header_name == self.HTTP_PROXY_HEADER: 277 | (proxyURL, proxyPort) = self.getProxyURLandPort(header_value) 278 | elif header_name == self.HTTPS_SSL_CLIENT_CERT: 279 | clientSSLCertificateFile = header_value 280 | elif header_name == self.HTTPS_SSL_CLIENT_KEY: 281 | clientSSLKeyFile = header_value 282 | elif header_name == self.HTML_CHARSET_HEADER: 283 | self.htmlCharset = header_value 284 | elif header_name == self.HTML_SHOW_RESULTS_SAME_FILE_HEADER: 285 | boolDict = {"true": True, "false": False} 286 | self.showResultInSameFile = boolDict.get(header_value.lower()) 287 | elif header_name == self.TIMEOUT_KEY: 288 | timeoutValue = int(header_value) 289 | else: 290 | extra_headers[header_name] = header_value 291 | else: # read all following lines as HTTP POST body 292 | lineBreak = "" 293 | if not(lastLine): 294 | lineBreak = "\n" 295 | 296 | requestPOSTBody = requestPOSTBody + headerLines[i] + lineBreak 297 | 298 | return (extra_headers, requestPOSTBody, proxyURL, proxyPort, clientSSLCertificateFile, clientSSLKeyFile, timeoutValue) 299 | 300 | def getProxyURLandPort(self, proxyAddress): 301 | proxyURL = "" 302 | proxyPort = 0 303 | 304 | proxyParts = proxyAddress.split(":") 305 | 306 | proxyURL = proxyParts[0] 307 | 308 | if len(proxyParts) > 1: 309 | proxyURL = proxyParts[0] 310 | for idx in range(1, len(proxyParts) - 1): 311 | proxyURL = proxyURL + ":" + proxyParts[idx] 312 | 313 | lastIdx = len(proxyParts) - 1 314 | proxyPort = int(proxyParts[lastIdx]) 315 | else: 316 | proxyPort = 80 317 | 318 | return (proxyURL, proxyPort) 319 | 320 | def getParsedResponse(self, resp): 321 | fileType = self.FILE_TYPE_HTML 322 | resp_status = "%d " % resp.status + resp.reason + "\n" 323 | respHeaderText = resp_status 324 | 325 | for header in resp.getheaders(): 326 | respHeaderText += header[0] + ":" + header[1] + "\n" 327 | 328 | # get resp. file type (html, json and xml supported). fallback to html 329 | if header[0] == "content-type": 330 | fileType = self.getFileTypeFromContentType(header[1]) 331 | 332 | respBodyText = "" 333 | self.contentLenght = int(resp.getheader("content-length", 0)) 334 | 335 | # download a 8KB buffer at a time 336 | respBody = resp.read(self.MAX_BYTES_BUFFER_SIZE) 337 | numDownloaded = len(respBody) 338 | self.totalBytesDownloaded = numDownloaded 339 | while numDownloaded == self.MAX_BYTES_BUFFER_SIZE: 340 | data = resp.read(self.MAX_BYTES_BUFFER_SIZE) 341 | respBody += data 342 | numDownloaded = len(data) 343 | self.totalBytesDownloaded += numDownloaded 344 | 345 | respBodyText += respBody.decode(self.htmlCharset, "replace") 346 | 347 | return (respHeaderText, respBodyText, fileType) 348 | 349 | def getFileTypeFromContentType(self, contentType): 350 | fileType = self.FILE_TYPE_HTML 351 | contentType = contentType.lower() 352 | 353 | print "File type: ", contentType 354 | 355 | for cType in self.httpContentTypes: 356 | if cType in contentType: 357 | fileType = cType 358 | 359 | return fileType 360 | 361 | def getResponseTextForPresentation(self, respHeaderText, respBodyText, latencyTimeMilisec, downloadTimeMilisec): 362 | return respHeaderText + "\n" + "Latency: " + str(latencyTimeMilisec) + "ms" + "\n" + "Download time:" + str(downloadTimeMilisec) + "ms" + "\n\n\n" + respBodyText 363 | 364 | def getCurrentMessage(self): 365 | return "HttpRequester downloading " + str(self.totalBytesDownloaded) + " / " + str(self.contentLenght) 366 | 367 | def showResultToPresenter(self): 368 | self.resultsPresenter.createWindowWithText(self.respText, self.fileType, self.showResultInSameFile) 369 | 370 | 371 | class HttpRequesterRefreshCommand(sublime_plugin.TextCommand): 372 | 373 | def run(self, edit): 374 | global gPrevHttpRequest 375 | selection = gPrevHttpRequest 376 | 377 | resultsPresenter = ResultsPresenter() 378 | httpRequester = HttpRequester(resultsPresenter) 379 | httpRequester.request(selection) 380 | 381 | 382 | class ResultsPresenter(): 383 | 384 | def __init__(self): 385 | pass 386 | 387 | def createWindowWithText(self, textToDisplay, fileType, showResultInSameFile): 388 | if not(showResultInSameFile): 389 | view = sublime.active_window().new_file() 390 | openedNewView = True 391 | else: 392 | view = self.findHttpResponseView() 393 | openedNewView = False 394 | if view is None: 395 | view = sublime.active_window().new_file() 396 | openedNewView = True 397 | 398 | edit = view.begin_edit() 399 | if not(openedNewView): 400 | view.insert(edit, 0, "\n\n\n") 401 | view.insert(edit, 0, textToDisplay) 402 | view.end_edit(edit) 403 | view.set_scratch(True) 404 | view.set_read_only(False) 405 | view.set_name("http response") 406 | 407 | if fileType == HttpRequester.FILE_TYPE_HTML: 408 | view.set_syntax_file("Packages/HTML/HTML.tmLanguage") 409 | if fileType == HttpRequester.FILE_TYPE_JSON: 410 | view.set_syntax_file("Packages/JavaScript/JSON.tmLanguage") 411 | if fileType == HttpRequester.FILE_TYPE_XML: 412 | view.set_syntax_file("Packages/XML/XML.tmLanguage") 413 | 414 | return view.id() 415 | 416 | def findHttpResponseView(self): 417 | for window in sublime.windows(): 418 | for view in window.views(): 419 | if view.name() == "http response": 420 | return view 421 | 422 | return None 423 | 424 | 425 | class HttpRequesterCommand(sublime_plugin.TextCommand): 426 | 427 | def run(self, edit): 428 | global gPrevHttpRequest 429 | selection = "" 430 | if self.has_selection(): 431 | for region in self.view.sel(): 432 | # Concatenate selected regions together. 433 | selection += self.view.substr(region) 434 | else: 435 | # Use entire document as selection 436 | entireDocument = sublime.Region(0, self.view.size()) 437 | selection = self.view.substr(entireDocument) 438 | 439 | gPrevHttpRequest = selection 440 | resultsPresenter = ResultsPresenter() 441 | httpRequester = HttpRequester(resultsPresenter) 442 | httpRequester.request(selection) 443 | 444 | def has_selection(self): 445 | has_selection = False 446 | 447 | # Only enable menu option if at least one region contains selected text. 448 | for region in self.view.sel(): 449 | if not region.empty(): 450 | has_selection = True 451 | 452 | return has_selection 453 | --------------------------------------------------------------------------------