├── __init__.py ├── rester ├── __init__.py ├── constants.py ├── commands │ ├── set_syntax_command.py │ ├── __init__.py │ ├── auto_form_encode_command.py │ └── http_request_command.py ├── overrideable.py ├── reloader.py ├── util.py ├── phantoms.py ├── message.py ├── parse.py └── http.py ├── .gitignore ├── messages.json ├── Default (OSX).sublime-keymap ├── Default (Linux).sublime-keymap ├── Default (Windows).sublime-keymap ├── Default.sublime-commands ├── messages ├── 1.4.3.txt ├── 1.8.2.txt ├── 1.7.0.txt ├── 1.8.0.txt ├── 1.4.0.txt ├── 1.5.0.txt └── 1.6.0.txt ├── LICENSE ├── Main.sublime-menu ├── RESTer.py ├── RESTer.sublime-settings ├── http.tmLanguage └── README.md /__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /rester/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .idea 3 | -------------------------------------------------------------------------------- /messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "1.4.0": "messages/1.4.0.txt" 3 | } 4 | -------------------------------------------------------------------------------- /rester/constants.py: -------------------------------------------------------------------------------- 1 | SETTINGS_FILE = "RESTer.sublime-settings" 2 | SYNTAX_FILE = "Packages/RESTer HTTP Client/http.tmLanguage" 3 | -------------------------------------------------------------------------------- /Default (OSX).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "keys": ["ctrl+alt+r"], 4 | "command": "rester_http_request" 5 | } 6 | ] 7 | -------------------------------------------------------------------------------- /Default (Linux).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "keys": ["ctrl+shift+r"], 4 | "command": "rester_http_request" 5 | } 6 | ] 7 | -------------------------------------------------------------------------------- /Default (Windows).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "keys": ["ctrl+alt+r"], 4 | "command": "rester_http_request" 5 | } 6 | ] 7 | -------------------------------------------------------------------------------- /Default.sublime-commands: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "caption": "RESTer: HTTP Request", 4 | "command": "rester_http_request" 5 | } 6 | ] 7 | -------------------------------------------------------------------------------- /messages/1.4.3.txt: -------------------------------------------------------------------------------- 1 | RESTer 1.4.3 Change Log 2 | 3 | 4 | Bug Fix: 5 | 6 | Default response command updated to call "pretty_json" instead of 7 | "prettyjson" to match Pretty JSON's new API. 8 | 9 | -------------------------------------------------------------------------------- /messages/1.8.2.txt: -------------------------------------------------------------------------------- 1 | RESTer 1.8.2 Change Log 2 | 3 | Bug fix: 4 | 5 | Fixed an issue in Sublime Text 2 where RESTer would undo the last change to the 6 | request buffer even when no request commands caused changes. 7 | -------------------------------------------------------------------------------- /rester/commands/set_syntax_command.py: -------------------------------------------------------------------------------- 1 | import sublime_plugin 2 | 3 | class SetSyntaxCommand(sublime_plugin.TextCommand): 4 | """Wrapper command for setting the syntax of the current view.""" 5 | def run(self, edit, syntax_file): 6 | self.view.set_syntax_file(syntax_file) 7 | -------------------------------------------------------------------------------- /rester/commands/__init__.py: -------------------------------------------------------------------------------- 1 | from .auto_form_encode_command import AutoFormEncodeCommand 2 | from .http_request_command import ResterHttpRequestCommand, ResterHttpResponseCloseEvent 3 | from .set_syntax_command import SetSyntaxCommand 4 | 5 | __all__ = [ 6 | 'AutoFormEncodeCommand', 7 | 'ResterHttpRequestCommand', 8 | 'ResterHttpResponseCloseEvent', 9 | 'SetSyntaxCommand' 10 | ] 11 | -------------------------------------------------------------------------------- /messages/1.7.0.txt: -------------------------------------------------------------------------------- 1 | RESTer 1.7.0 Change Log 2 | 3 | 4 | New Features: 5 | 6 | - Host Setting 7 | 8 | The @host setting allows communicating with a different host or IP address 9 | than in in the Host header or request line. 10 | 11 | Example: 12 | 13 | GET /my-endpoint 14 | Host: api.my-example-site.com 15 | @host: 127.0.0.1 16 | 17 | - Multiple Headers 18 | 19 | Support added for multiple headers with the same name, such as Set-Cookies 20 | -------------------------------------------------------------------------------- /messages/1.8.0.txt: -------------------------------------------------------------------------------- 1 | RESTer 1.8.0 Change Log 2 | 3 | 4 | New Feature: 5 | 6 | - set_syntax Command 7 | 8 | Use the new "set_syntax" command to set the syntax highlighting on new responses. 9 | 10 | Example: 11 | 12 | { 13 | "response_commands": [ 14 | { 15 | "name": "set_syntax", 16 | "args": { 17 | "syntax_file": "Packages/HTTP Spec Syntax/httpspec.tmLanguage" 18 | } 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /messages/1.4.0.txt: -------------------------------------------------------------------------------- 1 | RESTer 1.4.0 Change Log 2 | 3 | 4 | New Features: 5 | 6 | - cURL 7 | 8 | To allow Linux users to make HTTPS requests, I added the ability to use 9 | cURL in place of the native Python connection. This is an optional feature 10 | and must be turned on through the settings. 11 | 12 | - Redirects 13 | 14 | RESTer can now automatically follow redirects. This can be disabled through 15 | settings, either entirely, or for specific status codes. 16 | 17 | - Sublime Text 2 18 | 19 | RESTer will now work in Sublime Text 2. 20 | -------------------------------------------------------------------------------- /messages/1.5.0.txt: -------------------------------------------------------------------------------- 1 | RESTer 1.5.0 Change Log 2 | 3 | 4 | New Features: 5 | 6 | - Side-by-side mode 7 | 8 | Responses can now open in a specific group. You can tweak the settings to 9 | behave how you like, but these settings will work for a standard, 10 | two-column, side-by-side mode: 11 | 12 | { 13 | "response_group": 1, 14 | "response_group_clean": true, 15 | "request_focus": true 16 | } 17 | 18 | See the configuration file or README for descriptions of these options. 19 | 20 | - Response views now marked as "scratch" 21 | 22 | When a response loads, the view is marked as a "scratch" view to prevent 23 | Sublime from prompting to save on close. If you work extensively with the 24 | requested data and want to have this prompt, set "response_scratch" to 25 | False in the configuration. 26 | 27 | 28 | -------------------------------------------------------------------------------- /rester/overrideable.py: -------------------------------------------------------------------------------- 1 | class OverrideableSettings(): 2 | """ 3 | Class for adding a layer of overrides on top of a Settings object 4 | 5 | The class is read-only. If a dictionary-like _overrides member is present, 6 | the get() method will look there first for a setting before reading from 7 | the _settings member. 8 | """ 9 | 10 | def __init__(self, settings=None, overrides=None): 11 | self._settings = settings 12 | self._overrides = overrides 13 | 14 | def set_settings(self, settings): 15 | self._settings = settings 16 | 17 | def set_overrides(self, overrides): 18 | self._overrides = overrides 19 | 20 | def get(self, setting, default=None): 21 | if self._overrides and setting in self._overrides: 22 | return self._overrides[setting] 23 | elif self._settings: 24 | return self._settings.get(setting, default) 25 | else: 26 | return default 27 | -------------------------------------------------------------------------------- /messages/1.6.0.txt: -------------------------------------------------------------------------------- 1 | RESTer 1.6.0 Change Log 2 | 3 | 4 | New Features: 5 | 6 | - Multiline Form Field Values 7 | 8 | The support for automatically encoding a request body as a form now allows 9 | for multiline field values and values with whitespace preserved intact. 10 | 11 | To use this feature, wrap the value in delimiters. By default, the values 12 | will look like Python-style """triple-quotes strings""", but you can 13 | customize this by changing the form_field_start and form_field_end 14 | values in the settings file. 15 | 16 | Here's a sample request using the default delimiters: 17 | 18 | POST http://api.my-example-site.com/cats/ 19 | Content-type: application/x-www-form-urlencoded 20 | 21 | name: Molly 22 | color: Calico 23 | nickname: Mrs. Puff 24 | extra: """{ 25 | "id": 2, 26 | "description": "This JSON snippet is wrapped in delimiters because it has multiple lines." 27 | }""" 28 | -------------------------------------------------------------------------------- /rester/reloader.py: -------------------------------------------------------------------------------- 1 | """ 2 | Sublime imports modules in package roots, but will not look deeper. 3 | This module manually reloads the package's modules in such an order that 4 | modules with dependencies are loaded after their dependencies. 5 | """ 6 | 7 | import sys 8 | import sublime 9 | 10 | MODULE_PREFIX = "rester" 11 | PACKAGE_DIRECTORY = "rester-sublime-http-client" 12 | 13 | ST_VERSION = 2 14 | if int(sublime.version()) > 3000: 15 | ST_VERSION = 3 16 | 17 | if ST_VERSION == 3: 18 | from imp import reload 19 | 20 | mod_prefix = MODULE_PREFIX 21 | if ST_VERSION == 3: 22 | mod_prefix = PACKAGE_DIRECTORY + "." + mod_prefix 23 | 24 | # Reload modules in this order. 25 | # Modules with dependencies must be loaded after the dependencies. 26 | mods_load_order = [ 27 | '.overrideable', 28 | '.util', 29 | '.message', 30 | '.http', 31 | '.parse', 32 | '.commands.auto_form_encode_command', 33 | '.commands.http_request_command', 34 | '.commands', 35 | ] 36 | 37 | for suffix in mods_load_order: 38 | mod = mod_prefix + suffix 39 | if mod in sys.modules: 40 | reload(sys.modules[mod]) 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 PJ Dietz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Main.sublime-menu: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "mnemonic": "n", 4 | "caption": "Preferences", 5 | "id": "preferences", 6 | "children": [ 7 | { 8 | "mnemonic": "P", 9 | "caption": "Package Settings", 10 | "id": "package-settings", 11 | "children": [ 12 | { 13 | "caption": "RESTer HTTP Client", 14 | "children": [ 15 | { 16 | "caption": "Settings – Default", 17 | "args": { 18 | "file": "${packages}/RESTer HTTP Client/RESTer.sublime-settings" 19 | }, 20 | "command": "open_file" 21 | }, 22 | { 23 | "caption": "Settings – User", 24 | "args": { 25 | "file": "${packages}/User/RESTer.sublime-settings" 26 | }, 27 | "command": "open_file" 28 | }, 29 | { 30 | "caption": "-" 31 | } 32 | ] 33 | } 34 | ] 35 | } 36 | ] 37 | } 38 | ] 39 | -------------------------------------------------------------------------------- /rester/util.py: -------------------------------------------------------------------------------- 1 | """ 2 | Utility functions 3 | """ 4 | 5 | import re 6 | 7 | 8 | RE_ENCODING = """(?:encoding|charset)=['"]*([a-zA-Z0-9\-]+)['"]*""" 9 | 10 | 11 | def get_end_of_line_character(view): 12 | """Return the EOL character from the view's settings.""" 13 | line_endings = view.settings().get("default_line_ending") 14 | if line_endings == "windows": 15 | return "\r\n" 16 | elif line_endings == "mac": 17 | return "\r" 18 | else: 19 | return "\n" 20 | 21 | 22 | def get_query_string(query_map): 23 | """Return the query string given a map of key-value pairs.""" 24 | if query_map: 25 | query = [] 26 | for (name, values) in query_map.items(): 27 | for value in values: 28 | query.append(name + "=" + value) 29 | return "&".join(query) 30 | return None 31 | 32 | 33 | def normalize_line_endings(string, eol): 34 | """Return a string with consistent line endings.""" 35 | string = string.replace("\r\n", "\n").replace("\r", "\n") 36 | if eol != "\n": 37 | string = string.replace("\n", eol) 38 | return string 39 | 40 | 41 | def scan_string_for_encoding(string): 42 | """Read a string and return the encoding identified within.""" 43 | m = re.search(RE_ENCODING, string) 44 | if m: 45 | return m.groups()[0] 46 | return None 47 | 48 | 49 | def scan_bytes_for_encoding(bytes_sequence): 50 | """Read a byte sequence and return the encoding identified within.""" 51 | m = re.search(RE_ENCODING.encode('ascii'), bytes_sequence) 52 | if m: 53 | encoding = m.groups()[0] 54 | return encoding.decode('ascii') 55 | return None 56 | -------------------------------------------------------------------------------- /rester/phantoms.py: -------------------------------------------------------------------------------- 1 | import sublime 2 | import sublime_plugin 3 | 4 | from .constants import SYNTAX_FILE 5 | 6 | 7 | class RESTer(sublime_plugin.ViewEventListener): 8 | def __init__(self, view): 9 | self.view = view 10 | self.phantom_set = sublime.PhantomSet(view) 11 | self.timeout_scheduled = False 12 | self.needs_update = False 13 | 14 | @classmethod 15 | def is_applicable(cls, settings): 16 | syntax = settings.get('syntax') 17 | return syntax == SYNTAX_FILE 18 | 19 | def update_phantoms(self): 20 | phantoms = [] 21 | 22 | # Don't do any calculations on 1MB or larger files 23 | if self.view.size() < 2**20: 24 | candidates = self.view.find_all(r'\n(https?://|[A-Z]+ )') 25 | for r in candidates: 26 | phantoms.append(sublime.Phantom( 27 | sublime.Region(r.a), 28 | 'Send Request ' % (r.a + 1), 29 | sublime.LAYOUT_BLOCK, 30 | self.rester_http_request)) 31 | 32 | self.phantom_set.update(phantoms) 33 | 34 | def rester_http_request(self, href): 35 | self.view.window().run_command('rester_http_request', {'pos': int(href)}) 36 | 37 | def handle_timeout(self): 38 | self.timeout_scheduled = False 39 | if self.needs_update: 40 | self.needs_update = False 41 | self.update_phantoms() 42 | 43 | def on_activated(self): 44 | self.update_phantoms() 45 | 46 | def on_modified(self): 47 | # Call update_phantoms(), but not any more than 10 times a second 48 | if self.timeout_scheduled: 49 | self.needs_update = True 50 | else: 51 | self.timeout_scheduled = True 52 | sublime.set_timeout(lambda: self.handle_timeout(), 100) 53 | self.update_phantoms() 54 | -------------------------------------------------------------------------------- /rester/message.py: -------------------------------------------------------------------------------- 1 | from . import util 2 | 3 | 4 | class Message(object): 5 | """Base class for HTTP messages""" 6 | 7 | def __init__(self): 8 | self.headers = [] 9 | self.body = "" 10 | 11 | @property 12 | def header_lines(self): 13 | lines = [] 14 | for key, value in self.headers: 15 | lines.append("%s: %s" % (key, value)) 16 | return lines 17 | 18 | def get_header(self, header): 19 | header = header.lower() 20 | for key, value in self.headers: 21 | if key.lower() == header: 22 | return value 23 | return None 24 | 25 | 26 | class Request(Message): 27 | """Represents an HTTP request""" 28 | 29 | def __init__(self): 30 | Message.__init__(self) 31 | self.host = None 32 | self.protocol = "http" 33 | self.method = "GET" 34 | self.path = "/" 35 | self.port = None 36 | self.query = {} 37 | 38 | @property 39 | def full_path(self): 40 | """Path + query string for the request.""" 41 | uri = self.path 42 | if self.query: 43 | uri += "?" + util.get_query_string(self.query) 44 | return uri 45 | 46 | @property 47 | def request_line(self): 48 | """First line, ex: GET /my-path/ HTTP/1.1""" 49 | return "%s %s HTTP/1.1" % (self.method, self.full_path) 50 | 51 | @property 52 | def uri(self): 53 | """Full URI, including protocol""" 54 | uri = self.protocol + "://" + self.host 55 | if self.port: 56 | uri += ":" + str(self.port) 57 | uri += self.full_path 58 | return uri 59 | 60 | 61 | class Response(Message): 62 | """Represents an HTTP request""" 63 | 64 | def __init__(self): 65 | Message.__init__(self) 66 | self.protocol = "HTTP/1.1" 67 | self.status = 500 68 | self.reason = None 69 | 70 | @property 71 | def status_line(self): 72 | if self.protocol and self.status: 73 | return "%s %d %s" % (self.protocol, self.status, "" if self.reason is None else self.reason) 74 | return None 75 | -------------------------------------------------------------------------------- /RESTer.py: -------------------------------------------------------------------------------- 1 | """ 2 | Sublime imports modules it finds in package roots, but will not look deeper. 3 | This module loads the modules needed for the package and manages reloading 4 | of these dependencies when this module is itself reloaded. 5 | """ 6 | 7 | import os 8 | import sys 9 | 10 | import sublime 11 | 12 | EXPECTED_PACKAGE_DIR = "RESTer HTTP Client" 13 | RELOADER_NAME = "rester.reloader" 14 | 15 | if int(sublime.version()) > 3000: 16 | ST_VERSION = 3 17 | PACKAGE_DIR = __name__.split('.')[0] 18 | else: 19 | ST_VERSION = 2 20 | PACKAGE_DIR = os.path.basename(os.getcwd()) 21 | 22 | # Ensure the package is installed in the correct directory. 23 | # The menu commands will not work properly otherwise. 24 | if PACKAGE_DIR != EXPECTED_PACKAGE_DIR: 25 | m = u'RESTer appears to be installed incorrectly.\n\n' \ 26 | u'It should be installed as "%s", but is installed as "%s".\n\n' 27 | message = m % (EXPECTED_PACKAGE_DIR, PACKAGE_DIR) 28 | # If installed unpacked 29 | if os.path.exists(os.path.join(sublime.packages_path(), PACKAGE_DIR)): 30 | m = u'Please use the Preferences > Browse Packages... menu ' \ 31 | u'entry to open the "Packages/" folder and rename "%s" to "%s" ' 32 | message += m % (PACKAGE_DIR, EXPECTED_PACKAGE_DIR) 33 | # If installed as a .sublime-package file 34 | else: 35 | m = u'Please use the Preferences > Browse Packages... menu ' \ 36 | u'entry to open the "Packages/" folder, then browse up a ' \ 37 | u'folder and into the "Installed Packages/" folder.\n\n' \ 38 | u'Inside of "Installed Packages/", rename ' \ 39 | u'"%s.sublime-package" to "%s.sublime-package" ' 40 | message += m % (PACKAGE_DIR, EXPECTED_PACKAGE_DIR) 41 | message += u"and restart Sublime Text." 42 | sublime.error_message(message) 43 | 44 | # Reload modules. 45 | reloader_name = RELOADER_NAME 46 | if ST_VERSION == 3: 47 | reloader_name = PACKAGE_DIR + "." + reloader_name 48 | from imp import reload 49 | if reloader_name in sys.modules: 50 | reload(sys.modules[reloader_name]) 51 | 52 | # Initial loading. 53 | try: 54 | # Python 3 55 | from .rester import reloader 56 | from .rester.commands import * 57 | from .rester.phantoms import * 58 | except ValueError: 59 | # Python 2 60 | from rester import reloader 61 | from rester.commands import * 62 | -------------------------------------------------------------------------------- /RESTer.sublime-settings: -------------------------------------------------------------------------------- 1 | { 2 | // Do not include headers when writing the response to a new buffer. 3 | "body_only": false, 4 | 5 | // Path to the curl command. If curl is on you path, you should not need to 6 | // change this. Windows users will need to use forward slashes in the path. 7 | // 8 | // Only meaningful when http_client is "curl" 9 | "curl_command": "curl", 10 | 11 | // Additional options to send to cURL. Each option must be a separate 12 | // string. For example, to add custom header: 13 | // curl_options: ["--header", "X-Custom-Header: 1"] 14 | // 15 | // Only meaningful when http_client is "curl" 16 | "curl_options": [], 17 | 18 | // Default headers to add for each request. 19 | "default_headers": { 20 | "Accept-Encoding": "gzip, deflate", 21 | "Cache-control": "no-cache" 22 | }, 23 | 24 | // List of encodings to try if not discernable from the response. 25 | "default_response_encodings": ["utf-8", "ISO-8859-1", "ascii"], 26 | 27 | // Automatically make a GET (or HEAD) request to the URI in the Location 28 | // header upon receiveing a response with a redirect status code listed 29 | // in follow_redirect_status_codes. 30 | "follow_redirects": true, 31 | 32 | // When follow_redirects is true, follow any response with one of these 33 | // status codes. 34 | "follow_redirect_status_codes": [300, 301, 302, 303, 307], 35 | 36 | // When contructing form fields using the auto_form_encode command, RESTer 37 | // will normally strip whitespace from field values and parse only single- 38 | // line field values. To include a multiline value or to preserve the 39 | // whitespace of a value, wrap the value in these delimiteres: 40 | "form_field_start": "\"\"\"", 41 | "form_field_end": "\"\"\"", 42 | 43 | // RESTer includes two clients to use to perform requests. The native 44 | // Python client is the default, however some Linux users will not be 45 | // able to make HTTPS requests if Python was compiled without SSL. 46 | // 47 | // If you have cURL installed, you may want to set this to "curl". See 48 | // also the options "curl_command" and "curl_options". 49 | // 50 | // Allowed values: "python", "curl" 51 | "http_client": "python", 52 | 53 | // Output the request to the console. 54 | "output_request": true, 55 | 56 | // Output the response headers to the console. 57 | "output_response_headers": true, 58 | 59 | // Output the response body to the console. 60 | "output_response_body": true, 61 | 62 | // Port to use when not listed in the request line. 63 | // Do not set unless you need something other than 80 (http) or 443 (https) 64 | "port": null, 65 | 66 | // Protocol to use when not listed in the request line. 67 | // Allowed values are "http" and "https" 68 | "protocol": "http", 69 | 70 | // List of commands to run on the request. 71 | // These commands will be undone after the request is issued. 72 | // 73 | // Each request or response command must be a string (the name of the 74 | // command), or an object with a "name" member (the name of the command) 75 | // and optionally an "args" member as an object of arguments to pass to the 76 | // command. 77 | // 78 | // Example: 79 | // { 80 | // "name": "merge_variables", 81 | // "args": { 82 | // "active_sets": ["mysite", "mysite-dev"] 83 | // } 84 | // } 85 | // 86 | "request_commands": [ 87 | "auto_form_encode", 88 | "merge_variables" 89 | ], 90 | 91 | // If true, return focus to the request view. 92 | // If false, keep focus on the new response view. 93 | "request_focus": false, 94 | 95 | // Create a new buffer for the response. 96 | "response_buffer": true, 97 | 98 | // Commands to run on the body of the response. 99 | "response_commands": ["pretty_json"], 100 | 101 | // Open the response in a specific group. If no group at that index exists, 102 | // RESTer will create the group. 103 | // 104 | // Use null to open the response in the same group as the request. 105 | // 106 | // For two-column, side-by-side mode, set to 1. (0 is the request group.) 107 | // These settings work well for a standard two-column mode: 108 | // 109 | // "response_group": 1, 110 | // "response_group_clean": true, 111 | // "request_focus": true 112 | // 113 | "response_group": null, 114 | 115 | // Set to true to close all other views in the response group when 116 | // displaying the response. 117 | // 118 | // Only use this if you are working if "response_group" is set. 119 | "response_group_clean": false, 120 | 121 | // Prevent response views from reporting a dirty state allowing them 122 | // to be closed without a save prompt. 123 | "response_scratch": true, 124 | 125 | // Timeout after this number of seconds. 126 | "timeout": 15 127 | } 128 | -------------------------------------------------------------------------------- /rester/commands/auto_form_encode_command.py: -------------------------------------------------------------------------------- 1 | from ..constants import SETTINGS_FILE 2 | from ..util import get_end_of_line_character 3 | from ..util import get_query_string 4 | 5 | import sublime 6 | import sublime_plugin 7 | 8 | try: 9 | from urllib.parse import quote 10 | except ImportError: 11 | # Python 2 12 | from urllib import quote 13 | 14 | 15 | def encode_form(body_lines, eol): 16 | """Return the form-urlencoded version of the body.""" 17 | 18 | # Field names as keys, and lists of field values as values. 19 | form = {} 20 | 21 | # Key and value for multiple field. These are set only while in the 22 | # process of consuming lines. 23 | delimited_key = None 24 | delimited_value = None 25 | 26 | # Read delimiters from settings. 27 | settings = sublime.load_settings(SETTINGS_FILE) 28 | form_field_start = settings.get("form_field_start", None) 29 | form_field_end = settings.get("form_field_end", None) 30 | delimited = form_field_start and form_field_end 31 | 32 | for line in body_lines: 33 | 34 | key = None 35 | value = None 36 | 37 | # Currently building delimited field. 38 | if delimited and delimited_key: 39 | 40 | # Check if this line ends with the closing delimiter. 41 | if line.rstrip().endswith(form_field_end): 42 | 43 | # Read the line up to the delimiter. 44 | value = line.rstrip()[:-len(form_field_end)] 45 | 46 | # The field is complete. Prepare to copy this to the form. 47 | key = delimited_key 48 | value = delimited_value + eol + value 49 | delimited_key = None 50 | delimited_value = None 51 | 52 | # The field is still being built. Append the current line. 53 | else: 54 | delimited_value += eol + line 55 | 56 | # No delimited field in progress. 57 | else: 58 | 59 | # Attempt to parse this line into a key-value pair. 60 | if "=" in line: 61 | (key, value) = line.split("=", 1) 62 | elif ":" in line: 63 | (key, value) = line.split(":", 1) 64 | 65 | if key and value: 66 | 67 | key = key.strip() 68 | 69 | # Test if this value begins a delimited value. 70 | 71 | # If the field begins with the starting delimiter, copy the 72 | # contents after that delimiter to a variable. 73 | if delimited and value.lstrip().startswith(form_field_start): 74 | value = value.lstrip()[len(form_field_start):] 75 | 76 | # If the field ends with the ending delimiter, trim the 77 | # delimiter from the end and close field. 78 | if value.rstrip().endswith(form_field_end): 79 | value = value.rstrip()[:-len(form_field_end)] 80 | delimited_key = None 81 | delimited_value = None 82 | 83 | # If the field does NOT end with the delimiter, keep 84 | # building the field with subsequent lines. 85 | else: 86 | delimited_key = key 87 | delimited_value = value 88 | key = None 89 | value = None 90 | 91 | # Normal field. 92 | else: 93 | value = value.strip() 94 | 95 | # As long as key and value are set, add the item to the form 96 | if key and value: 97 | value = quote(value) 98 | if key in form: 99 | form[key].append(value) 100 | else: 101 | form[key] = [value] 102 | 103 | return get_query_string(form) 104 | 105 | 106 | def has_form_encoded_header(header_lines): 107 | """Return if list includes form encoded header""" 108 | for line in header_lines: 109 | if ":" in line: 110 | (header, value) = line.split(":", 1) 111 | if header.lower() == "content-type" \ 112 | and "x-www-form-urlencoded" in value: 113 | return True 114 | return False 115 | 116 | 117 | class AutoFormEncodeCommand(sublime_plugin.TextCommand): 118 | """Encode a request as x-www-form-urlencoded""" 119 | 120 | def __init__(self, *args, **kwargs): 121 | self._edit = None 122 | sublime_plugin.TextCommand.__init__(self, *args, **kwargs) 123 | 124 | def run(self, edit): 125 | self._edit = edit 126 | # Replace the text in each selection. 127 | for selection in self._get_selections(): 128 | self._replace_text(selection) 129 | 130 | def _get_selections(self): 131 | # Return a list of Regions for the selection(s). 132 | sels = self.view.sel() 133 | if len(sels) == 1 and sels[0].empty(): 134 | return [sublime.Region(0, self.view.size())] 135 | else: 136 | return sels 137 | 138 | def _replace_text(self, selection): 139 | # Replace the selected text with the new version. 140 | 141 | text = self.view.substr(selection) 142 | eol = get_end_of_line_character(self.view) 143 | 144 | # Quit if there's no body to encode. 145 | if (eol * 2) not in text: 146 | return 147 | 148 | (headers, body) = text.split(eol * 2, 1) 149 | if has_form_encoded_header(headers.split(eol)): 150 | encoded_body = encode_form(body.split(eol), eol) 151 | request = headers + eol + eol + encoded_body 152 | self.view.replace(self._edit, selection, request) 153 | -------------------------------------------------------------------------------- /rester/parse.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from .message import Request 4 | from .util import normalize_line_endings 5 | 6 | try: 7 | # Python 3 8 | from urllib.parse import urlparse 9 | from urllib.parse import parse_qs 10 | from urllib.parse import quote 11 | except ImportError: 12 | # Python 2 13 | from urlparse import urlparse 14 | from urlparse import parse_qs 15 | from urllib import quote 16 | 17 | 18 | def _read_request_line_dict(line): 19 | """Return a dict containing the method and uri for a request line""" 20 | 21 | # Split the line into words. 22 | words = line.split(" ") 23 | method = "GET" 24 | # If the line contains only one word, assume the line is the URI. 25 | if len(words) == 1: 26 | uri = words[0] 27 | else: 28 | method = words[0] 29 | uri = words[1] 30 | 31 | return { 32 | "method": method, 33 | "uri": uri 34 | } 35 | 36 | 37 | class RequestParser: 38 | def __init__(self, settings, eol): 39 | self.settings = settings 40 | self.eol = eol 41 | self.request = None 42 | 43 | def get_request(self, text): 44 | """Build and return a new Request""" 45 | 46 | # Build a new Request. 47 | self.request = Request() 48 | 49 | # Set defaults from settings. 50 | default_host_header = None 51 | default_headers = self.settings.get("default_headers", {}) 52 | if isinstance(default_headers, dict): 53 | for header in default_headers: 54 | if header.lower() == 'host': 55 | default_host_header = default_headers[header] 56 | else: 57 | self.request.headers.append((header, default_headers[header])) 58 | elif default_headers: 59 | for header in default_headers: 60 | if header[0].lower() == 'host': 61 | default_host_header = header[1] 62 | else: 63 | self.request.headers.append(header) 64 | 65 | self.request.host = self.settings.get("host", None) 66 | self.request.port = self.settings.get("port", None) 67 | self.request.protocol = self.settings.get("protocol", None) 68 | 69 | # Pre-parse clean-up. 70 | text = normalize_line_endings(text, self.eol) 71 | 72 | # Split the string into lines. 73 | lines = text.split(self.eol) 74 | 75 | # Consume empty and comment lines at the top. 76 | for i in range(len(lines)): 77 | line = lines[i].strip() 78 | if line == "" or line[0] == "#": 79 | pass 80 | else: 81 | lines = lines[i:] 82 | break 83 | 84 | # Parse the first line as the request line. 85 | self._parse_request_line(lines[0]) 86 | 87 | # All lines following the request line are headers until an empty line. 88 | # All content after the empty line is the request body. 89 | has_body = False 90 | i = 0 91 | for i in range(1, len(lines)): 92 | if lines[i] == "": 93 | has_body = True 94 | break 95 | 96 | if has_body: 97 | header_lines = lines[1:i] 98 | self.request.body = self.eol.join(lines[i + 1:]) 99 | else: 100 | header_lines = lines[1:] 101 | 102 | # Make a dictionary of headers. 103 | self._parse_header_lines(header_lines) 104 | 105 | # Try to set the hostname from the host header, if not yet set. 106 | if not self.request.host: 107 | host = self.request.get_header("host") 108 | if host: 109 | self.request.host = host 110 | 111 | if not self.request.host and default_host_header: 112 | self.request.host = default_host_header 113 | 114 | # If there is still no hostname, but there is a path, try re-parsing 115 | # the path with // prepended. 116 | # 117 | # From the Python documentation: 118 | # Following the syntax specifications in RFC 1808, urlparse recognizes 119 | # a netloc only if it is properly introduced by '//'. Otherwise the 120 | # input is presumed to be a relative URL and thus to start with a path 121 | # component. 122 | # 123 | if not self.request.host and self.request.path: 124 | uri = urlparse("//" + self.request.path) 125 | self.request.host = uri.netloc 126 | self.request.path = uri.path 127 | 128 | # Set path to / instead of empty. 129 | if not self.request.path: 130 | self.request.path = "/" 131 | 132 | return self.request 133 | 134 | def _parse_header_lines(self, header_lines): 135 | 136 | # Parse the lines before the body. 137 | # Build request headers list. 138 | 139 | headers = [] 140 | 141 | for header in header_lines: 142 | header = header.lstrip() 143 | 144 | # Skip comments and overrides. 145 | if header[0] in ("#", "@"): 146 | pass 147 | 148 | # Query parameters begin with ? or & 149 | elif header[0] in ("?", "&"): 150 | 151 | if "=" in header: 152 | (key, value) = header[1:].split("=", 1) 153 | elif ":" in header: 154 | (key, value) = header[1:].split(":", 1) 155 | else: 156 | key, value = None, None 157 | 158 | if key and value: 159 | key = key.strip() 160 | value = quote(value.strip()) 161 | if key in self.request.query: 162 | self.request.query[key].append(value) 163 | else: 164 | self.request.query[key] = [value] 165 | 166 | # All else are headers 167 | elif ":" in header: 168 | (key, value) = header.split(":", 1) 169 | headers.append((key, value.strip())) 170 | 171 | # Merge headers with default headers provided in settings. 172 | self.request.headers.extend(headers) 173 | 174 | def _parse_request_line(self, line): 175 | 176 | # Parse the first line as the request line. 177 | # Fail, if unable to parse. 178 | 179 | request_line = _read_request_line_dict(line) 180 | if not request_line: 181 | return 182 | 183 | # Parse the URI. 184 | uri = urlparse(request_line["uri"]) 185 | 186 | # Copy from the parsed URI. 187 | if uri.scheme: 188 | self.request.protocol = uri.scheme 189 | if uri.netloc: 190 | # Sometimes urlparse leave the port in the netloc. 191 | if ":" in uri.netloc: 192 | (self.request.host, self.request.port) = uri.netloc.split(":") 193 | else: 194 | self.request.host = uri.netloc 195 | if uri.port: 196 | self.request.port = uri.port 197 | if uri.path: 198 | self.request.path = uri.path 199 | if uri.query: 200 | query = parse_qs(uri.query) 201 | for key in query: 202 | self.request.query[key] = query[key] 203 | 204 | # Read the method from the request line. Default is GET. 205 | if "method" in request_line: 206 | self.request.method = request_line["method"] 207 | -------------------------------------------------------------------------------- /http.tmLanguage: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | name 6 | HTTP 7 | scopeName 8 | source.http 9 | fileTypes 10 | 11 | http 12 | rest 13 | 14 | keyEquivalent 15 | ^~H 16 | patterns 17 | 18 | 19 | begin 20 | ^\s*(?=curl) 21 | name 22 | http.request.curl 23 | end 24 | ^\s*(\#{3,}.*?)?\s*$ 25 | endCaptures 26 | 27 | 0 28 | 29 | name 30 | comment.source.sharp.http 31 | 32 | 33 | patterns 34 | 35 | 36 | include 37 | source.shell 38 | 39 | 40 | 41 | 42 | begin 43 | ^\s*(?=[\{\[]) 44 | name 45 | http.request.body.json 46 | end 47 | ^\s*(\#{3,}.*?)?\s*$ 48 | endCaptures 49 | 50 | 0 51 | 52 | name 53 | comment.source.sharp.http 54 | 55 | 56 | patterns 57 | 58 | 59 | include 60 | source.json 61 | 62 | 63 | 64 | 65 | begin 66 | ^\s*(?=\<\S) 67 | name 68 | http.request.body.xml 69 | end 70 | ^\s*(\#{3,}.*?)?\s*$ 71 | endCaptures 72 | 73 | 0 74 | 75 | name 76 | comment.source.sharp.http 77 | 78 | 79 | patterns 80 | 81 | 82 | include 83 | text.xml 84 | 85 | 86 | 87 | 88 | match 89 | ^\s*#{1,}\s*(((@)name)\s+(\S+))?.*$ 90 | captures 91 | 92 | 2 93 | 94 | name 95 | storage.type.class.metadata 96 | 97 | 3 98 | 99 | name 100 | punctuation.definition.block.tag.metadata 101 | 102 | 4 103 | 104 | name 105 | entity.name.type.instance.metadata 106 | 107 | 108 | name 109 | comment.source.sharp.http 110 | 111 | 112 | match 113 | ^\s*/{2,}\s*(((@)name)\s+(\S+))?.*$ 114 | captures 115 | 116 | 2 117 | 118 | name 119 | storage.type.class.metadata 120 | 121 | 3 122 | 123 | name 124 | punctuation.definition.block.tag.metadata 125 | 126 | 4 127 | 128 | name 129 | entity.name.type.instance.metadata 130 | 131 | 132 | name 133 | comment.source.double-slash.http 134 | 135 | 136 | captures 137 | 138 | 1 139 | 140 | name 141 | keyword.control.http 142 | 143 | 3 144 | 145 | name 146 | const.language.http 147 | 148 | 5 149 | 150 | name 151 | keyword.other.http 152 | 153 | 7 154 | 155 | name 156 | constant.numeric.http 157 | 158 | 159 | match 160 | ^(?:((?i)get|post|put|delete|patch|head|options|connect|trace(-?))\s+)?\s*(\S+)(?:\s+(((?i)HTTP(-?))\/(\S+)))?$ 161 | name 162 | http.requestline 163 | 164 | 165 | captures 166 | 167 | 1 168 | 169 | name 170 | support.variable.http 171 | 172 | 2 173 | 174 | name 175 | string.other.http 176 | 177 | 178 | match 179 | ^([\w\-]+)\s*\:\s*(.*?)\s*$ 180 | name 181 | http.requestheaders 182 | 183 | 184 | captures 185 | 186 | 1 187 | 188 | name 189 | keyword.other.http 190 | 191 | 3 192 | 193 | name 194 | constant.numeric.http 195 | 196 | 4 197 | 198 | name 199 | constant.numeric.http 200 | 201 | 5 202 | 203 | name 204 | keyword.other.http 205 | 206 | 207 | match 208 | ^\s*((?i)HTTP(-?))\/(\S+)\s([1-5][0-9][0-9])\s(.*)$ 209 | name 210 | http.responseLine 211 | 212 | 213 | captures 214 | 215 | 1 216 | 217 | name 218 | keyword.other.http 219 | 220 | 2 221 | 222 | name 223 | variable.other.http 224 | 225 | 3 226 | 227 | name 228 | string.other.http 229 | 230 | 231 | match 232 | ^\s*(@)([^\s=]+)\s*=\s*(.+)\s*$ 233 | name 234 | http.filevariable 235 | 236 | 237 | 238 | -------------------------------------------------------------------------------- /rester/http.py: -------------------------------------------------------------------------------- 1 | """ 2 | Modules for making HTTP requests using the built in Python http.client module 3 | """ 4 | 5 | import codecs 6 | import json 7 | import os 8 | import socket 9 | import subprocess 10 | import tempfile 11 | import threading 12 | import time 13 | import zlib 14 | import errno 15 | 16 | from .message import Response 17 | from .util import normalize_line_endings 18 | from .util import scan_bytes_for_encoding 19 | from .util import scan_string_for_encoding 20 | import sublime 21 | 22 | try: 23 | from http.client import HTTPConnection 24 | try: 25 | from http.client import HTTPSConnection 26 | except ImportError: 27 | # Linux with no SSL support. 28 | pass 29 | except ImportError: 30 | # Python 2 31 | from httplib import HTTPConnection 32 | try: 33 | from httplib import HTTPSConnection 34 | except ImportError: 35 | # Linux with no SSL support. 36 | pass 37 | 38 | 39 | def decode(bytes_sequence, encodings): 40 | """Return the first successfully decoded string""" 41 | for encoding in encodings: 42 | try: 43 | decoded = bytes_sequence.decode(encoding) 44 | return decoded 45 | except UnicodeDecodeError: 46 | # Try the next in the list. 47 | pass 48 | raise DecodeError 49 | 50 | 51 | class HttpRequestThread(threading.Thread): 52 | def __init__(self, request, settings, encoding="UTF8", eol="\n"): 53 | threading.Thread.__init__(self) 54 | self.request = request 55 | self.response = None 56 | self.message = None 57 | self.success = False 58 | self.elapsed = None 59 | self._encoding = encoding 60 | self._encodings = settings.get("default_response_encodings", []) 61 | self._eol = eol 62 | self._output_request = settings.get("output_request", True) 63 | self._output_response = settings.get("output_response", True) 64 | self._timeout = settings.get("timeout", None) 65 | 66 | def _decode_body(self, body_bytes): 67 | 68 | # Decode the body. The hard part here is finding the right encoding. 69 | # To do this, create a list of possible matches. 70 | encodings = [] 71 | 72 | # Check the content-type header, if present. 73 | content_type = self.response.get_header("content-type") 74 | if content_type: 75 | encoding = scan_string_for_encoding(content_type) 76 | if encoding: 77 | encodings.append(encoding) 78 | 79 | # Scan the body 80 | encoding = scan_bytes_for_encoding(body_bytes) 81 | if encoding: 82 | encodings.append(encoding) 83 | 84 | # Add any default encodings not already discovered. 85 | for encoding in self._encodings: 86 | if encoding not in encodings: 87 | encodings.append(encoding) 88 | 89 | # Decoding using the encodings discovered. 90 | try: 91 | body = decode(body_bytes, encodings) 92 | except DecodeError: 93 | body = "{Unable to decode body}" 94 | 95 | return body 96 | 97 | def _read_body(self, body_bytes): 98 | # Decode the body from a list of bytes 99 | # This must be called AFTER the response headers are populated. 100 | if not body_bytes: 101 | return None 102 | body_bytes = self._unzip_body(body_bytes) 103 | body = self._decode_body(body_bytes) 104 | body = normalize_line_endings(body, self._eol) 105 | return body 106 | 107 | def _unzip_body(self, body_bytes): 108 | content_encoding = self.response.get_header("content-encoding") 109 | if content_encoding: 110 | content_encoding = content_encoding.lower() 111 | if "gzip" in content_encoding or "deflate" in content_encoding: 112 | body_bytes = zlib.decompress(body_bytes, 15 + 32) 113 | return body_bytes 114 | 115 | def _validate_request(self): 116 | 117 | # Fail if the hostname is not set. 118 | if not self.request.host: 119 | self.message = "Unable to make request. Please provide a hostname." 120 | self.success = False 121 | return False 122 | 123 | if self.request.protocol not in ("http", "https"): 124 | self.message = "Unsupported protocol " + self.request.protocol + \ 125 | ". Use http of https" 126 | self.success = False 127 | return False 128 | 129 | return True 130 | 131 | 132 | class HttpClientRequestThread(HttpRequestThread): 133 | def run(self): 134 | """Method to run when the thread is started.""" 135 | 136 | if not self._validate_request(): 137 | return 138 | 139 | # Determine the class to use for the connection. 140 | if self.request.protocol == "https": 141 | try: 142 | connection_class = HTTPSConnection 143 | except NameError: 144 | message = "Your Python interpreter does not have SSL. " \ 145 | "If you have cURL installed, set the http_client " \ 146 | "setting to \"curl\"." 147 | sublime.error_message(message) 148 | self.message = "Unable to make HTTPS requests." 149 | self.success = False 150 | return 151 | 152 | else: 153 | connection_class = HTTPConnection 154 | 155 | # Create the connection. 156 | conn = connection_class(self.request.host, 157 | port=self.request.port, 158 | timeout=self._timeout) 159 | 160 | try: 161 | 162 | # Body: encode and add Content-length header 163 | body_bytes = None 164 | if self.request.body: 165 | body_bytes = self.request.body.encode(self._encoding) 166 | if not self.request.get_header("Content-length"): 167 | self.request.headers.append(("Content-length", len(body_bytes))) 168 | 169 | # Insert a host header, if needed. 170 | if not self.request.get_header("host"): 171 | self.request.headers.append(("Host", self.request.host)) 172 | 173 | # Method and Path 174 | conn.putrequest(self.request.method, self.request.full_path, True, True) 175 | 176 | # Headers 177 | for key, value in self.request.headers: 178 | conn.putheader(key, value) 179 | conn.endheaders() 180 | 181 | # Body 182 | if body_bytes: 183 | conn.send(body_bytes) 184 | 185 | except socket.gaierror: 186 | self.message = "Unable to make request. " \ 187 | "Make sure the hostname is valid." 188 | self.success = False 189 | conn.close() 190 | return 191 | 192 | except OSError as e: 193 | if e.errno != errno.ECONNREFUSED: 194 | raise 195 | self.message = "Connection refused." 196 | self.success = False 197 | conn.close() 198 | return 199 | 200 | # Read the response. 201 | # noinspection PyBroadException 202 | try: 203 | time_start = time.time() 204 | resp = conn.getresponse() 205 | except socket.timeout: 206 | self.message = "Request timed out." 207 | self.success = False 208 | conn.close() 209 | return 210 | except Exception: 211 | self.message = "Unexpected error making request." 212 | self.success = False 213 | conn.close() 214 | return 215 | 216 | # Read the response 217 | self._read_response(resp) 218 | time_end = time.time() 219 | self.elapsed = time_end - time_start 220 | conn.close() 221 | self.success = True 222 | 223 | def _read_response(self, resp): 224 | 225 | # Read the HTTPResponse and populate the response member. 226 | self.response = Response() 227 | 228 | # HTTP/1.1 is the default 229 | if resp.version == 10: 230 | self.response.protocol = "HTTP/1.0" 231 | 232 | # Status 233 | self.response.status = resp.status 234 | self.response.reason = resp.reason 235 | 236 | # Headers 237 | self.response.headers = resp.getheaders() 238 | 239 | # Body 240 | self.response.body = self._read_body(resp.read()) 241 | 242 | 243 | class CurlRequestThread(HttpRequestThread): 244 | def __init__(self, request, settings, **kwargs): 245 | HttpRequestThread.__init__(self, request, settings, **kwargs) 246 | self._curl_command = settings.get("curl_command", "curl") 247 | self._curl_options = settings.get("curl_options", []) 248 | self._request_body_file = None 249 | 250 | def run(self): 251 | 252 | if not self._validate_request(): 253 | return 254 | 255 | # Build the list of arguments to run cURL. 256 | curl = subprocess.Popen(self._get_args(), stdout=subprocess.PIPE) 257 | time_start = time.time() 258 | output = curl.communicate()[0] 259 | time_end = time.time() 260 | self.elapsed = time_end - time_start 261 | returncode = curl.returncode 262 | 263 | # Delete the temporary file for message body. 264 | if self._request_body_file: 265 | os.remove(self._request_body_file) 266 | 267 | if returncode != 0: 268 | self._read_curl_error(returncode) 269 | self.success = False 270 | return 271 | 272 | self._read_response(output) 273 | 274 | def _get_args(self): 275 | 276 | # Build the list of arguments to run cURL. 277 | 278 | # Append a JSON dict of metadata to the end. 279 | extra = "\n\n" 280 | extra += "{" 281 | extra += "\"size_header\": %{size_header}," 282 | extra += "\"size_download\": %{size_download}" 283 | extra += "}" 284 | 285 | args = [self._curl_command, "--include", "--write-out", extra] 286 | 287 | if self._timeout: 288 | args += ["--max-time", str(self._timeout)] 289 | args += ["--connect-timeout", str(self._timeout)] 290 | 291 | # Method 292 | if self.request.method == "HEAD": 293 | args.append("--head") 294 | elif self.request.method == "GET": 295 | pass 296 | else: 297 | args.append("--request") 298 | args.append(self.request.method) 299 | 300 | # Headers 301 | for header in self.request.header_lines: 302 | args += ['--header', header] 303 | 304 | # Body 305 | if self.request.method in ("POST", "PUT", "PATCH") and \ 306 | self.request.body: 307 | 308 | # Open a temporary file to write the request body to. 309 | # (Note: Using codecs to support Python 2.6) 310 | tmpfile = tempfile.NamedTemporaryFile("w", delete=False) 311 | filename = tmpfile.name 312 | tmpfile.close() 313 | tmpfile = codecs.open(filename, "w", encoding="UTF8") 314 | tmpfile.write(self.request.body) 315 | tmpfile.close() 316 | 317 | args.append("--data-binary") 318 | args.append("@" + filename) 319 | 320 | # Store the temporary file's filename for later deletion. 321 | self._request_body_file = filename 322 | 323 | args += self._curl_options 324 | 325 | # URI 326 | args.append(self.request.uri) 327 | return args 328 | 329 | def _read_response(self, curl_output): 330 | 331 | # Build a new response. 332 | self.response = Response() 333 | 334 | # Read the metadata appended to the end of the request. 335 | meta = curl_output[curl_output.rfind(b"\n\n"):] 336 | meta = meta.decode("ascii") 337 | meta = json.loads(meta) 338 | size_header = meta["size_header"] 339 | size_download = meta["size_download"] 340 | 341 | # Extract the headers and body 342 | headers = curl_output[0:size_header] 343 | body = curl_output[size_header:size_header + size_download] 344 | 345 | # Parse the headers as ASCII 346 | headers = headers.decode("ascii") 347 | headers = headers.split("\r\n") 348 | 349 | # Consume blank lines and CONTINUE status lines from headers 350 | for i in range(len(headers)): 351 | header = headers[i].upper() 352 | if header and "100 CONTINUE" not in header: 353 | headers = headers[i:] 354 | break 355 | 356 | # Read the first line as the status line. 357 | status_line = headers[0] 358 | try: 359 | (protocol, status, reason) = status_line.split(" ", 2) 360 | except ValueError: 361 | print(curl_output) 362 | self.message = "Unable to read response. " \ 363 | "Response may have times out." 364 | self.success = False 365 | return 366 | 367 | self.response.protocol = protocol 368 | self.response.status = int(status) 369 | self.response.reason = reason 370 | 371 | # Add each header 372 | for header in headers[1:]: 373 | if ":" in header: 374 | (key, value) = header.split(":", 1) 375 | self.response.headers.append((key.strip(), value.strip())) 376 | 377 | # Read the body 378 | self.response.body = self._read_body(body) 379 | self.success = True 380 | 381 | def _read_curl_error(self, code): 382 | # Set the message based on the cURL error code. 383 | # CURLE_UNSUPPORTED_PROTOCOL 384 | if code == 1: 385 | self.message = "Unsupported protocol " + self.request.protocol + \ 386 | "Use http of https" 387 | # CURLE_COULDNT_RESOLVE_HOST 388 | elif code == 6: 389 | self.message = "Unable to resolve host." 390 | # CURLE_COULDNT_RESOLVE_HOST 391 | elif code == 7: 392 | self.message = "Unable to connect." 393 | # CURLE_COULDNT_RESOLVE_HOST 394 | elif code == 28: 395 | self.message = "Operation timed out." 396 | else: 397 | self.message = "cURL exited with error code " + str(code) 398 | 399 | 400 | class DecodeError(Exception): 401 | pass 402 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RESTer 2 | 3 | HTTP client for Sublime Text 4 | 5 | RESTer allows you to build an HTTP request in Sublime Text and view the response in a new tab. 6 | 7 | ## Using 8 | 9 | A request can be as simple as a URI: 10 | 11 | ``` 12 | http://api.my-example-site.com 13 | ``` 14 | 15 | Or, you can send headers and a body: 16 | 17 | ``` 18 | PUT /my-endpoint HTTP/1.1 19 | Host: api.my-example-site.com 20 | 21 | Accept: text/plain 22 | Accept-Charset: utf-8 23 | X-custom-header: whatever you want 24 | 25 | Here is the payload for the PUT request. Just add an empty line after the headers. 26 | ``` 27 | 28 | Once you have a request ready, use shortcut `Ctrl + Alt + r` or open the Command Palette (`Shift + Command + P`) and enter `RESTer HTTP Request`. 29 | 30 | ## Installation 31 | 32 | ### Sublime Package Control 33 | 34 | You can install RESTer using the excellent [Package Control][] package manager for Sublime Text: 35 | 36 | 1. Open "Package Control: Install Package" from the Command Palette (`Shift + Command + P`). 37 | 2. Select the "RESTer" option to install RESTer. 38 | 39 | [Package Control]: http://wbond.net/sublime_packages/package_control 40 | 41 | ### Git Installation 42 | 43 | To install, clone to your "Packages" directory. 44 | 45 | ``` 46 | git clone https://github.com/pjdietz/rester-sublime-http-client.git "RESTer HTTP Client" 47 | ``` 48 | 49 | **Note** RESTer expects to be installed to a directory called "RESTer HTTP Client". Some features like the meny command to open settings will not work if installed somewhere else. 50 | 51 | 52 | ## Making Requests 53 | 54 | ### The Request Line 55 | 56 | The first non-empty line of the selection (or document if nothing is selected) is the "request line". RESTer parses this to determine the method, URI, and protocol. 57 | 58 | You may include the hostname in the request line, but RESTer does not require it. If omitted, be sure to include a `Host` header indicating the hostname. 59 | 60 | Here are some example request lines: 61 | 62 | ``` 63 | GET /my-endpoint HTTP/1.1 64 | Host: api.my-example-site.com 65 | ``` 66 | 67 | ``` 68 | GET http://api.my-example-site.com/my-endpoint 69 | ``` 70 | 71 | ``` 72 | http://api.my-example-site.com/my-endpoint 73 | ``` 74 | 75 | ``` 76 | api.my-example-site.com/my-endpoint 77 | ``` 78 | 79 | Because GET is the default method, each of these will have the same effect. 80 | 81 | ### Headers 82 | 83 | RESTer parses the lines immediately following the first non-empty line up to the first empty line as headers. Use the standard `field-name: field-value` format. 84 | 85 | ### Query Parameters 86 | 87 | For requests with many query parameters, you may want to spread your request across a number of lines. RESTer will parse any lines in the headers section that begin with `?` or `&` as query parameters. You may use `=` or `:` to separate the key from the value. 88 | 89 | The following example requests are equivalent: 90 | 91 | All in the URI 92 | ``` 93 | http://api.my-example-site.com/?cat=molly&dog=bear 94 | ``` 95 | 96 | With new lines 97 | ``` 98 | http://api.my-example-site.com/ 99 | ?cat=molly 100 | &dog=bear 101 | ``` 102 | 103 | Indented, using colons, and only using ? 104 | ``` 105 | http://api.my-example-site.com/ 106 | ? cat: molly 107 | ? dog: bear 108 | ``` 109 | 110 | #### Percent Encoding 111 | 112 | One thing to note is that RESTer assumes that anything you place directly in the request line is the way you want it, but query parameters added on individual lines are assumed to be in plain text. So, values of query parameters added on individual lines are percent encoded. 113 | 114 | These requests are equivalent: 115 | 116 | ``` 117 | http://api.my-example-site.com/?item=I%20like%20spaces 118 | ``` 119 | 120 | ``` 121 | http://api.my-example-site.com/ 122 | ? item: I like spaces 123 | ``` 124 | 125 | 126 | ### Body 127 | 128 | To supply a message body for POST and PUT requests, add an empty line after the last header. RESTer will treat all content that follows the blank line as the request body. 129 | 130 | Here's an example of adding a new cat representation by supplying JSON: 131 | 132 | ``` 133 | POST http://api.my-example-site.com/cats/ 134 | 135 | { 136 | "name": "Molly", 137 | "color": "Calico", 138 | "nickname": "Mrs. Puff" 139 | } 140 | ``` 141 | 142 | #### Form Encoding 143 | 144 | For `application/x-www-form-urlencoded` requests, you can use the `auto_form_encode` command (part of RESTer) to automatically encode a body of key-value pairs. To use this functionality, make sure that `auto_form_encode` is enabled as a [`request_command`](#request-commands) and include a `Content-type: application/x-www-form-urlencoded` header. 145 | 146 | The key-value pairs must be on separate lines. You may use `=` or `:` to separate the key from the value. As with query parameters, whitespace around the key and value is ignored. 147 | 148 | Example: 149 | 150 | ``` 151 | POST http://api.my-example-site.com/cats/ 152 | Content-type: application/x-www-form-urlencoded 153 | 154 | name=Molly 155 | color=Calico 156 | nickname=Mrs. Puff 157 | ``` 158 | 159 | Colons and whitespace 160 | 161 | ``` 162 | POST http://api.my-example-site.com/cats/ 163 | Content-type: application/x-www-form-urlencoded 164 | 165 | name: Molly 166 | color: Calico 167 | nickname: Mrs. Puff 168 | ``` 169 | 170 | ##### Multiline Values 171 | 172 | Use delimiters to mark the boundaries of multiline field values. By default, the delimiters are `"""` to mimic a triple-quoted Python string. You may customize this by providing values for the `form_field_start` and `form_field_end` settings. 173 | 174 | Here's an example of a request using mixed single- and multiline fields. 175 | 176 | ``` 177 | POST http://api.my-example-site.com/cats/ 178 | Content-type: application/x-www-form-urlencoded 179 | 180 | name: Molly 181 | color: Calico 182 | nickname: Mrs. Puff 183 | extra: """{ 184 | "id": 2, 185 | "description": "This JSON snippet is wrapped in delimiters because it has multiple lines." 186 | }""" 187 | 188 | ``` 189 | 190 | 191 | ### Comments 192 | 193 | You may include comments in your request by adding lines in the headers section that begin with `#`. RESTer will ignore these lines. 194 | 195 | ``` 196 | GET /my-endpoint HTTP/1.1 197 | Host: /api.my-example-site.com 198 | # This is a comment. 199 | Cache-control: no-cache 200 | ``` 201 | 202 | ## Settings 203 | 204 | RESTer has some other features that you can customize through settings. To customize, add the desired key to the user settings file. 205 | 206 | You may also provide configuration settings for the current request by adding lines to the headers section that begin with `@`. 207 | 208 | The format of the line is `@{name}: {value}` where `{name}` is the key for a setting and `{value}` is the value. The value is parsed as a chunk of JSON. 209 | 210 | ``` 211 | GET /my-endpoint HTTP/1.1 212 | Host: /api.my-example-site.com 213 | @timeout: 2 214 | @default_response_encodings: ["utf-8", "ISO-8859-1", "ascii"] 215 | ``` 216 | 217 | ### Displaying the Response and Request 218 | 219 | By default, RESTer outputs the request and response to the console and opens a new buffer where it writes the full contents of the response. This view is created in the same group as the request view. You can change this behavior by tweaking several settings. 220 | 221 | Setting | Default | Description 222 | ----------------------- | ------- | ----------- 223 | output_request | `true` | Write the request to the console. 224 | output_response_headers | `true` | Write the status line and headers to the console. 225 | output_response_body | `true` | Write the body of response to the console. **Note**: because [response commands](#response-commands) must by run in a buffer, the body is not processed. 226 | response_buffer | `true` | Open a new buffer, write the response, and run any number of [response commands](#response-commands) on the response body. 227 | response_group | `null` | Set to the integer index of the group the response should appear in. `1` is the typical choice for a two-column presentation. 228 | response_group_clean | `false` | If indicating a specific response_group, close all other views in that group on each response. 229 | request_focus | `false` | Return focus to the request view after displaying the response. 230 | body_only | `false` | When writing the response to the buffer, do not include headers. 231 | 232 | #### Side-by-Side Mode 233 | 234 | If you'd like to author your request in one panel and view your response in a second, use this configuration: 235 | 236 | ```json 237 | { 238 | "response_group": 1, 239 | "response_group_clean": true, 240 | "request_focus": true 241 | } 242 | ``` 243 | 244 | ### Protocol 245 | 246 | As of version 1.3.0, RESTer supports making HTTP and HTTPS requests. To use HTTPS, you can include the protocol in the request line. You can also set a default protocol in the settings. 247 | 248 | ```json 249 | { 250 | "protocol": "https" 251 | } 252 | ``` 253 | 254 | You may also set the protocol using an override. These requests are equivalent: 255 | 256 | ``` 257 | GET https://api.my-secure-example-site.com/my-endpoint 258 | ``` 259 | 260 | ``` 261 | GET /my-endpoint 262 | Host: api.my-secure-example-site.com 263 | @protocol:https 264 | ``` 265 | 266 | **Note for Linux Users:** The Python interpreter in Sublime Text on Linux does not have SSL support. To make HTTPS requests, you will need to change the RESTer settings to use [cURL](#curl). 267 | 268 | ### Host 269 | 270 | RESTer will open the connection to the host it finds in the Host header or in the request line. Hoever, you may want to communicate with a server at a specfic IP address. Do do this, use the `@host` setting. 271 | 272 | ``` 273 | GET /my-endpoint 274 | Host: api.my-example-site.com 275 | @host: 127.0.0.1 276 | ``` 277 | 278 | ### Port 279 | 280 | RESTer will assume ports 80 and 443 for HTTP and HTTPS respectively. If you ofter require a specific custom port, you can set it with the `@port` setting. 281 | 282 | ``` 283 | GET /my-endpoint 284 | Host: api.my-example-site.com 285 | @host: 127.0.0.1 286 | @port: 8888 287 | ``` 288 | 289 | ### Default Headers 290 | 291 | To include a set of headers with each request, add them to the `"default_headers"` setting. This is a dictionary with the header names as the keys. 292 | 293 | ```json 294 | { 295 | "default_headers": { 296 | "Accept-Encoding": "gzip, deflate", 297 | "Cache-control": "no-cache" 298 | } 299 | } 300 | ``` 301 | 302 | ### Default Response Encodings 303 | 304 | RESTer can try to discern the encoding for a response. This doesn't always work, so it's a good idea to give it some encodings to try. Do this by supplying a list for the `"default_response_encodings"` setting. 305 | 306 | ```json 307 | { 308 | "default_response_encodings": ["utf-8", "ISO-8859-1", "ascii"] 309 | } 310 | ``` 311 | 312 | ### Response Commands 313 | 314 | After RESTer writes the response into a new tab, it selects the response body. With the body selected, it can perform a series of operations on the text. For example, you could instruct RESTer to pretty-print JSON responses. 315 | 316 | To specify commands for RESTer to run on the response, add entries to the `response_commands` member of the settings. The value for `response_commands` must be a list of string names of commands. 317 | 318 | ```json 319 | { 320 | "response_commands": ["pretty_json"] 321 | } 322 | ``` 323 | 324 | If you don't have the [PrettyJson](https://github.com/dzhibas/SublimePrettyJson) package installed, nothing bad will happen. You won't get any errors, but you won't get any pretty printed JSON either. 325 | 326 | If you're not sure what the command is for a given feature, you may be able to read its name from the command history. Run the command as you normally would, then open the Python console (`Ctrl` + \`), and enter `view.command_history(0)`. You should see the last command that was run on the current view. 327 | 328 | ```python 329 | >>> view.command_history(0) 330 | ('insert', {'characters': '\n\n'}, 1) 331 | ``` 332 | 333 | #### Response Syntax 334 | 335 | Start by finding the name of the syntax file. To view the current syntax, use the Python console: 336 | 337 | ```python 338 | >>> view.settings().get("syntax") 339 | 'Packages/HTTP Spec Syntax/httpspec.tmLanguage' 340 | ``` 341 | 342 | Use the bundled `set_syntax` command to set the syntax of any new resposes to the syntax file you chose. Here we are using "Packages/HTTP Spec Syntax/httpspec.tmLanguage", a syntax for HTTP messages that is part of the [httpspec/sublime-highlighting](https://github.com/httpspec/sublime-highlighting) package. 343 | 344 | ```json 345 | { 346 | "response_commands": [ 347 | { 348 | "name": "set_syntax", 349 | "args": { 350 | "syntax_file": "Packages/HTTP Spec Syntax/httpspec.tmLanguage" 351 | } 352 | } 353 | ] 354 | } 355 | ``` 356 | 357 | Notice that this command requires a `syntax_file` parameter. See [Request and Response Commands with Parameters](#request-and-response-commands-with-parameters) below to learn more about running commands with parameters. 358 | 359 | 360 | 361 | ### Request Commands 362 | 363 | RESTer can perform operations on the text of your request before it parses it. These commands are undone after the request is made, so your file is never modified. As with response commands, you'll specify these by adding a list entry to the settings file. This time, the setting name is `request_commands`. 364 | 365 | ```json 366 | { 367 | "request_commands": ["merge_variables"] 368 | } 369 | ``` 370 | 371 | A useful command to use as a request command is `merge_variables` from my [Merge Variables](https://github.com/pjdietz/sublime-merge-variables) package. Using Merge Variables, you can write your requests using placeholder variables that are not expanded until the moment you make the request. Merge Variables allows you to specify multiple configurations as well, so you can build a request once, and have it merge in various configurations. For example, you could start with this request: 372 | 373 | ``` 374 | GET http://{{API}}/my-endpoint 375 | ``` 376 | 377 | For a development configuration, this could expand to: 378 | 379 | ``` 380 | GET http://dev.my-example-site.com/my-endpoint 381 | ``` 382 | 383 | And for a production configuration: 384 | 385 | ``` 386 | GET http://api.my-example-site.com/my-endpoint 387 | ``` 388 | 389 | See [Merge Variables](https://github.com/pjdietz/sublime-merge-variables) for more information. 390 | 391 | ### Request and Response Commands with Parameters 392 | 393 | Most of the time, you'll only need to supply the name for a command. Some commands can take parameters, and you can pass these in by supplying an object instead of a string for your command. To use the object format, be sure to include the name of the command as the `name` member, and any parameters as the `args` member. 394 | 395 | ```json 396 | { 397 | "name": "merge_variables", 398 | "args": { 399 | "active_sets": ["mysite", "mysite-dev"] 400 | } 401 | } 402 | ``` 403 | 404 | ### Redirects 405 | 406 | RESTer will follow redirects automatically. To disable this or limit the response codes which will trigger an automatic redirect, modify these settings (defaults shown): 407 | 408 | ```json 409 | { 410 | "follow_redirects": true, 411 | "follow_redirect_status_codes": [300, 301, 302, 303, 307] 412 | } 413 | ``` 414 | 415 | ### cURL 416 | 417 | If you have [cURL](http://curl.haxx.se/) installed, you can set RESTer to use cURL instead of the Python `http.client` library. Most users will not need to do this, but this may be helpful for Linux users that are unable to make HTTPS requests because Python was not compiled with SSL support. Or, if you're familiar with using cURL on the command line, you may find it useful to add custom arguments to the cURL command. 418 | 419 | There are three settings related to cURL. The first is `http_client` which tells RESTer which client to use (allowed values are `python` for the native Python connector or `curl` for cURL.). 420 | 421 | Next is `curl_command` which is the path to the cURL executable. On OSX and Linux, if `curl` is on your path, you will not need to change this. Windows users providing a full path to `curl.exe` will need to use forward slashes in the path (e.g., `C:/curl/curl.exe`). 422 | 423 | The last setting is `curl_options`, an optional list of arguments to pass to the `curl` executable. Each option must be a separate string, so to send a custom header, use `["--header", "X-custom-header: header-value"]`, not `"--header X-custom-header: header-value"`. Here's an example showing these three settings: 424 | 425 | ```json 426 | { 427 | "http_client": "curl", 428 | "curl_command": "C:/curl/curl.exe", 429 | "curl_options": ["--header", "X-custom-header: header-value"] 430 | } 431 | ``` 432 | 433 | For more information on cURL, see the [cURL man page](http://curl.haxx.se/docs/manpage.html) 434 | 435 | ## Author 436 | 437 | **PJ Dietz** 438 | 439 | + [http://pjdietz.com](http://pjdietz.com) 440 | + [http://github.com/pjdietz](http://github.com/pjdietz) 441 | + [http://twitter.com/pjdietz](http://twitter.com/pjdietz) 442 | 443 | ## Copyright and license 444 | Copyright 2013 PJ Dietz 445 | 446 | [MIT License](LICENSE) 447 | -------------------------------------------------------------------------------- /rester/commands/http_request_command.py: -------------------------------------------------------------------------------- 1 | import codecs 2 | import hashlib 3 | import json 4 | import os 5 | import re 6 | import tempfile 7 | import time 8 | 9 | from ..constants import SETTINGS_FILE, SYNTAX_FILE 10 | from ..http import CurlRequestThread 11 | from ..http import HttpClientRequestThread 12 | from ..message import Request 13 | from ..overrideable import OverrideableSettings 14 | from ..parse import RequestParser 15 | from ..util import get_end_of_line_character 16 | from ..util import normalize_line_endings 17 | import sublime 18 | import sublime_plugin 19 | 20 | try: 21 | from urllib.parse import parse_qs 22 | from urllib.parse import urljoin 23 | from urllib.parse import urlparse 24 | except ImportError: 25 | # Python 2 26 | from urlparse import parse_qs 27 | from urlparse import urlparse 28 | from urlparse import urljoin 29 | 30 | MAX_REDIRECTS = 10 31 | MAX_GROUPS = 10 32 | RE_OVERRIDE = """^\s*@\s*([^\:]*)\s*:\s*(.*)$""" 33 | 34 | 35 | def _normalize_command(command): 36 | # Return a well formed dictionary for a request or response command 37 | 38 | valid = False 39 | 40 | # Find the string class. (str for py3, basestring for py2) 41 | string_class = str 42 | try: 43 | # If Python 2, use basestring instead of str 44 | #noinspection PyStatementEffect 45 | basestring 46 | string_class = basestring 47 | except NameError: 48 | pass 49 | 50 | if isinstance(command, string_class): 51 | command = {"name": command} 52 | valid = True 53 | elif isinstance(command, dict): 54 | if "name" in command: 55 | valid = True 56 | 57 | # Skip here if invalid. 58 | if not valid: 59 | print("Skipping invalid command.") 60 | print("Each command must be a string or a dict with a 'name'") 61 | print(command) 62 | return None 63 | 64 | # Ensure each command has all needed fields. 65 | if not "args" in command: 66 | command["args"] = None 67 | 68 | return command 69 | 70 | 71 | class ResterHttpRequestCommand(sublime_plugin.WindowCommand): 72 | def __init__(self, *args, **kwargs): 73 | sublime_plugin.WindowCommand.__init__(self, *args, **kwargs) 74 | self.encoding = "UTF-8" 75 | self.eol = "\n" 76 | self.request_view = None 77 | self.response_view = None 78 | self.settings = None 79 | self._command_hash = None 80 | self._completed_message = "Done." 81 | self._redirect_count = 0 82 | self._requesting = False 83 | self._request_view_group = None 84 | self._request_view_index = None 85 | 86 | def run(self, pos=None): 87 | # Store references. 88 | self.request_view = self.window.active_view() 89 | self._request_view_group, self._request_view_index = \ 90 | self.window.get_view_index(self.request_view) 91 | self.response_view = None 92 | self.eol = get_end_of_line_character(self.request_view) 93 | self.settings = self._get_settings() 94 | self._completed_message = "Done." 95 | self._redirect_count = 0 96 | self._requesting = False 97 | 98 | # Determine the encoding of the editor starting the request. 99 | # Sublime returns "Undefined" for views that are not yet saved. 100 | self.encoding = self.request_view.encoding() 101 | if not self.encoding or self.encoding == "Undefined": 102 | self.encoding = "UTF-8" 103 | 104 | # Store the text before any request commands are applied. 105 | originalText = self._get_selection(pos) 106 | 107 | # Perform commands on the request buffer. 108 | # Store the number of changes made so we can undo them. 109 | try: 110 | changes = self.request_view.change_count() 111 | self._run_request_commands() 112 | changes = self.request_view.change_count() - changes 113 | except AttributeError: 114 | # ST2 does not have a change_count() method. 115 | # It does allow creating an Edit on the fly though. 116 | edit = self.request_view.begin_edit() 117 | self._run_request_commands() 118 | self.request_view.end_edit(edit) 119 | changes = 1 120 | 121 | # Read the selected text. 122 | text = self._get_selection(pos) 123 | 124 | # Undo the request commands to return to the starting state. 125 | if text != originalText: 126 | for i in range(changes): 127 | self.request_view.run_command("undo") 128 | 129 | def replace(m): 130 | return variables.get(m.group(1), '') 131 | view = self.request_view 132 | extractions = [] 133 | view.find_all(r'(?:(#)\s*)?@([_a-zA-Z][_a-zA-Z0-9]*)\s*=\s*(.*)', 0, r'\1\2=\3', extractions) 134 | variables = {} 135 | for var in extractions: 136 | var, _, val = var.partition('=') 137 | if var[0] != '#': 138 | variables[var] = val.strip() 139 | for var in re.findall(r'(?:(#)\s*)?@([_a-zA-Z][_a-zA-Z0-9]*)\s*=\s*(.*)', originalText): 140 | if var[0] != '#': 141 | var, val = var[1], var[2] 142 | variables[var] = val.strip() 143 | text = re.sub(r'\{\{\s*([_a-zA-Z][_a-zA-Z0-9]*)\s*\}\}', replace, text) 144 | 145 | # Build a message.Request from the text. 146 | request_parser = RequestParser(self.settings, self.eol) 147 | request = request_parser.get_request(text) 148 | 149 | # Set the state to requesting. 150 | self._requesting = True 151 | 152 | # Create a new hash for this specific run of the command. 153 | command_hash = hashlib.sha1() 154 | command_hash.update(str(time.time()).encode("ascii")) 155 | command_hash = command_hash.hexdigest() 156 | self._command_hash = command_hash 157 | self.check_if_requesting(command_hash) 158 | 159 | # Make the request. 160 | self._start_request(request) 161 | 162 | def check_if_requesting(self, command_hash, i=0, direction=1): 163 | 164 | # Ignore if the command hash does not match. 165 | # That indicates the callback is stale. 166 | if self._command_hash != command_hash: 167 | return 168 | 169 | # Show an animation until the command is complete. 170 | if self._requesting: 171 | # This animates a little activity indicator in the status area. 172 | before = i % 8 173 | after = 7 - before 174 | if not after: 175 | direction = -1 176 | if not before: 177 | direction = 1 178 | i += direction 179 | message = "RESTer [%s=%s]" % (" " * before, " " * after) 180 | self.request_view.set_status("rester", message) 181 | fn = lambda: self.check_if_requesting(command_hash, i, direction) 182 | sublime.set_timeout(fn, 100) 183 | else: 184 | if not self._completed_message: 185 | self._completed_message = "Done." 186 | self.request_view.set_status("rester", self._completed_message) 187 | 188 | def handle_response_view(self, filepath, title, body_only): 189 | if self.response_view.is_loading(): 190 | fn = lambda: self.handle_response_view(filepath, title, 191 | body_only) 192 | sublime.set_timeout(fn, 100) 193 | 194 | else: 195 | view = self.response_view 196 | view.set_scratch(self.settings.get("response_scratch", True)) 197 | view.set_name(title) 198 | 199 | # Delete the temp file. 200 | os.remove(filepath) 201 | 202 | # Select the body. 203 | selection = None 204 | if body_only: 205 | selection = sublime.Region(0, view.size()) 206 | else: 207 | eol = get_end_of_line_character(view) 208 | headers = view.find(eol * 2, 0) 209 | if headers: 210 | selection = sublime.Region(headers.b, view.size()) 211 | 212 | if selection: 213 | view.sel().clear() 214 | view.sel().add(selection) 215 | 216 | # Run response commands and finish. 217 | self._run_response_commands() 218 | self._complete("Request complete. " + title) 219 | 220 | # Close all views in the response group other than the current 221 | # response view. 222 | if (not self.settings.get("response_group", None) is None) \ 223 | and self.settings.get("response_group_clean", False): 224 | 225 | views = self.window.views_in_group(self.window.active_group()) 226 | for other_view in views: 227 | if other_view.id() != view.id(): 228 | self.window.focus_view(other_view) 229 | self.window.run_command("close_file") 230 | 231 | # Set the focus back to the request group and view. 232 | if self.settings.get("request_focus", False): 233 | self.window.focus_group(self._request_view_group) 234 | self.window.focus_view(self.request_view) 235 | 236 | def handle_thread(self, thread): 237 | if thread.is_alive(): 238 | # Working... 239 | sublime.set_timeout(lambda: self.handle_thread(thread), 100) 240 | elif thread.success: 241 | # Success. 242 | self._complete_thread(thread) 243 | else: 244 | # Failed. 245 | if thread.message: 246 | self._complete(thread.message) 247 | else: 248 | self._complete("Unable to make request.") 249 | 250 | def _complete(self, message): 251 | # End the command and display a message. 252 | self._requesting = False 253 | self._completed_message = message 254 | 255 | def _complete_thread(self, thread): 256 | response = thread.response 257 | status_line = response.status_line 258 | 259 | # Output the response to the console. 260 | output_headers = self.settings.get("output_response_headers", True) 261 | output_body = self.settings.get("output_response_body", True) and \ 262 | response.body 263 | 264 | if output_headers or output_body: 265 | 266 | if thread.elapsed: 267 | print("\nResponse time:", thread.elapsed) 268 | 269 | print("\n[Response]") 270 | 271 | if output_headers: 272 | print(status_line) 273 | print("\n".join(response.header_lines)) 274 | 275 | if output_headers and output_body: 276 | print("") 277 | 278 | if output_body: 279 | try: 280 | print(response.body) 281 | except UnicodeEncodeError: 282 | # Python 2 283 | print(response.body.encode("UTF8")) 284 | 285 | # Redirect. 286 | follow = self.settings.get("follow_redirects", True) 287 | follow_codes = self.settings.get("follow_redirect_status_codes", []) 288 | if follow and response.status in follow_codes: 289 | self._follow_redirect(response, thread.request) 290 | return 291 | 292 | # Stop now if the user does not want a response buffer. 293 | if not self.settings.get("response_buffer", True): 294 | self._complete("Request complete. " + status_line) 295 | return 296 | 297 | # Open a temporary file to write the response to. 298 | # (Note: Using codecs to support Python 2.6) 299 | tmpfile = tempfile.NamedTemporaryFile("w", delete=False) 300 | filename = tmpfile.name 301 | tmpfile.close() 302 | tmpfile = codecs.open(filename, "w", encoding="UTF8") 303 | 304 | # Body only, but only on success. 305 | success = 200 <= thread.response.status <= 299 306 | if success and self.settings.get("body_only", False): 307 | if response.body: 308 | tmpfile.write(response.body) 309 | body_only = True 310 | 311 | # Status line and headers. 312 | else: 313 | 314 | tmpfile.write(response.status_line) 315 | tmpfile.write("\n") 316 | 317 | for header in response.header_lines: 318 | tmpfile.write(header) 319 | tmpfile.write("\n") 320 | 321 | if response.body: 322 | tmpfile.write("\n") 323 | tmpfile.write(response.body) 324 | 325 | body_only = False 326 | 327 | if not response.body: 328 | body_only = False 329 | 330 | # Close the file. 331 | tmpfile.close() 332 | filepath = tmpfile.name 333 | 334 | # Open the file in a new view. 335 | title = status_line 336 | if thread.elapsed: 337 | title += " (%.4f sec.)" % thread.elapsed 338 | self.response_view = self.window.open_file(filepath) 339 | self.response_view.set_syntax_file(SYNTAX_FILE) 340 | 341 | # Create, if needed, a group specific for responses and move the 342 | # response view to that group. 343 | response_group = self.settings.get("response_group", None) 344 | if response_group is not None: 345 | response_group = min(response_group, MAX_GROUPS) 346 | while self.window.num_groups() < response_group + 1: 347 | self.window.run_command("new_pane") 348 | self.window.set_view_index(self.response_view, response_group, 0) 349 | if not self.settings.get("request_focus", False): 350 | # Set the focus to the response group. 351 | self.window.focus_group(response_group) 352 | self.handle_response_view(tmpfile.name, title, body_only) 353 | 354 | def _get_selection(self, pos=None): 355 | # Return a string of the selected text or the entire buffer. 356 | # if there are multiple selections, concatenate them. 357 | view = self.request_view 358 | if pos is None: 359 | sels = view.sel() 360 | if len(sels) == 1 and sels[0].empty(): 361 | pos = sels[0].a 362 | if pos is not None: 363 | selection = view.substr(sublime.Region(0, view.size())) 364 | begin = selection.rfind('\n###', 0, pos) 365 | end = selection.find('\n###', pos) 366 | if begin != -1 and end != -1: 367 | selection = selection[begin:end] 368 | elif begin != -1: 369 | selection = selection[begin:] 370 | elif end != -1: 371 | selection = selection[:end] 372 | else: 373 | selection = "" 374 | for sel in sels: 375 | selection += view.substr(sel) 376 | return selection 377 | 378 | def _get_settings(self): 379 | 380 | # Return a setting-like object that combines the user's settings with 381 | # overrides from the current request. 382 | 383 | # Scan the request for overrides. 384 | text = self._get_selection().lstrip() 385 | text = normalize_line_endings(text, self.eol) 386 | 387 | headers = text.split(self.eol * 2, 1)[0] 388 | 389 | # Build a dictionary of the overrides. 390 | overrides = {} 391 | for (name, value) in re.findall(RE_OVERRIDE, headers, re.MULTILINE): 392 | try: 393 | overrides[name] = json.loads(value) 394 | except ValueError: 395 | # If unable to parse as JSON, assume it's an un-quoted string. 396 | overrides[name] = value 397 | 398 | # Return an OverrideableSettings object. 399 | return OverrideableSettings( 400 | settings=sublime.load_settings(SETTINGS_FILE), 401 | overrides=overrides) 402 | 403 | def _follow_redirect(self, response, request): 404 | # Stop now in the event of an infinite loop. 405 | if self._redirect_count > MAX_REDIRECTS: 406 | self._complete("Maximum redirects reached.") 407 | return 408 | 409 | # Read the location header and start a new request. 410 | location = response.get_header("Location") 411 | 412 | # Stop now if no location header. 413 | if not location: 414 | self._complete("Unable to redirect. No Location header found.") 415 | return 416 | 417 | # Create a new request instance. 418 | redirect = Request() 419 | 420 | # Use GET unless the original request was HEAD. 421 | if request.method == "HEAD": 422 | redirect.method = "HEAD" 423 | 424 | # Parse the Location URI 425 | uri = urlparse(location) 426 | 427 | if uri.netloc: 428 | # If there is a netloc, it's an absolute path. 429 | redirect.host = uri.netloc 430 | if uri.scheme: 431 | redirect.protocol = uri.scheme 432 | if uri.path: 433 | redirect.path = uri.path 434 | 435 | elif uri.path: 436 | # If no netloc, but there is a path, resolve from last. 437 | redirect.host = request.host 438 | redirect.path = urljoin(request.path, uri.path) 439 | 440 | # Always add the query. 441 | if uri.query: 442 | redirect.query += parse_qs(uri.query) 443 | 444 | print("\n[...redirecting...]") 445 | self._redirect_count += 1 446 | self._start_request(redirect) 447 | return 448 | 449 | def _run_request_commands(self): 450 | # Process the request buffer to prepare the contents for the request. 451 | view = self.request_view 452 | commands = self.settings.get("request_commands", []) 453 | for command in commands: 454 | command = _normalize_command(command) 455 | if command: 456 | view.run_command(command["name"], command["args"]) 457 | 458 | def _run_response_commands(self): 459 | view = self.response_view 460 | commands = self.settings.get("response_commands", []) 461 | for command in commands: 462 | command = _normalize_command(command) 463 | if command: 464 | view.run_command(command["name"], command["args"]) 465 | 466 | def _start_request(self, request): 467 | # Create, start, and handle a thread for the selection. 468 | if self.settings.get("output_request", True): 469 | print("\n[Request]") 470 | print(request.request_line) 471 | print("Host: %s" % request.host) 472 | for header in request.header_lines: 473 | print(header) 474 | if request.body: 475 | print("") 476 | try: 477 | print(request.body) 478 | except UnicodeEncodeError: 479 | # Python 2 480 | print(request.body.encode("UTF8")) 481 | 482 | client = self.settings.get("http_client", "python") 483 | if client == "python": 484 | thread_class = HttpClientRequestThread 485 | elif client == "curl": 486 | thread_class = CurlRequestThread 487 | else: 488 | message = "Invalid request_client. " 489 | message += "Must be 'python' or 'curl'. Found " + client 490 | self._complete(message) 491 | return 492 | 493 | thread = thread_class(request, self.settings, encoding=self.encoding) 494 | thread.start() 495 | self.handle_thread(thread) 496 | 497 | 498 | class ResterHttpResponseCloseEvent(sublime_plugin.ViewEventListener): 499 | @classmethod 500 | def is_applicable(cls, settings): 501 | syntax = settings.get('syntax') 502 | return syntax == SYNTAX_FILE 503 | 504 | @classmethod 505 | def applies_to_primary_view_only(cls): 506 | return True 507 | 508 | def on_pre_close(self): 509 | settings = sublime.load_settings(SETTINGS_FILE) 510 | response_group = settings.get("response_group", None) 511 | if response_group is not None: 512 | response_group = min(response_group, MAX_GROUPS) 513 | window = self.view.window() 514 | views = window.views_in_group(response_group) 515 | if len(views) == 1 and self.view == views[0]: 516 | window.focus_group(0) 517 | fn = lambda: window.run_command("close_pane") 518 | sublime.set_timeout(fn, 0) 519 | --------------------------------------------------------------------------------