├── screenshot.png ├── .gitignore ├── Makefile ├── LICENSE ├── setup.py ├── httpstat_test.sh ├── README.md └── httpstat.py /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reorx/httpstat/HEAD/screenshot.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Mac # 2 | .DS_Store 3 | 4 | # Vim swap files # 5 | *.sw[po] 6 | 7 | # Byte-compiled 8 | *.py[cod] 9 | 10 | # Distribution / packaging 11 | /build/ 12 | /dist/ 13 | *.egg-info/ 14 | 15 | # Sphinx documentation 16 | docs/_build/ 17 | 18 | # Others # 19 | .virtualenv 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test build 2 | 3 | test: 4 | @bash httpstat_test.sh 5 | 6 | clean: 7 | rm -rf build dist *.egg-info 8 | 9 | build: 10 | python setup.py build 11 | 12 | build-dist: 13 | python setup.py sdist bdist_wheel 14 | 15 | publish: clean build-dist 16 | python -m twine upload dist/* 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Xiao Meng 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, 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, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding=utf-8 3 | 4 | from setuptools import setup 5 | 6 | 7 | package_name = 'httpstat' 8 | filename = package_name + '.py' 9 | 10 | 11 | def get_version(): 12 | import ast 13 | 14 | with open(filename) as input_file: 15 | for line in input_file: 16 | if line.startswith('__version__'): 17 | return ast.parse(line).body[0].value.s 18 | 19 | 20 | def get_long_description(): 21 | try: 22 | with open('README.md', 'r') as f: 23 | return f.read() 24 | except IOError: 25 | return '' 26 | 27 | 28 | setup( 29 | name=package_name, 30 | version=get_version(), 31 | author='reorx', 32 | author_email='novoreorx@gmail.com', 33 | description='curl statistics made simple', 34 | url='https://github.com/reorx/httpstat', 35 | long_description=get_long_description(), 36 | long_description_content_type='text/markdown', 37 | py_modules=[package_name], 38 | entry_points={ 39 | 'console_scripts': [ 40 | 'httpstat = httpstat:main' 41 | ] 42 | }, 43 | license='License :: OSI Approved :: MIT License', 44 | ) 45 | -------------------------------------------------------------------------------- /httpstat_test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | function assert_exit() { 4 | rc=$? 5 | expect=$1 6 | if [ "$rc" -eq "$expect" ]; then 7 | echo OK 8 | else 9 | echo "Failed, expect $expect, got $rc" 10 | exit 1 11 | fi 12 | } 13 | 14 | function title() { 15 | echo 16 | echo "Test $1 ..." 17 | } 18 | 19 | function check_url() { 20 | url=$1 21 | echo "Checking $url ..." 22 | if curl -s --head "$url" >/dev/null; then 23 | echo "URL $url is accessible" 24 | else 25 | echo "URL $url is not accessible" 26 | exit 1 27 | fi 28 | } 29 | 30 | http_url="www.gstatic.com/generate_204" 31 | https_url="https://http2.akamai.com" 32 | 33 | check_url "$http_url" 34 | check_url "$https_url" 35 | 36 | for pybin in python python3; do 37 | #for pybin in python; do 38 | echo 39 | echo "# Test in $pybin" 40 | 41 | function main() { 42 | $pybin httpstat.py $@ 2>&1 43 | } 44 | 45 | function main_silent() { 46 | $pybin httpstat.py $@ >/dev/null 2>&1 47 | } 48 | 49 | title "basic" 50 | main_silent $http_url 51 | assert_exit 0 52 | 53 | title "https site ($https_url)" 54 | main_silent $https_url 55 | assert_exit 0 56 | 57 | title "comma decimal language (ru_RU)" 58 | LC_ALL=ru_RU main_silent $http_url 59 | assert_exit 0 60 | 61 | title "HTTPSTAT_DEBUG" 62 | HTTPSTAT_DEBUG=true main $http_url | grep -q 'HTTPSTAT_DEBUG=true' 63 | assert_exit 0 64 | 65 | title "HTTPSTAT_SHOW_SPEED" 66 | HTTPSTAT_SHOW_SPEED=true main $http_url | grep -q 'speed_download' 67 | assert_exit 0 68 | 69 | title "HTTPSTAT_CURL_BIN" 70 | HTTPSTAT_CURL_BIN=/usr/bin/curl HTTPSTAT_DEBUG=true main $http_url | grep -q '/usr/bin/curl' 71 | assert_exit 0 72 | 73 | title "HTTPSTAT_SHOW_IP" 74 | HTTPSTAT_SHOW_IP="true" main $http_url | grep -q 'Connected' 75 | assert_exit 0 76 | 77 | title "HTTPSTAT_SHOW_BODY=true, -G --data-urlencode \"a=中文\"" 78 | HTTPSTAT_SHOW_BODY="true" main_silent httpbin.org/get -G --data-urlencode "a=中文" 79 | assert_exit 0 80 | 81 | title "HTTPSTAT_SHOW_BODY=true, -G --data-urlencode \"a=中文\"" 82 | HTTPSTAT_SHOW_BODY="true" main_silent httpbin.org/post -X POST --data-urlencode "a=中文" 83 | assert_exit 0 84 | 85 | title "HTTPSTAT_SAVE_BODY=true" 86 | HTTPSTAT_SAVE_BODY=true main $http_url | grep -q 'stored in' 87 | assert_exit 0 88 | 89 | title "HTTPSTAT_SAVE_BODY=false" 90 | HTTPSTAT_SAVE_BODY=false HTTPSTAT_DEBUG=true main $http_url | grep -q 'rm body file' 91 | assert_exit 0 92 | 93 | title "HTTPSTAT_SHOW_BODY=true HTTPSTAT_SAVE_BODY=true, has 'is truncated, has 'stored in'" 94 | out=$(HTTPSTAT_SHOW_BODY=true HTTPSTAT_SAVE_BODY=true \ 95 | main $https_url) 96 | echo "$out" | grep -q 'is truncated' 97 | assert_exit 0 98 | 99 | echo "$out" | grep -q 'stored in' 100 | assert_exit 0 101 | 102 | title "HTTPSTAT_SHOW_BODY=true HTTPSTAT_SAVE_BODY=false, has 'is truncated', no 'stored in'" 103 | out=$(HTTPSTAT_SHOW_BODY=true HTTPSTAT_SAVE_BODY=false \ 104 | main $https_url) 105 | echo "$out" | grep -q 'is truncated' 106 | assert_exit 0 107 | 108 | echo "$out" | grep -q 'stored in' 109 | assert_exit 1 110 | done 111 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # httpstat 2 | 3 | ![screenshot](screenshot.png) 4 | 5 | httpstat visualizes `curl(1)` statistics in a way of beauty and clarity. 6 | 7 | It is a **single file🌟** Python script that has **no dependency👏** and is compatible with **Python 3🍻**. 8 | 9 | 10 | ## Installation 11 | 12 | There are three ways to get `httpstat`: 13 | 14 | - Download the script directly: `wget https://raw.githubusercontent.com/reorx/httpstat/master/httpstat.py` 15 | 16 | - Through pip: `pip install httpstat` 17 | 18 | - Through homebrew (macOS only): `brew install httpstat` 19 | 20 | > For Windows users, @davecheney's [Go version](https://github.com/davecheney/httpstat) is suggested. → [download link](https://github.com/davecheney/httpstat/releases) 21 | 22 | ## Usage 23 | 24 | Simply: 25 | 26 | ```bash 27 | python httpstat.py httpbin.org/get 28 | ``` 29 | 30 | If installed through pip or brew, you can use `httpstat` as a command: 31 | 32 | ```bash 33 | httpstat httpbin.org/get 34 | ``` 35 | 36 | ### cURL Options 37 | 38 | Because `httpstat` is a wrapper of cURL, you can pass any cURL supported option after the url (except for `-w`, `-D`, `-o`, `-s`, `-S` which are already used by `httpstat`): 39 | 40 | ```bash 41 | httpstat httpbin.org/post -X POST --data-urlencode "a=b" -v 42 | ``` 43 | 44 | ### Environment Variables 45 | 46 | `httpstat` has a bunch of environment variables to control its behavior. 47 | Here are some usage demos, you can also run `httpstat --help` to see full explanation. 48 | 49 | - HTTPSTAT_SHOW_BODY 50 | 51 | Set to `true` to show response body in the output, note that body length 52 | is limited to 1023 bytes, will be truncated if exceeds. Default is `false`. 53 | 54 | - HTTPSTAT_SHOW_IP 55 | 56 | By default httpstat shows remote and local IP/port address. 57 | Set to `false` to disable this feature. Default is `true`. 58 | 59 | - HTTPSTAT_SHOW_SPEED 60 | 61 | Set to `true` to show download and upload speed. Default is `false`. 62 | 63 | ```bash 64 | HTTPSTAT_SHOW_SPEED=true httpstat http://cachefly.cachefly.net/10mb.test 65 | 66 | ... 67 | speed_download: 3193.3 KiB/s, speed_upload: 0.0 KiB/s 68 | ``` 69 | 70 | - HTTPSTAT_SAVE_BODY 71 | 72 | By default httpstat stores body in a tmp file, 73 | set to `false` to disable this feature. Default is `true` 74 | 75 | - HTTPSTAT_CURL_BIN 76 | 77 | Indicate the cURL bin path to use. Default is `curl` from current shell $PATH. 78 | 79 | This exampe uses brew installed cURL to make HTTP2 request: 80 | 81 | ```bash 82 | HTTPSTAT_CURL_BIN=/usr/local/Cellar/curl/7.50.3/bin/curl httpstat https://http2.akamai.com/ --http2 83 | 84 | HTTP/2 200 85 | ... 86 | ``` 87 | 88 | > cURL must be compiled with nghttp2 to enable http2 feature 89 | > ([#12](https://github.com/reorx/httpstat/issues/12)). 90 | 91 | - HTTPSTAT_METRICS_ONLY 92 | 93 | If set to `true`, httpstat will only output metrics in json format, 94 | this is useful if you want to parse the data instead of reading it. 95 | 96 | - HTTPSTAT_DEBUG 97 | 98 | Set to `true` to see debugging logs. Default is `false` 99 | 100 | 101 | For convenience, you can export these environments in your `.zshrc` or `.bashrc`, 102 | example: 103 | 104 | ```bash 105 | export HTTPSTAT_SHOW_IP=false 106 | export HTTPSTAT_SHOW_SPEED=true 107 | export HTTPSTAT_SAVE_BODY=false 108 | ``` 109 | 110 | ## Related Projects 111 | 112 | Here are some implementations in various languages: 113 | 114 | 115 | - Go: [davecheney/httpstat](https://github.com/davecheney/httpstat) 116 | 117 | This is the Go alternative of httpstat, it's written in pure Go and relies no external programs. Choose it if you like solid binary executions (actually I do). 118 | 119 | - Go (library): [tcnksm/go-httpstat](https://github.com/tcnksm/go-httpstat) 120 | 121 | Other than being a cli tool, this project is used as library to help debugging latency of HTTP requests in Go code, very thoughtful and useful, see more in this [article](https://medium.com/@deeeet/trancing-http-request-latency-in-golang-65b2463f548c#.mm1u8kfnu) 122 | 123 | - Bash: [b4b4r07/httpstat](https://github.com/b4b4r07/httpstat) 124 | 125 | This is what exactly I want to do at the very beginning, but gave up due to not confident in my bash skill, good job! 126 | 127 | - Node: [yosuke-furukawa/httpstat](https://github.com/yosuke-furukawa/httpstat) 128 | 129 | [b4b4r07](https://twitter.com/b4b4r07) mentioned this in his [article](https://tellme.tokyo/post/2016/09/25/213810), could be used as a HTTP client also. 130 | 131 | - PHP: [talhasch/php-httpstat](https://github.com/talhasch/php-httpstat) 132 | 133 | The PHP implementation by @talhasch 134 | 135 | Some code blocks in `httpstat` are copied from other projects of mine, have a look: 136 | 137 | - [reorx/python-terminal-color](https://github.com/reorx/python-terminal-color) Drop-in single file library for printing terminal color. 138 | 139 | - [reorx/getenv](https://github.com/reorx/getenv) Environment variable definition with type. 140 | -------------------------------------------------------------------------------- /httpstat.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | # References: 4 | # man curl 5 | # https://curl.haxx.se/libcurl/c/curl_easy_getinfo.html 6 | # https://curl.haxx.se/libcurl/c/easy_getinfo_options.html 7 | # http://blog.kenweiner.com/2014/11/http-request-timings-with-curl.html 8 | 9 | from __future__ import print_function 10 | 11 | import os 12 | import json 13 | import sys 14 | import logging 15 | import tempfile 16 | import subprocess 17 | 18 | 19 | __version__ = '1.3.2' 20 | 21 | 22 | PY3 = sys.version_info >= (3,) 23 | 24 | if PY3: 25 | xrange = range 26 | 27 | 28 | # Env class is copied from https://github.com/reorx/getenv/blob/master/getenv.py 29 | class Env(object): 30 | prefix = 'HTTPSTAT' 31 | _instances = [] 32 | 33 | def __init__(self, key): 34 | self.key = key.format(prefix=self.prefix) 35 | Env._instances.append(self) 36 | 37 | def get(self, default=None): 38 | return os.environ.get(self.key, default) 39 | 40 | 41 | ENV_SHOW_BODY = Env('{prefix}_SHOW_BODY') 42 | ENV_SHOW_IP = Env('{prefix}_SHOW_IP') 43 | ENV_SHOW_SPEED = Env('{prefix}_SHOW_SPEED') 44 | ENV_SAVE_BODY = Env('{prefix}_SAVE_BODY') 45 | ENV_CURL_BIN = Env('{prefix}_CURL_BIN') 46 | ENV_METRICS_ONLY = Env('{prefix}_METRICS_ONLY') 47 | ENV_DEBUG = Env('{prefix}_DEBUG') 48 | 49 | 50 | curl_format = """{ 51 | "time_namelookup": %{time_namelookup}, 52 | "time_connect": %{time_connect}, 53 | "time_appconnect": %{time_appconnect}, 54 | "time_pretransfer": %{time_pretransfer}, 55 | "time_redirect": %{time_redirect}, 56 | "time_starttransfer": %{time_starttransfer}, 57 | "time_total": %{time_total}, 58 | "speed_download": %{speed_download}, 59 | "speed_upload": %{speed_upload}, 60 | "remote_ip": "%{remote_ip}", 61 | "remote_port": "%{remote_port}", 62 | "local_ip": "%{local_ip}", 63 | "local_port": "%{local_port}" 64 | }""" 65 | 66 | https_template = """ 67 | DNS Lookup TCP Connection TLS Handshake Server Processing Content Transfer 68 | [ {a0000} | {a0001} | {a0002} | {a0003} | {a0004} ] 69 | | | | | | 70 | namelookup:{b0000} | | | | 71 | connect:{b0001} | | | 72 | pretransfer:{b0002} | | 73 | starttransfer:{b0003} | 74 | total:{b0004} 75 | """[1:] 76 | 77 | http_template = """ 78 | DNS Lookup TCP Connection Server Processing Content Transfer 79 | [ {a0000} | {a0001} | {a0003} | {a0004} ] 80 | | | | | 81 | namelookup:{b0000} | | | 82 | connect:{b0001} | | 83 | starttransfer:{b0003} | 84 | total:{b0004} 85 | """[1:] 86 | 87 | 88 | # Color code is copied from https://github.com/reorx/python-terminal-color/blob/master/color_simple.py 89 | ISATTY = sys.stdout.isatty() 90 | 91 | 92 | def make_color(code): 93 | def color_func(s): 94 | if not ISATTY: 95 | return s 96 | tpl = '\x1b[{}m{}\x1b[0m' 97 | return tpl.format(code, s) 98 | return color_func 99 | 100 | 101 | red = make_color(31) 102 | green = make_color(32) 103 | yellow = make_color(33) 104 | blue = make_color(34) 105 | magenta = make_color(35) 106 | cyan = make_color(36) 107 | 108 | bold = make_color(1) 109 | underline = make_color(4) 110 | 111 | grayscale = {(i - 232): make_color('38;5;' + str(i)) for i in xrange(232, 256)} 112 | 113 | 114 | def quit(s, code=0): 115 | if s is not None: 116 | print(s) 117 | sys.exit(code) 118 | 119 | 120 | def print_help(): 121 | help = """ 122 | Usage: httpstat URL [CURL_OPTIONS] 123 | httpstat -h | --help 124 | httpstat --version 125 | 126 | Arguments: 127 | URL url to request, could be with or without `http(s)://` prefix 128 | 129 | Options: 130 | CURL_OPTIONS any curl supported options, except for -w -D -o -S -s, 131 | which are already used internally. 132 | -h --help show this screen. 133 | --version show version. 134 | 135 | Environments: 136 | HTTPSTAT_SHOW_BODY Set to `true` to show response body in the output, 137 | note that body length is limited to 1023 bytes, will be 138 | truncated if exceeds. Default is `false`. 139 | HTTPSTAT_SHOW_IP By default httpstat shows remote and local IP/port address. 140 | Set to `false` to disable this feature. Default is `true`. 141 | HTTPSTAT_SHOW_SPEED Set to `true` to show download and upload speed. 142 | Default is `false`. 143 | HTTPSTAT_SAVE_BODY By default httpstat stores body in a tmp file, 144 | set to `false` to disable this feature. Default is `true` 145 | HTTPSTAT_CURL_BIN Indicate the curl bin path to use. Default is `curl` 146 | from current shell $PATH. 147 | HTTPSTAT_DEBUG Set to `true` to see debugging logs. Default is `false` 148 | """[1:-1] 149 | print(help) 150 | 151 | 152 | def main(): 153 | args = sys.argv[1:] 154 | if not args: 155 | print_help() 156 | quit(None, 0) 157 | 158 | # get envs 159 | show_body = 'true' in ENV_SHOW_BODY.get('false').lower() 160 | show_ip = 'true' in ENV_SHOW_IP.get('true').lower() 161 | show_speed = 'true'in ENV_SHOW_SPEED.get('false').lower() 162 | save_body = 'true' in ENV_SAVE_BODY.get('true').lower() 163 | curl_bin = ENV_CURL_BIN.get('curl') 164 | metrics_only = 'true' in ENV_METRICS_ONLY.get('false').lower() 165 | is_debug = 'true' in ENV_DEBUG.get('false').lower() 166 | 167 | # configure logging 168 | if is_debug: 169 | log_level = logging.DEBUG 170 | else: 171 | log_level = logging.INFO 172 | logging.basicConfig(level=log_level) 173 | lg = logging.getLogger('httpstat') 174 | 175 | # log envs 176 | lg.debug('Envs:\n%s', '\n'.join(' {}={}'.format(i.key, i.get('')) for i in Env._instances)) 177 | lg.debug('Flags: %s', dict( 178 | show_body=show_body, 179 | show_ip=show_ip, 180 | show_speed=show_speed, 181 | save_body=save_body, 182 | curl_bin=curl_bin, 183 | is_debug=is_debug, 184 | )) 185 | 186 | # get url 187 | url = args[0] 188 | if url in ['-h', '--help']: 189 | print_help() 190 | quit(None, 0) 191 | elif url == '--version': 192 | print('httpstat {}'.format(__version__)) 193 | quit(None, 0) 194 | 195 | curl_args = args[1:] 196 | 197 | # check curl args 198 | exclude_options = [ 199 | '-w', '--write-out', 200 | '-D', '--dump-header', 201 | '-o', '--output', 202 | '-s', '--silent', 203 | ] 204 | for i in exclude_options: 205 | if i in curl_args: 206 | quit(yellow('Error: {} is not allowed in extra curl args'.format(i)), 1) 207 | 208 | # tempfile for output 209 | bodyf = tempfile.NamedTemporaryFile(delete=False) 210 | bodyf.close() 211 | 212 | headerf = tempfile.NamedTemporaryFile(delete=False) 213 | headerf.close() 214 | 215 | # run cmd 216 | cmd_env = os.environ.copy() 217 | cmd_env.update( 218 | LC_ALL='C', 219 | ) 220 | cmd_core = [curl_bin, '-w', curl_format, '-D', headerf.name, '-o', bodyf.name, '-s', '-S'] 221 | cmd = cmd_core + curl_args + [url] 222 | lg.debug('cmd: %s', cmd) 223 | p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=cmd_env) 224 | out, err = p.communicate() 225 | if PY3: 226 | out, err = out.decode(), err.decode() 227 | lg.debug('out: %s', out) 228 | 229 | # print stderr 230 | if p.returncode == 0: 231 | if err: 232 | print(grayscale[16](err)) 233 | else: 234 | _cmd = list(cmd) 235 | _cmd[2] = '' 236 | _cmd[4] = '' 237 | _cmd[6] = '' 238 | print('> {}'.format(' '.join(_cmd))) 239 | quit(yellow('curl error: {}'.format(err)), p.returncode) 240 | 241 | # parse output 242 | try: 243 | d = json.loads(out) 244 | except ValueError as e: 245 | print(yellow('Could not decode json: {}'.format(e))) 246 | print('curl result:', p.returncode, grayscale[16](out), grayscale[16](err)) 247 | quit(None, 1) 248 | 249 | # convert time_ metrics from seconds to milliseconds 250 | for k in d: 251 | if k.startswith('time_'): 252 | v = d[k] 253 | # Convert time_ values to milliseconds in int 254 | if isinstance(v, float): 255 | # Before 7.61.0, time values are represented as seconds in float 256 | d[k] = int(v * 1000) 257 | elif isinstance(v, int): 258 | # Starting from 7.61.0, libcurl uses microsecond in int 259 | # to return time values, references: 260 | # https://daniel.haxx.se/blog/2018/07/11/curl-7-61-0/ 261 | # https://curl.se/bug/?i=2495 262 | d[k] = int(v / 1000) 263 | else: 264 | raise TypeError('{} value type is invalid: {}'.format(k, type(v))) 265 | 266 | # calculate ranges 267 | d.update( 268 | range_dns=d['time_namelookup'], 269 | range_connection=d['time_connect'] - d['time_namelookup'], 270 | range_ssl=d['time_pretransfer'] - d['time_connect'], 271 | range_server=d['time_starttransfer'] - d['time_pretransfer'], 272 | range_transfer=d['time_total'] - d['time_starttransfer'], 273 | ) 274 | 275 | # print json if metrics_only is enabled 276 | if metrics_only: 277 | print(json.dumps(d, indent=2)) 278 | quit(None, 0) 279 | 280 | # ip 281 | if show_ip: 282 | s = 'Connected to {}:{} from {}:{}'.format( 283 | cyan(d['remote_ip']), cyan(d['remote_port']), 284 | d['local_ip'], d['local_port'], 285 | ) 286 | print(s) 287 | print() 288 | 289 | # print header & body summary 290 | with open(headerf.name, 'r') as f: 291 | headers = f.read().strip() 292 | # remove header file 293 | lg.debug('rm header file %s', headerf.name) 294 | os.remove(headerf.name) 295 | 296 | for loop, line in enumerate(headers.split('\n')): 297 | if loop == 0: 298 | p1, p2 = tuple(line.split('/')) 299 | print(green(p1) + grayscale[14]('/') + cyan(p2)) 300 | else: 301 | pos = line.find(':') 302 | print(grayscale[14](line[:pos + 1]) + cyan(line[pos + 1:])) 303 | 304 | print() 305 | 306 | # body 307 | if show_body: 308 | body_limit = 1024 309 | with open(bodyf.name, 'r') as f: 310 | body = f.read().strip() 311 | body_len = len(body) 312 | 313 | if body_len > body_limit: 314 | print(body[:body_limit] + cyan('...')) 315 | print() 316 | s = '{} is truncated ({} out of {})'.format(green('Body'), body_limit, body_len) 317 | if save_body: 318 | s += ', stored in: {}'.format(bodyf.name) 319 | print(s) 320 | else: 321 | print(body) 322 | else: 323 | if save_body: 324 | print('{} stored in: {}'.format(green('Body'), bodyf.name)) 325 | 326 | # remove body file 327 | if not save_body: 328 | lg.debug('rm body file %s', bodyf.name) 329 | os.remove(bodyf.name) 330 | 331 | # print stat 332 | if url.startswith('https://'): 333 | template = https_template 334 | else: 335 | template = http_template 336 | 337 | # colorize template first line 338 | tpl_parts = template.split('\n') 339 | tpl_parts[0] = grayscale[16](tpl_parts[0]) 340 | template = '\n'.join(tpl_parts) 341 | 342 | def fmta(s): 343 | return cyan('{:^7}'.format(str(s) + 'ms')) 344 | 345 | def fmtb(s): 346 | return cyan('{:<7}'.format(str(s) + 'ms')) 347 | 348 | stat = template.format( 349 | # a 350 | a0000=fmta(d['range_dns']), 351 | a0001=fmta(d['range_connection']), 352 | a0002=fmta(d['range_ssl']), 353 | a0003=fmta(d['range_server']), 354 | a0004=fmta(d['range_transfer']), 355 | # b 356 | b0000=fmtb(d['time_namelookup']), 357 | b0001=fmtb(d['time_connect']), 358 | b0002=fmtb(d['time_pretransfer']), 359 | b0003=fmtb(d['time_starttransfer']), 360 | b0004=fmtb(d['time_total']), 361 | ) 362 | print() 363 | print(stat) 364 | 365 | # speed, originally bytes per second 366 | if show_speed: 367 | print('speed_download: {:.1f} KiB/s, speed_upload: {:.1f} KiB/s'.format( 368 | d['speed_download'] / 1024, d['speed_upload'] / 1024)) 369 | 370 | 371 | if __name__ == '__main__': 372 | main() 373 | --------------------------------------------------------------------------------