├── __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 |
--------------------------------------------------------------------------------