├── .dockerignore
├── .gitignore
├── .travis.yml
├── CHANGELOG.md
├── Dockerfile
├── LICENSE
├── README.md
├── _config.yml
├── commands.go
├── config
└── config.go
├── docker-entrypoint.sh
├── docs
└── images
│ └── screencast.gif
├── formatter
├── binary.go
├── formatter.go
├── formatter_test.go
├── html.go
├── json.go
└── text.go
├── go.mod
├── go.sum
├── keys.go
├── request-headers.go
├── sample-config.toml
├── status-line.go
└── wuzz.go
/.dockerignore:
--------------------------------------------------------------------------------
1 | **
2 |
3 | !config
4 | !formatter
5 | !*.go
6 | !go.mod
7 | !sample-config.toml
8 | !docker-entrypoint.sh
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | wuzz
2 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: go
2 | go:
3 | - 1.12.x #Update go version
4 | - tip
5 | os:
6 | - linux
7 | # remove osx, getting vm from travis is extremely slow
8 | # - osx
9 |
10 | arch:
11 | - amd64
12 | - ppc64le
13 | env:
14 | - "PATH=/home/travis/gopath/bin:$PATH"
15 | script:
16 | - test -z "$(go get -a)"
17 | - test -z "$(gofmt -l ./)"
18 | - test -z "$(go vet -v ./...)"
19 | - go test ./...
20 | - go build
21 | # after_success:
22 | # # Publish as pre-release if it isn't a pull request
23 | # - 'if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then go get github.com/mitchellh/gox && go get github.com/tcnksm/ghr; fi'
24 | # - 'if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then gox -output "dist/{{.Dir}}_{{.OS}}_{{.Arch}}" && ghr --username $GITHUB_USERNAME --token $GITHUB_TOKEN --replace --prerelease --debug pre-release dist/; fi'
25 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## 0.5.0 2020.01.19
2 |
3 | - Added context specific search for HTML (goquery)
4 | - Request data displayed in request history
5 | - Fixed JSON indentation
6 | - Redirect toggle command Added
7 | - Multiple minor bug fixes
8 |
9 | ## 0.4.0 2017.08.21
10 |
11 | - Save/load requests (`-f`/`--file` flags for loading)
12 | - Multipart form handling (`-F`/`--form` flags)
13 | - Edit window content in external editor
14 | - Colorized html output
15 | - Context specific search (github.com/tidwall/gjson for JSON)
16 | - More consistency with cURL API (`--data-urlencode` flag added)
17 | - Update to the latest `gocui` ui library
18 |
19 |
20 | ## 0.3.0 2017.03.07
21 |
22 | - Request header autocompletion
23 | - Configurable statusline
24 | - JSON requests with `-j`/`--json` flags
25 | - Allow insecure HTTPS requests (`-k`/`--insecure` flags)
26 | - Socks proxy support (`-x`/`--proxy` flags)
27 | - Disable following redirects (`-R`/`--disable-redirects` flags)
28 | - Enhanced TLS support (`-T`/`--tls`, `-1`/`--tlsv1`, `--tlsv1.0`, `--tlsv1.1`, `--tlsv1.2` flags)
29 | - Commands for line and word deletion
30 | - Home/end navigation fix
31 |
32 | ## 0.2.0 2017.02.18
33 |
34 | - Config file support with configurable keybindings
35 | - Help popup (F1 key)
36 | - Ignore invalid SSL certs with the --insecure flag
37 | - PATCH request support
38 | - Allow JSON request body (--data-binary flag)
39 | - Colorized JSON response
40 | - Parameter encoding bugfix
41 | - Multiple UI bugfixes
42 |
43 | ## 0.1.0 2017.02.11
44 |
45 | Initial release
46 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM alpine:3.12 AS permissions-giver
2 |
3 | # Make sure docker-entrypoint.sh is executable, regardless of the build host.
4 | WORKDIR /out
5 | COPY docker-entrypoint.sh .
6 | RUN chmod +x docker-entrypoint.sh
7 |
8 | FROM golang:1.14-alpine3.12 AS builder
9 |
10 | # Build wuzz
11 | WORKDIR /out
12 | COPY . .
13 | RUN go build .
14 |
15 | FROM alpine:3.12 AS organizer
16 |
17 | # Prepare executables
18 | WORKDIR /out
19 | COPY --from=builder /out/wuzz .
20 | COPY --from=permissions-giver /out/docker-entrypoint.sh .
21 |
22 | FROM alpine:3.12 AS runner
23 | WORKDIR /wuzz
24 | COPY --from=organizer /out /usr/local/bin
25 | ENTRYPOINT [ "docker-entrypoint.sh" ]
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU AFFERO GENERAL PUBLIC LICENSE
2 | Version 3, 19 November 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 | Preamble
9 |
10 | The GNU Affero General Public License is a free, copyleft license for
11 | software and other kinds of works, specifically designed to ensure
12 | cooperation with the community in the case of network server software.
13 |
14 | The licenses for most software and other practical works are designed
15 | to take away your freedom to share and change the works. By contrast,
16 | our General Public Licenses are intended to guarantee your freedom to
17 | share and change all versions of a program--to make sure it remains free
18 | software for all its users.
19 |
20 | When we speak of free software, we are referring to freedom, not
21 | price. Our General Public Licenses are designed to make sure that you
22 | have the freedom to distribute copies of free software (and charge for
23 | them if you wish), that you receive source code or can get it if you
24 | want it, that you can change the software or use pieces of it in new
25 | free programs, and that you know you can do these things.
26 |
27 | Developers that use our General Public Licenses protect your rights
28 | with two steps: (1) assert copyright on the software, and (2) offer
29 | you this License which gives you legal permission to copy, distribute
30 | and/or modify the software.
31 |
32 | A secondary benefit of defending all users' freedom is that
33 | improvements made in alternate versions of the program, if they
34 | receive widespread use, become available for other developers to
35 | incorporate. Many developers of free software are heartened and
36 | encouraged by the resulting cooperation. However, in the case of
37 | software used on network servers, this result may fail to come about.
38 | The GNU General Public License permits making a modified version and
39 | letting the public access it on a server without ever releasing its
40 | source code to the public.
41 |
42 | The GNU Affero General Public License is designed specifically to
43 | ensure that, in such cases, the modified source code becomes available
44 | to the community. It requires the operator of a network server to
45 | provide the source code of the modified version running there to the
46 | users of that server. Therefore, public use of a modified version, on
47 | a publicly accessible server, gives the public access to the source
48 | code of the modified version.
49 |
50 | An older license, called the Affero General Public License and
51 | published by Affero, was designed to accomplish similar goals. This is
52 | a different license, not a version of the Affero GPL, but Affero has
53 | released a new version of the Affero GPL which permits relicensing under
54 | this license.
55 |
56 | The precise terms and conditions for copying, distribution and
57 | modification follow.
58 |
59 | TERMS AND CONDITIONS
60 |
61 | 0. Definitions.
62 |
63 | "This License" refers to version 3 of the GNU Affero General Public License.
64 |
65 | "Copyright" also means copyright-like laws that apply to other kinds of
66 | works, such as semiconductor masks.
67 |
68 | "The Program" refers to any copyrightable work licensed under this
69 | License. Each licensee is addressed as "you". "Licensees" and
70 | "recipients" may be individuals or organizations.
71 |
72 | To "modify" a work means to copy from or adapt all or part of the work
73 | in a fashion requiring copyright permission, other than the making of an
74 | exact copy. The resulting work is called a "modified version" of the
75 | earlier work or a work "based on" the earlier work.
76 |
77 | A "covered work" means either the unmodified Program or a work based
78 | on the Program.
79 |
80 | To "propagate" a work means to do anything with it that, without
81 | permission, would make you directly or secondarily liable for
82 | infringement under applicable copyright law, except executing it on a
83 | computer or modifying a private copy. Propagation includes copying,
84 | distribution (with or without modification), making available to the
85 | public, and in some countries other activities as well.
86 |
87 | To "convey" a work means any kind of propagation that enables other
88 | parties to make or receive copies. Mere interaction with a user through
89 | a computer network, with no transfer of a copy, is not conveying.
90 |
91 | An interactive user interface displays "Appropriate Legal Notices"
92 | to the extent that it includes a convenient and prominently visible
93 | feature that (1) displays an appropriate copyright notice, and (2)
94 | tells the user that there is no warranty for the work (except to the
95 | extent that warranties are provided), that licensees may convey the
96 | work under this License, and how to view a copy of this License. If
97 | the interface presents a list of user commands or options, such as a
98 | menu, a prominent item in the list meets this criterion.
99 |
100 | 1. Source Code.
101 |
102 | The "source code" for a work means the preferred form of the work
103 | for making modifications to it. "Object code" means any non-source
104 | form of a work.
105 |
106 | A "Standard Interface" means an interface that either is an official
107 | standard defined by a recognized standards body, or, in the case of
108 | interfaces specified for a particular programming language, one that
109 | is widely used among developers working in that language.
110 |
111 | The "System Libraries" of an executable work include anything, other
112 | than the work as a whole, that (a) is included in the normal form of
113 | packaging a Major Component, but which is not part of that Major
114 | Component, and (b) serves only to enable use of the work with that
115 | Major Component, or to implement a Standard Interface for which an
116 | implementation is available to the public in source code form. A
117 | "Major Component", in this context, means a major essential component
118 | (kernel, window system, and so on) of the specific operating system
119 | (if any) on which the executable work runs, or a compiler used to
120 | produce the work, or an object code interpreter used to run it.
121 |
122 | The "Corresponding Source" for a work in object code form means all
123 | the source code needed to generate, install, and (for an executable
124 | work) run the object code and to modify the work, including scripts to
125 | control those activities. However, it does not include the work's
126 | System Libraries, or general-purpose tools or generally available free
127 | programs which are used unmodified in performing those activities but
128 | which are not part of the work. For example, Corresponding Source
129 | includes interface definition files associated with source files for
130 | the work, and the source code for shared libraries and dynamically
131 | linked subprograms that the work is specifically designed to require,
132 | such as by intimate data communication or control flow between those
133 | subprograms and other parts of the work.
134 |
135 | The Corresponding Source need not include anything that users
136 | can regenerate automatically from other parts of the Corresponding
137 | Source.
138 |
139 | The Corresponding Source for a work in source code form is that
140 | same work.
141 |
142 | 2. Basic Permissions.
143 |
144 | All rights granted under this License are granted for the term of
145 | copyright on the Program, and are irrevocable provided the stated
146 | conditions are met. This License explicitly affirms your unlimited
147 | permission to run the unmodified Program. The output from running a
148 | covered work is covered by this License only if the output, given its
149 | content, constitutes a covered work. This License acknowledges your
150 | rights of fair use or other equivalent, as provided by copyright law.
151 |
152 | You may make, run and propagate covered works that you do not
153 | convey, without conditions so long as your license otherwise remains
154 | in force. You may convey covered works to others for the sole purpose
155 | of having them make modifications exclusively for you, or provide you
156 | with facilities for running those works, provided that you comply with
157 | the terms of this License in conveying all material for which you do
158 | not control copyright. Those thus making or running the covered works
159 | for you must do so exclusively on your behalf, under your direction
160 | and control, on terms that prohibit them from making any copies of
161 | your copyrighted material outside their relationship with you.
162 |
163 | Conveying under any other circumstances is permitted solely under
164 | the conditions stated below. Sublicensing is not allowed; section 10
165 | makes it unnecessary.
166 |
167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
168 |
169 | No covered work shall be deemed part of an effective technological
170 | measure under any applicable law fulfilling obligations under article
171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or
172 | similar laws prohibiting or restricting circumvention of such
173 | measures.
174 |
175 | When you convey a covered work, you waive any legal power to forbid
176 | circumvention of technological measures to the extent such circumvention
177 | is effected by exercising rights under this License with respect to
178 | the covered work, and you disclaim any intention to limit operation or
179 | modification of the work as a means of enforcing, against the work's
180 | users, your or third parties' legal rights to forbid circumvention of
181 | technological measures.
182 |
183 | 4. Conveying Verbatim Copies.
184 |
185 | You may convey verbatim copies of the Program's source code as you
186 | receive it, in any medium, provided that you conspicuously and
187 | appropriately publish on each copy an appropriate copyright notice;
188 | keep intact all notices stating that this License and any
189 | non-permissive terms added in accord with section 7 apply to the code;
190 | keep intact all notices of the absence of any warranty; and give all
191 | recipients a copy of this License along with the Program.
192 |
193 | You may charge any price or no price for each copy that you convey,
194 | and you may offer support or warranty protection for a fee.
195 |
196 | 5. Conveying Modified Source Versions.
197 |
198 | You may convey a work based on the Program, or the modifications to
199 | produce it from the Program, in the form of source code under the
200 | terms of section 4, provided that you also meet all of these conditions:
201 |
202 | a) The work must carry prominent notices stating that you modified
203 | it, and giving a relevant date.
204 |
205 | b) The work must carry prominent notices stating that it is
206 | released under this License and any conditions added under section
207 | 7. This requirement modifies the requirement in section 4 to
208 | "keep intact all notices".
209 |
210 | c) You must license the entire work, as a whole, under this
211 | License to anyone who comes into possession of a copy. This
212 | License will therefore apply, along with any applicable section 7
213 | additional terms, to the whole of the work, and all its parts,
214 | regardless of how they are packaged. This License gives no
215 | permission to license the work in any other way, but it does not
216 | invalidate such permission if you have separately received it.
217 |
218 | d) If the work has interactive user interfaces, each must display
219 | Appropriate Legal Notices; however, if the Program has interactive
220 | interfaces that do not display Appropriate Legal Notices, your
221 | work need not make them do so.
222 |
223 | A compilation of a covered work with other separate and independent
224 | works, which are not by their nature extensions of the covered work,
225 | and which are not combined with it such as to form a larger program,
226 | in or on a volume of a storage or distribution medium, is called an
227 | "aggregate" if the compilation and its resulting copyright are not
228 | used to limit the access or legal rights of the compilation's users
229 | beyond what the individual works permit. Inclusion of a covered work
230 | in an aggregate does not cause this License to apply to the other
231 | parts of the aggregate.
232 |
233 | 6. Conveying Non-Source Forms.
234 |
235 | You may convey a covered work in object code form under the terms
236 | of sections 4 and 5, provided that you also convey the
237 | machine-readable Corresponding Source under the terms of this License,
238 | in one of these ways:
239 |
240 | a) Convey the object code in, or embodied in, a physical product
241 | (including a physical distribution medium), accompanied by the
242 | Corresponding Source fixed on a durable physical medium
243 | customarily used for software interchange.
244 |
245 | b) Convey the object code in, or embodied in, a physical product
246 | (including a physical distribution medium), accompanied by a
247 | written offer, valid for at least three years and valid for as
248 | long as you offer spare parts or customer support for that product
249 | model, to give anyone who possesses the object code either (1) a
250 | copy of the Corresponding Source for all the software in the
251 | product that is covered by this License, on a durable physical
252 | medium customarily used for software interchange, for a price no
253 | more than your reasonable cost of physically performing this
254 | conveying of source, or (2) access to copy the
255 | Corresponding Source from a network server at no charge.
256 |
257 | c) Convey individual copies of the object code with a copy of the
258 | written offer to provide the Corresponding Source. This
259 | alternative is allowed only occasionally and noncommercially, and
260 | only if you received the object code with such an offer, in accord
261 | with subsection 6b.
262 |
263 | d) Convey the object code by offering access from a designated
264 | place (gratis or for a charge), and offer equivalent access to the
265 | Corresponding Source in the same way through the same place at no
266 | further charge. You need not require recipients to copy the
267 | Corresponding Source along with the object code. If the place to
268 | copy the object code is a network server, the Corresponding Source
269 | may be on a different server (operated by you or a third party)
270 | that supports equivalent copying facilities, provided you maintain
271 | clear directions next to the object code saying where to find the
272 | Corresponding Source. Regardless of what server hosts the
273 | Corresponding Source, you remain obligated to ensure that it is
274 | available for as long as needed to satisfy these requirements.
275 |
276 | e) Convey the object code using peer-to-peer transmission, provided
277 | you inform other peers where the object code and Corresponding
278 | Source of the work are being offered to the general public at no
279 | charge under subsection 6d.
280 |
281 | A separable portion of the object code, whose source code is excluded
282 | from the Corresponding Source as a System Library, need not be
283 | included in conveying the object code work.
284 |
285 | A "User Product" is either (1) a "consumer product", which means any
286 | tangible personal property which is normally used for personal, family,
287 | or household purposes, or (2) anything designed or sold for incorporation
288 | into a dwelling. In determining whether a product is a consumer product,
289 | doubtful cases shall be resolved in favor of coverage. For a particular
290 | product received by a particular user, "normally used" refers to a
291 | typical or common use of that class of product, regardless of the status
292 | of the particular user or of the way in which the particular user
293 | actually uses, or expects or is expected to use, the product. A product
294 | is a consumer product regardless of whether the product has substantial
295 | commercial, industrial or non-consumer uses, unless such uses represent
296 | the only significant mode of use of the product.
297 |
298 | "Installation Information" for a User Product means any methods,
299 | procedures, authorization keys, or other information required to install
300 | and execute modified versions of a covered work in that User Product from
301 | a modified version of its Corresponding Source. The information must
302 | suffice to ensure that the continued functioning of the modified object
303 | code is in no case prevented or interfered with solely because
304 | modification has been made.
305 |
306 | If you convey an object code work under this section in, or with, or
307 | specifically for use in, a User Product, and the conveying occurs as
308 | part of a transaction in which the right of possession and use of the
309 | User Product is transferred to the recipient in perpetuity or for a
310 | fixed term (regardless of how the transaction is characterized), the
311 | Corresponding Source conveyed under this section must be accompanied
312 | by the Installation Information. But this requirement does not apply
313 | if neither you nor any third party retains the ability to install
314 | modified object code on the User Product (for example, the work has
315 | been installed in ROM).
316 |
317 | The requirement to provide Installation Information does not include a
318 | requirement to continue to provide support service, warranty, or updates
319 | for a work that has been modified or installed by the recipient, or for
320 | the User Product in which it has been modified or installed. Access to a
321 | network may be denied when the modification itself materially and
322 | adversely affects the operation of the network or violates the rules and
323 | protocols for communication across the network.
324 |
325 | Corresponding Source conveyed, and Installation Information provided,
326 | in accord with this section must be in a format that is publicly
327 | documented (and with an implementation available to the public in
328 | source code form), and must require no special password or key for
329 | unpacking, reading or copying.
330 |
331 | 7. Additional Terms.
332 |
333 | "Additional permissions" are terms that supplement the terms of this
334 | License by making exceptions from one or more of its conditions.
335 | Additional permissions that are applicable to the entire Program shall
336 | be treated as though they were included in this License, to the extent
337 | that they are valid under applicable law. If additional permissions
338 | apply only to part of the Program, that part may be used separately
339 | under those permissions, but the entire Program remains governed by
340 | this License without regard to the additional permissions.
341 |
342 | When you convey a copy of a covered work, you may at your option
343 | remove any additional permissions from that copy, or from any part of
344 | it. (Additional permissions may be written to require their own
345 | removal in certain cases when you modify the work.) You may place
346 | additional permissions on material, added by you to a covered work,
347 | for which you have or can give appropriate copyright permission.
348 |
349 | Notwithstanding any other provision of this License, for material you
350 | add to a covered work, you may (if authorized by the copyright holders of
351 | that material) supplement the terms of this License with terms:
352 |
353 | a) Disclaiming warranty or limiting liability differently from the
354 | terms of sections 15 and 16 of this License; or
355 |
356 | b) Requiring preservation of specified reasonable legal notices or
357 | author attributions in that material or in the Appropriate Legal
358 | Notices displayed by works containing it; or
359 |
360 | c) Prohibiting misrepresentation of the origin of that material, or
361 | requiring that modified versions of such material be marked in
362 | reasonable ways as different from the original version; or
363 |
364 | d) Limiting the use for publicity purposes of names of licensors or
365 | authors of the material; or
366 |
367 | e) Declining to grant rights under trademark law for use of some
368 | trade names, trademarks, or service marks; or
369 |
370 | f) Requiring indemnification of licensors and authors of that
371 | material by anyone who conveys the material (or modified versions of
372 | it) with contractual assumptions of liability to the recipient, for
373 | any liability that these contractual assumptions directly impose on
374 | those licensors and authors.
375 |
376 | All other non-permissive additional terms are considered "further
377 | restrictions" within the meaning of section 10. If the Program as you
378 | received it, or any part of it, contains a notice stating that it is
379 | governed by this License along with a term that is a further
380 | restriction, you may remove that term. If a license document contains
381 | a further restriction but permits relicensing or conveying under this
382 | License, you may add to a covered work material governed by the terms
383 | of that license document, provided that the further restriction does
384 | not survive such relicensing or conveying.
385 |
386 | If you add terms to a covered work in accord with this section, you
387 | must place, in the relevant source files, a statement of the
388 | additional terms that apply to those files, or a notice indicating
389 | where to find the applicable terms.
390 |
391 | Additional terms, permissive or non-permissive, may be stated in the
392 | form of a separately written license, or stated as exceptions;
393 | the above requirements apply either way.
394 |
395 | 8. Termination.
396 |
397 | You may not propagate or modify a covered work except as expressly
398 | provided under this License. Any attempt otherwise to propagate or
399 | modify it is void, and will automatically terminate your rights under
400 | this License (including any patent licenses granted under the third
401 | paragraph of section 11).
402 |
403 | However, if you cease all violation of this License, then your
404 | license from a particular copyright holder is reinstated (a)
405 | provisionally, unless and until the copyright holder explicitly and
406 | finally terminates your license, and (b) permanently, if the copyright
407 | holder fails to notify you of the violation by some reasonable means
408 | prior to 60 days after the cessation.
409 |
410 | Moreover, your license from a particular copyright holder is
411 | reinstated permanently if the copyright holder notifies you of the
412 | violation by some reasonable means, this is the first time you have
413 | received notice of violation of this License (for any work) from that
414 | copyright holder, and you cure the violation prior to 30 days after
415 | your receipt of the notice.
416 |
417 | Termination of your rights under this section does not terminate the
418 | licenses of parties who have received copies or rights from you under
419 | this License. If your rights have been terminated and not permanently
420 | reinstated, you do not qualify to receive new licenses for the same
421 | material under section 10.
422 |
423 | 9. Acceptance Not Required for Having Copies.
424 |
425 | You are not required to accept this License in order to receive or
426 | run a copy of the Program. Ancillary propagation of a covered work
427 | occurring solely as a consequence of using peer-to-peer transmission
428 | to receive a copy likewise does not require acceptance. However,
429 | nothing other than this License grants you permission to propagate or
430 | modify any covered work. These actions infringe copyright if you do
431 | not accept this License. Therefore, by modifying or propagating a
432 | covered work, you indicate your acceptance of this License to do so.
433 |
434 | 10. Automatic Licensing of Downstream Recipients.
435 |
436 | Each time you convey a covered work, the recipient automatically
437 | receives a license from the original licensors, to run, modify and
438 | propagate that work, subject to this License. You are not responsible
439 | for enforcing compliance by third parties with this License.
440 |
441 | An "entity transaction" is a transaction transferring control of an
442 | organization, or substantially all assets of one, or subdividing an
443 | organization, or merging organizations. If propagation of a covered
444 | work results from an entity transaction, each party to that
445 | transaction who receives a copy of the work also receives whatever
446 | licenses to the work the party's predecessor in interest had or could
447 | give under the previous paragraph, plus a right to possession of the
448 | Corresponding Source of the work from the predecessor in interest, if
449 | the predecessor has it or can get it with reasonable efforts.
450 |
451 | You may not impose any further restrictions on the exercise of the
452 | rights granted or affirmed under this License. For example, you may
453 | not impose a license fee, royalty, or other charge for exercise of
454 | rights granted under this License, and you may not initiate litigation
455 | (including a cross-claim or counterclaim in a lawsuit) alleging that
456 | any patent claim is infringed by making, using, selling, offering for
457 | sale, or importing the Program or any portion of it.
458 |
459 | 11. Patents.
460 |
461 | A "contributor" is a copyright holder who authorizes use under this
462 | License of the Program or a work on which the Program is based. The
463 | work thus licensed is called the contributor's "contributor version".
464 |
465 | A contributor's "essential patent claims" are all patent claims
466 | owned or controlled by the contributor, whether already acquired or
467 | hereafter acquired, that would be infringed by some manner, permitted
468 | by this License, of making, using, or selling its contributor version,
469 | but do not include claims that would be infringed only as a
470 | consequence of further modification of the contributor version. For
471 | purposes of this definition, "control" includes the right to grant
472 | patent sublicenses in a manner consistent with the requirements of
473 | this License.
474 |
475 | Each contributor grants you a non-exclusive, worldwide, royalty-free
476 | patent license under the contributor's essential patent claims, to
477 | make, use, sell, offer for sale, import and otherwise run, modify and
478 | propagate the contents of its contributor version.
479 |
480 | In the following three paragraphs, a "patent license" is any express
481 | agreement or commitment, however denominated, not to enforce a patent
482 | (such as an express permission to practice a patent or covenant not to
483 | sue for patent infringement). To "grant" such a patent license to a
484 | party means to make such an agreement or commitment not to enforce a
485 | patent against the party.
486 |
487 | If you convey a covered work, knowingly relying on a patent license,
488 | and the Corresponding Source of the work is not available for anyone
489 | to copy, free of charge and under the terms of this License, through a
490 | publicly available network server or other readily accessible means,
491 | then you must either (1) cause the Corresponding Source to be so
492 | available, or (2) arrange to deprive yourself of the benefit of the
493 | patent license for this particular work, or (3) arrange, in a manner
494 | consistent with the requirements of this License, to extend the patent
495 | license to downstream recipients. "Knowingly relying" means you have
496 | actual knowledge that, but for the patent license, your conveying the
497 | covered work in a country, or your recipient's use of the covered work
498 | in a country, would infringe one or more identifiable patents in that
499 | country that you have reason to believe are valid.
500 |
501 | If, pursuant to or in connection with a single transaction or
502 | arrangement, you convey, or propagate by procuring conveyance of, a
503 | covered work, and grant a patent license to some of the parties
504 | receiving the covered work authorizing them to use, propagate, modify
505 | or convey a specific copy of the covered work, then the patent license
506 | you grant is automatically extended to all recipients of the covered
507 | work and works based on it.
508 |
509 | A patent license is "discriminatory" if it does not include within
510 | the scope of its coverage, prohibits the exercise of, or is
511 | conditioned on the non-exercise of one or more of the rights that are
512 | specifically granted under this License. You may not convey a covered
513 | work if you are a party to an arrangement with a third party that is
514 | in the business of distributing software, under which you make payment
515 | to the third party based on the extent of your activity of conveying
516 | the work, and under which the third party grants, to any of the
517 | parties who would receive the covered work from you, a discriminatory
518 | patent license (a) in connection with copies of the covered work
519 | conveyed by you (or copies made from those copies), or (b) primarily
520 | for and in connection with specific products or compilations that
521 | contain the covered work, unless you entered into that arrangement,
522 | or that patent license was granted, prior to 28 March 2007.
523 |
524 | Nothing in this License shall be construed as excluding or limiting
525 | any implied license or other defenses to infringement that may
526 | otherwise be available to you under applicable patent law.
527 |
528 | 12. No Surrender of Others' Freedom.
529 |
530 | If conditions are imposed on you (whether by court order, agreement or
531 | otherwise) that contradict the conditions of this License, they do not
532 | excuse you from the conditions of this License. If you cannot convey a
533 | covered work so as to satisfy simultaneously your obligations under this
534 | License and any other pertinent obligations, then as a consequence you may
535 | not convey it at all. For example, if you agree to terms that obligate you
536 | to collect a royalty for further conveying from those to whom you convey
537 | the Program, the only way you could satisfy both those terms and this
538 | License would be to refrain entirely from conveying the Program.
539 |
540 | 13. Remote Network Interaction; Use with the GNU General Public License.
541 |
542 | Notwithstanding any other provision of this License, if you modify the
543 | Program, your modified version must prominently offer all users
544 | interacting with it remotely through a computer network (if your version
545 | supports such interaction) an opportunity to receive the Corresponding
546 | Source of your version by providing access to the Corresponding Source
547 | from a network server at no charge, through some standard or customary
548 | means of facilitating copying of software. This Corresponding Source
549 | shall include the Corresponding Source for any work covered by version 3
550 | of the GNU General Public License that is incorporated pursuant to the
551 | following paragraph.
552 |
553 | Notwithstanding any other provision of this License, you have
554 | permission to link or combine any covered work with a work licensed
555 | under version 3 of the GNU General Public License into a single
556 | combined work, and to convey the resulting work. The terms of this
557 | License will continue to apply to the part which is the covered work,
558 | but the work with which it is combined will remain governed by version
559 | 3 of the GNU General Public License.
560 |
561 | 14. Revised Versions of this License.
562 |
563 | The Free Software Foundation may publish revised and/or new versions of
564 | the GNU Affero General Public License from time to time. Such new versions
565 | will be similar in spirit to the present version, but may differ in detail to
566 | address new problems or concerns.
567 |
568 | Each version is given a distinguishing version number. If the
569 | Program specifies that a certain numbered version of the GNU Affero General
570 | Public License "or any later version" applies to it, you have the
571 | option of following the terms and conditions either of that numbered
572 | version or of any later version published by the Free Software
573 | Foundation. If the Program does not specify a version number of the
574 | GNU Affero General Public License, you may choose any version ever published
575 | by the Free Software Foundation.
576 |
577 | If the Program specifies that a proxy can decide which future
578 | versions of the GNU Affero General Public License can be used, that proxy's
579 | public statement of acceptance of a version permanently authorizes you
580 | to choose that version for the Program.
581 |
582 | Later license versions may give you additional or different
583 | permissions. However, no additional obligations are imposed on any
584 | author or copyright holder as a result of your choosing to follow a
585 | later version.
586 |
587 | 15. Disclaimer of Warranty.
588 |
589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
597 |
598 | 16. Limitation of Liability.
599 |
600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
608 | SUCH DAMAGES.
609 |
610 | 17. Interpretation of Sections 15 and 16.
611 |
612 | If the disclaimer of warranty and limitation of liability provided
613 | above cannot be given local legal effect according to their terms,
614 | reviewing courts shall apply local law that most closely approximates
615 | an absolute waiver of all civil liability in connection with the
616 | Program, unless a warranty or assumption of liability accompanies a
617 | copy of the Program in return for a fee.
618 |
619 | END OF TERMS AND CONDITIONS
620 |
621 | How to Apply These Terms to Your New Programs
622 |
623 | If you develop a new program, and you want it to be of the greatest
624 | possible use to the public, the best way to achieve this is to make it
625 | free software which everyone can redistribute and change under these terms.
626 |
627 | To do so, attach the following notices to the program. It is safest
628 | to attach them to the start of each source file to most effectively
629 | state the exclusion of warranty; and each file should have at least
630 | the "copyright" line and a pointer to where the full notice is found.
631 |
632 |
633 | Copyright (C)
634 |
635 | This program is free software: you can redistribute it and/or modify
636 | it under the terms of the GNU Affero General Public License as published by
637 | the Free Software Foundation, either version 3 of the License, or
638 | (at your option) any later version.
639 |
640 | This program is distributed in the hope that it will be useful,
641 | but WITHOUT ANY WARRANTY; without even the implied warranty of
642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
643 | GNU Affero General Public License for more details.
644 |
645 | You should have received a copy of the GNU Affero General Public License
646 | along with this program. If not, see .
647 |
648 | Also add information on how to contact you by electronic and paper mail.
649 |
650 | If your software can interact with users remotely through a computer
651 | network, you should also make sure that it provides a way for users to
652 | get its source. For example, if your program is a web application, its
653 | interface could display a "Source" link that leads users to an archive
654 | of the code. There are many ways you could offer source, and different
655 | solutions will be better for different programs; see section 13 for the
656 | specific requirements.
657 |
658 | You should also get your employer (if you work as a programmer) or school,
659 | if any, to sign a "copyright disclaimer" for the program, if necessary.
660 | For more information on this, and how to apply and follow the GNU AGPL, see
661 | .
662 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # wuzz
2 |
3 | Interactive cli tool for HTTP inspection.
4 |
5 | Wuzz command line arguments are similar to cURL's arguments,
6 | so it can be used to inspect/modify requests copied from the
7 | browser's network inspector with the "copy as cURL" feature.
8 |
9 | 
10 |
11 |
12 | ## Installation and usage
13 |
14 | ### GO
15 |
16 | ```
17 | $ go get github.com/asciimoo/wuzz
18 | $ "$GOPATH/bin/wuzz" --help
19 | ```
20 |
21 | Note: golang >= 1.10 required.
22 |
23 | [Binary releases](https://github.com/asciimoo/wuzz/releases) are also available.
24 |
25 | ### Apt
26 |
27 | ```
28 | $ apt install wuzz
29 | ```
30 |
31 | ### Apk
32 |
33 | ```
34 | $ apk add wuzz
35 | ```
36 |
37 | ### Scoop
38 |
39 | ```
40 | $ scoop bucket add main
41 | $ scoop install main/wuzz
42 | ```
43 |
44 | ### X-CMD
45 |
46 | ```
47 | $ x env use wuzz
48 | ```
49 |
50 | ### Nix
51 |
52 | ```
53 | $ nix-shell -p wuzz
54 | ```
55 |
56 |
57 | ### Configuration
58 |
59 | It is possible to override default settings in a configuration file.
60 | The default location is `"$XDG_CONFIG_HOME/wuzz/config.toml"`on linux
61 | and `~/.wuzz/config.toml` on other platforms.
62 | `-c`/`--config` switches can be used to load config file from custom location.
63 |
64 | See [example configuration](sample-config.toml) for more details.
65 |
66 |
67 | ### Commands
68 |
69 | Keybinding | Description
70 | ----------------------------------------|---------------------------------------
71 | F1 | Display help
72 | Ctrl+R | Send request
73 | Ret | Send request (only from URL view)
74 | Ctrl+S | Save response
75 | Ctrl+E | Save request
76 | Ctrl+F | Load request
77 | Ctrl+C | Quit
78 | Ctrl+K, Shift+Tab | Previous view
79 | Ctlr+J, Tab | Next view
80 | Ctlr+T | Toggle context specific search
81 | Alt+H | Toggle history
82 | Down | Move down one view line
83 | Up | Move up one view line
84 | Page down | Move down one view page
85 | Page up | Move up one view page
86 | F2 | Jump to URL
87 | F3 | Jump to query parameters
88 | F4 | Jump to HTTP method
89 | F5 | Jump to request body
90 | F6 | Jump to headers
91 | F7 | Jump to search
92 | F8 | Jump to response headers
93 | F9 | Jump to response body
94 | F11 | Redirects Restriction Mode
95 |
96 |
97 | ### Context specific search
98 |
99 | Wuzz accepts regular expressions by default to filter response body.
100 | Custom query syntax can be toggled by pressing Ctrl+T.
101 | The following formats have context specific search syntax:
102 |
103 | Response format | Query syntax
104 | -----------------|----------------------------------------
105 | HTML | https://github.com/PuerkitoBio/goquery
106 | JSON | https://github.com/tidwall/gjson
107 |
108 |
109 | ## TODO
110 |
111 | * Better navigation
112 | * Autocompletion
113 | * Tests
114 |
115 |
116 | ## Bugs / Suggestions
117 |
118 | Bugs or suggestions? Visit the [issue tracker](https://github.com/asciimoo/wuzz/issues)
119 | or join `#wuzz` on freenode
120 |
--------------------------------------------------------------------------------
/_config.yml:
--------------------------------------------------------------------------------
1 | theme: jekyll-theme-cayman
--------------------------------------------------------------------------------
/commands.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "io/ioutil"
6 | "os"
7 | "os/exec"
8 | "strings"
9 | "unicode"
10 |
11 | "github.com/jroimartin/gocui"
12 | "github.com/nsf/termbox-go"
13 | )
14 |
15 | type CommandFunc func(*gocui.Gui, *gocui.View) error
16 |
17 | var COMMANDS map[string]func(string, *App) CommandFunc = map[string]func(string, *App) CommandFunc{
18 | "submit": func(_ string, a *App) CommandFunc {
19 | return a.SubmitRequest
20 | },
21 | "saveResponse": func(_ string, a *App) CommandFunc {
22 | return func(g *gocui.Gui, _ *gocui.View) error {
23 | return a.OpenSaveDialog(VIEW_TITLES[SAVE_RESPONSE_DIALOG_VIEW], g,
24 | func(g *gocui.Gui, _ *gocui.View) error {
25 | saveLocation := getViewValue(g, SAVE_DIALOG_VIEW)
26 |
27 | if len(a.history) == 0 {
28 | return nil
29 | }
30 | req := a.history[a.historyIndex]
31 | if req.RawResponseBody == nil {
32 | return nil
33 | }
34 |
35 | err := ioutil.WriteFile(saveLocation, req.RawResponseBody, 0644)
36 |
37 | var saveResult string
38 | if err == nil {
39 | saveResult = "Response saved successfully."
40 | } else {
41 | saveResult = "Error saving response: " + err.Error()
42 | }
43 | viewErr := a.OpenSaveResultView(saveResult, g)
44 | return viewErr
45 | })
46 | }
47 | },
48 | "loadRequest": func(_ string, a *App) CommandFunc {
49 | return func(g *gocui.Gui, _ *gocui.View) error {
50 | return a.OpenSaveDialog(VIEW_TITLES[LOAD_REQUEST_DIALOG_VIEW], g,
51 | func(g *gocui.Gui, _ *gocui.View) error {
52 | defer a.closePopup(g, SAVE_DIALOG_VIEW)
53 | loadLocation := getViewValue(g, SAVE_DIALOG_VIEW)
54 | return a.LoadRequest(g, loadLocation)
55 | })
56 | }
57 | },
58 | "saveRequest": func(_ string, a *App) CommandFunc {
59 | return a.SaveRequest
60 | },
61 | "history": func(_ string, a *App) CommandFunc {
62 | return a.ToggleHistory
63 | },
64 | "quit": func(_ string, _ *App) CommandFunc {
65 | return quit
66 | },
67 | "focus": func(args string, a *App) CommandFunc {
68 | return func(g *gocui.Gui, _ *gocui.View) error {
69 | return a.setViewByName(g, args)
70 | }
71 | },
72 | "nextView": func(_ string, a *App) CommandFunc {
73 | return a.NextView
74 | },
75 | "prevView": func(_ string, a *App) CommandFunc {
76 | return a.PrevView
77 | },
78 | "scrollDown": func(_ string, _ *App) CommandFunc {
79 | return scrollViewDown
80 | },
81 | "scrollUp": func(_ string, _ *App) CommandFunc {
82 | return scrollViewUp
83 | },
84 | "pageDown": func(_ string, _ *App) CommandFunc {
85 | return pageDown
86 | },
87 | "pageUp": func(_ string, _ *App) CommandFunc {
88 | return pageUp
89 | },
90 | "deleteLine": func(_ string, _ *App) CommandFunc {
91 | return deleteLine
92 | },
93 | "deleteWord": func(_ string, _ *App) CommandFunc {
94 | return deleteWord
95 | },
96 | "openEditor": func(_ string, a *App) CommandFunc {
97 | return func(g *gocui.Gui, v *gocui.View) error {
98 | return openEditor(g, v, a.config.General.Editor)
99 | }
100 | },
101 | "toggleContextSpecificSearch": func(_ string, a *App) CommandFunc {
102 | return func(g *gocui.Gui, _ *gocui.View) error {
103 | a.config.General.ContextSpecificSearch = !a.config.General.ContextSpecificSearch
104 | a.PrintBody(g)
105 | return nil
106 | }
107 | },
108 | "clearHistory": func(_ string, a *App) CommandFunc {
109 | return func(g *gocui.Gui, _ *gocui.View) error {
110 | a.history = make([]*Request, 0, 31)
111 | a.historyIndex = 0
112 | a.Layout(g)
113 | return nil
114 | }
115 | },
116 | "redirectRestriction": func(_ string, a *App) CommandFunc {
117 | return func(g *gocui.Gui, _ *gocui.View) error {
118 | a.config.General.FollowRedirects = !a.config.General.FollowRedirects
119 | return nil
120 | }
121 | },
122 | }
123 |
124 | func scrollView(v *gocui.View, dy int) error {
125 | v.Autoscroll = false
126 | ox, oy := v.Origin()
127 | if oy+dy < 0 {
128 | dy = -oy
129 | }
130 | if _, err := v.Line(dy); dy > 0 && err != nil {
131 | dy = 0
132 | }
133 | v.SetOrigin(ox, oy+dy)
134 | return nil
135 | }
136 |
137 | func scrollViewUp(_ *gocui.Gui, v *gocui.View) error {
138 | return scrollView(v, -1)
139 | }
140 |
141 | func scrollViewDown(_ *gocui.Gui, v *gocui.View) error {
142 | return scrollView(v, 1)
143 | }
144 |
145 | func pageUp(_ *gocui.Gui, v *gocui.View) error {
146 | _, height := v.Size()
147 | scrollView(v, -height*2/3)
148 | return nil
149 | }
150 |
151 | func pageDown(_ *gocui.Gui, v *gocui.View) error {
152 | _, height := v.Size()
153 | scrollView(v, height*2/3)
154 | return nil
155 | }
156 |
157 | func deleteLine(_ *gocui.Gui, v *gocui.View) error {
158 | if !v.Editable {
159 | return nil
160 | }
161 | _, curY := v.Cursor()
162 | _, oY := v.Origin()
163 | currentLine := curY + oY
164 | viewLines := strings.Split(strings.TrimSpace(v.Buffer()), "\n")
165 | if currentLine >= len(viewLines) {
166 | return nil
167 | }
168 | v.Clear()
169 | if currentLine > 0 {
170 | fmt.Fprintln(v, strings.Join(viewLines[:currentLine], "\n"))
171 | }
172 | fmt.Fprint(v, strings.Join(viewLines[currentLine+1:], "\n"))
173 | v.SetCursor(0, currentLine)
174 | v.SetOrigin(0, oY)
175 | return nil
176 | }
177 |
178 | func deleteWord(_ *gocui.Gui, v *gocui.View) error {
179 | cX, cY := v.Cursor()
180 | oX, _ := v.Origin()
181 | cX = cX - 1 + oX
182 | line, err := v.Line(cY)
183 | if err != nil || line == "" || cX < 0 {
184 | return nil
185 | }
186 | if cX >= len(line) {
187 | cX = len(line) - 1
188 | }
189 | origCharCateg := getCharCategory(rune(line[cX]))
190 | v.EditDelete(true)
191 | cX -= 1
192 | for cX >= 0 {
193 | c := rune(line[cX])
194 | if origCharCateg != getCharCategory(c) {
195 | break
196 | }
197 | v.EditDelete(true)
198 | cX -= 1
199 | }
200 | return nil
201 | }
202 |
203 | func getCharCategory(chr rune) int {
204 | switch {
205 | case unicode.IsDigit(chr):
206 | return 0
207 | case unicode.IsLetter(chr):
208 | return 1
209 | case unicode.IsSpace(chr):
210 | return 2
211 | case unicode.IsPunct(chr):
212 | return 3
213 | }
214 | return int(chr)
215 | }
216 |
217 | func quit(g *gocui.Gui, v *gocui.View) error {
218 | return gocui.ErrQuit
219 | }
220 |
221 | func openEditor(g *gocui.Gui, v *gocui.View, editor string) error {
222 | file, err := ioutil.TempFile(os.TempDir(), "wuzz-")
223 | if err != nil {
224 | return nil
225 | }
226 | defer os.Remove(file.Name())
227 |
228 | val := getViewValue(g, v.Name())
229 | if val != "" {
230 | fmt.Fprint(file, val)
231 | }
232 | file.Close()
233 |
234 | info, err := os.Stat(file.Name())
235 | if err != nil {
236 | return nil
237 | }
238 |
239 | cmd := exec.Command(editor, file.Name())
240 | cmd.Stdout = os.Stdout
241 | cmd.Stdin = os.Stdin
242 | cmd.Stderr = os.Stderr
243 | err = cmd.Run()
244 | // sync termbox to reset console settings
245 | // this is required because the external editor can modify the console
246 | defer g.Update(func(_ *gocui.Gui) error {
247 | termbox.Sync()
248 | return nil
249 | })
250 | if err != nil {
251 | rv, _ := g.View(RESPONSE_BODY_VIEW)
252 | rv.Clear()
253 | fmt.Fprintf(rv, "Editor open error: %v", err)
254 | return nil
255 | }
256 |
257 | newInfo, err := os.Stat(file.Name())
258 | if err != nil || newInfo.ModTime().Before(info.ModTime()) {
259 | return nil
260 | }
261 |
262 | newVal, err := ioutil.ReadFile(file.Name())
263 | if err != nil {
264 | return nil
265 | }
266 |
267 | v.SetCursor(0, 0)
268 | v.SetOrigin(0, 0)
269 | v.Clear()
270 | fmt.Fprint(v, strings.TrimSpace(string(newVal)))
271 |
272 | return nil
273 | }
274 |
--------------------------------------------------------------------------------
/config/config.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "errors"
5 | "os"
6 | "path/filepath"
7 | "runtime"
8 | "time"
9 |
10 | "github.com/BurntSushi/toml"
11 | "github.com/mitchellh/go-homedir"
12 | )
13 |
14 | var ContentTypes = map[string]string{
15 | "json": "application/json",
16 | "form": "application/x-www-form-urlencoded",
17 | "multipart": "multipart/form-data",
18 | }
19 |
20 | // Duration is used to automatically unmarshal timeout strings to
21 | // time.Duration values
22 | type Duration struct {
23 | time.Duration
24 | }
25 |
26 | func (d *Duration) UnmarshalText(text []byte) error {
27 | var err error
28 | d.Duration, err = time.ParseDuration(string(text))
29 | return err
30 | }
31 |
32 | type Config struct {
33 | General GeneralOptions
34 | Keys map[string]map[string]string
35 | }
36 |
37 | type GeneralOptions struct {
38 | ContextSpecificSearch bool
39 | DefaultURLScheme string
40 | Editor string
41 | FollowRedirects bool
42 | FormatJSON bool
43 | Insecure bool
44 | PreserveScrollPosition bool
45 | StatusLine string
46 | TLSVersionMax uint16
47 | TLSVersionMin uint16
48 | Timeout Duration
49 | }
50 |
51 | var defaultTimeoutDuration, _ = time.ParseDuration("1m")
52 |
53 | var DefaultKeys = map[string]map[string]string{
54 | "global": {
55 | "CtrlR": "submit",
56 | "CtrlC": "quit",
57 | "CtrlS": "saveResponse",
58 | "CtrlF": "loadRequest",
59 | "CtrlE": "saveRequest",
60 | "CtrlD": "deleteLine",
61 | "CtrlW": "deleteWord",
62 | "CtrlO": "openEditor",
63 | "CtrlT": "toggleContextSpecificSearch",
64 | "CtrlX": "clearHistory",
65 | "Tab": "nextView",
66 | "CtrlJ": "nextView",
67 | "CtrlK": "prevView",
68 | "AltH": "history",
69 | "F2": "focus url",
70 | "F3": "focus get",
71 | "F4": "focus method",
72 | "F5": "focus data",
73 | "F6": "focus headers",
74 | "F7": "focus search",
75 | "F8": "focus response-headers",
76 | "F9": "focus response-body",
77 | "F11": "redirectRestriction",
78 | },
79 | "url": {
80 | "Enter": "submit",
81 | },
82 | "response-headers": {
83 | "ArrowUp": "scrollUp",
84 | "ArrowDown": "scrollDown",
85 | "PageUp": "pageUp",
86 | "PageDown": "pageDown",
87 | },
88 | "response-body": {
89 | "ArrowUp": "scrollUp",
90 | "ArrowDown": "scrollDown",
91 | "PageUp": "pageUp",
92 | "PageDown": "pageDown",
93 | },
94 | "help": {
95 | "ArrowUp": "scrollUp",
96 | "ArrowDown": "scrollDown",
97 | "PageUp": "pageUp",
98 | "PageDown": "pageDown",
99 | },
100 | }
101 |
102 | var DefaultConfig = Config{
103 | General: GeneralOptions{
104 | DefaultURLScheme: "https",
105 | Editor: "vim",
106 | FollowRedirects: true,
107 | FormatJSON: true,
108 | Insecure: false,
109 | PreserveScrollPosition: true,
110 | StatusLine: "[wuzz {{.Version}}]{{if .Duration}} [Response time: {{.Duration}}]{{end}} [Request no.: {{.RequestNumber}}/{{.HistorySize}}] [Search type: {{.SearchType}}]{{if .DisableRedirect}} [Redirects Restricted Mode {{.DisableRedirect}}]{{end}}",
111 | Timeout: Duration{
112 | defaultTimeoutDuration,
113 | },
114 | },
115 | }
116 |
117 | func init() {
118 | if os.Getenv("EDITOR") != "" {
119 | DefaultConfig.General.Editor = os.Getenv("EDITOR")
120 | }
121 | }
122 |
123 | func LoadConfig(configFile string) (*Config, error) {
124 | if _, err := os.Stat(configFile); os.IsNotExist(err) {
125 | return nil, errors.New("Config file does not exist.")
126 | } else if err != nil {
127 | return nil, err
128 | }
129 |
130 | conf := DefaultConfig
131 | if _, err := toml.DecodeFile(configFile, &conf); err != nil {
132 | return nil, err
133 | }
134 |
135 | if conf.Keys == nil {
136 | conf.Keys = DefaultKeys
137 | } else {
138 | // copy default keys
139 | for keyCategory, keys := range DefaultKeys {
140 | confKeys, found := conf.Keys[keyCategory]
141 | if found {
142 | for key, action := range keys {
143 | if _, found := confKeys[key]; !found {
144 | conf.Keys[keyCategory][key] = action
145 | }
146 | }
147 | } else {
148 | conf.Keys[keyCategory] = keys
149 | }
150 | }
151 | }
152 |
153 | return &conf, nil
154 | }
155 |
156 | func GetDefaultConfigLocation() string {
157 | var configFolderLocation string
158 | switch runtime.GOOS {
159 | case "linux":
160 | // Use the XDG_CONFIG_HOME variable if it is set, otherwise
161 | // $HOME/.config/wuzz/config.toml
162 | xdgConfigHome := os.Getenv("XDG_CONFIG_HOME")
163 | if xdgConfigHome != "" {
164 | configFolderLocation = xdgConfigHome
165 | } else {
166 | configFolderLocation, _ = homedir.Expand("~/.config/wuzz/")
167 | }
168 |
169 | default:
170 | // On other platforms we just use $HOME/.wuzz
171 | configFolderLocation, _ = homedir.Expand("~/.wuzz/")
172 | }
173 |
174 | return filepath.Join(configFolderLocation, "config.toml")
175 | }
176 |
--------------------------------------------------------------------------------
/docker-entrypoint.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | set -e
3 |
4 | if [ "${1#-}" != "${1}" ] || [ -z "$(command -v "${1}")" ]; then
5 | sleep 0.01
6 | set -- wuzz "$@"
7 | fi
8 |
9 | exec "$@"
--------------------------------------------------------------------------------
/docs/images/screencast.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/asciimoo/wuzz/9ae9b524f225f085a152195aac0c6322f289d966/docs/images/screencast.gif
--------------------------------------------------------------------------------
/formatter/binary.go:
--------------------------------------------------------------------------------
1 | package formatter
2 |
3 | import (
4 | "encoding/hex"
5 | "errors"
6 | "fmt"
7 | "io"
8 | )
9 |
10 | type binaryFormatter struct {
11 | }
12 |
13 | func (f *binaryFormatter) Format(writer io.Writer, data []byte) error {
14 | fmt.Fprint(writer, hex.Dump(data))
15 | return nil
16 | }
17 |
18 | func (f *binaryFormatter) Title() string {
19 | return "[binary]"
20 | }
21 |
22 | func (f *binaryFormatter) Searchable() bool {
23 | return false
24 | }
25 |
26 | func (f *binaryFormatter) Search(q string, body []byte) ([]string, error) {
27 | return nil, errors.New("Cannot perform search on binary content type")
28 | }
29 |
--------------------------------------------------------------------------------
/formatter/formatter.go:
--------------------------------------------------------------------------------
1 | package formatter
2 |
3 | import (
4 | "io"
5 | "mime"
6 | "strings"
7 |
8 | "github.com/asciimoo/wuzz/config"
9 | )
10 |
11 | type ResponseFormatter interface {
12 | Format(writer io.Writer, data []byte) error
13 | Title() string
14 | Searchable() bool
15 | Search(string, []byte) ([]string, error)
16 | }
17 |
18 | func New(appConfig *config.Config, contentType string) ResponseFormatter {
19 | ctype, _, err := mime.ParseMediaType(contentType)
20 | if err == nil && appConfig.General.FormatJSON && (ctype == config.ContentTypes["json"] || strings.HasSuffix(ctype, "+json")) {
21 | return &jsonFormatter{}
22 | } else if strings.Contains(contentType, "text/html") {
23 | return &htmlFormatter{}
24 | } else if strings.Index(contentType, "text") == -1 && strings.Index(contentType, "application") == -1 {
25 | return &binaryFormatter{}
26 | } else {
27 | return &TextFormatter{}
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/formatter/formatter_test.go:
--------------------------------------------------------------------------------
1 | package formatter
2 |
3 | import (
4 | "bytes"
5 | "testing"
6 |
7 | "github.com/asciimoo/wuzz/config"
8 | "github.com/nwidger/jsoncolor"
9 | "github.com/x86kernel/htmlcolor"
10 | )
11 |
12 | func TestFormat(t *testing.T) {
13 | var binBuffer bytes.Buffer
14 | New(configFixture(true), "octet-stream").Format(&binBuffer, []byte("some binary data"))
15 | if binBuffer.String() != "00000000 73 6f 6d 65 20 62 69 6e 61 72 79 20 64 61 74 61 |some binary data|\n" {
16 | t.Error("Expected binary to eq " + binBuffer.String())
17 | }
18 |
19 | var htmlBuffer bytes.Buffer
20 | New(configFixture(true), "text/html; charset=utf-8").Format(&htmlBuffer, []byte("unfomatted"))
21 | var htmltargetBuffer bytes.Buffer
22 | htmlcolor.NewFormatter().Format(&htmltargetBuffer, []byte("unfomatted"))
23 | htmltarget := htmltargetBuffer.String()
24 |
25 | if htmlBuffer.String() != htmltarget {
26 | t.Error("Expected html to eq " + htmlBuffer.String())
27 | }
28 |
29 | var jsonEnabledBuffer bytes.Buffer
30 | New(configFixture(true), "application/json; charset=utf-8").Format(&jsonEnabledBuffer, []byte("{\"json\": \"some value\"}"))
31 | var jsontargetBuffer bytes.Buffer
32 | f := jsoncolor.NewFormatter()
33 | f.Indent = " "
34 | f.Format(&jsontargetBuffer, []byte("{\"json\": \"some value\"}"))
35 | jsontarget := jsontargetBuffer.String()
36 |
37 | if jsonEnabledBuffer.String() != jsontarget {
38 | t.Error("Expected json to eq \n" + jsonEnabledBuffer.String() + "\nbut not\n" + jsontarget)
39 | }
40 |
41 | var jsonDisabledBuffer bytes.Buffer
42 | New(configFixture(false), "application/json; charset=utf-8").Format(&jsonDisabledBuffer, []byte("{\"json\": \"some value\"}"))
43 | if jsonDisabledBuffer.String() != "{\"json\": \"some value\"}" {
44 | t.Error("Expected json to eq " + jsonDisabledBuffer.String())
45 | }
46 |
47 | var textBuffer bytes.Buffer
48 | New(configFixture(true), "text/html; charset=utf-8").Format(&textBuffer, []byte("some text"))
49 | if textBuffer.String() != "some text" {
50 | t.Error("Expected text to eq " + textBuffer.String())
51 | }
52 | }
53 |
54 | func TestTitle(t *testing.T) {
55 | //binary
56 | title := New(configFixture(true), "octet-stream").Title()
57 | if title != "[binary]" {
58 | t.Error("for octet-stream content type expected title ", title, "to be [binary]")
59 | }
60 |
61 | //html
62 | title = New(configFixture(true), "text/html; charset=utf-8").Title()
63 | if title != "[html]" {
64 | t.Error("For text/html content type expected title ", title, " to be [html]")
65 | }
66 |
67 | //json
68 | title = New(configFixture(true), "application/json; charset=utf-8").Title()
69 | if title != "[json]" {
70 | t.Error("For text/html content type expected title ", title, " to be [json]")
71 | }
72 |
73 | //text
74 | title = New(configFixture(true), "text/plain; charset=utf-8").Title()
75 | if title != "[text]" {
76 | t.Error("For text/html content type expected title ", title, " to be [text]")
77 | }
78 | }
79 |
80 | func TestSearchable(t *testing.T) {
81 | if New(configFixture(true), "octet-stream").Searchable() {
82 | t.Error("binary file can't be searchable")
83 | }
84 |
85 | if !New(configFixture(true), "text/html").Searchable() {
86 | t.Error("text/html should be searchable")
87 | }
88 |
89 | if !New(configFixture(true), "application/json").Searchable() {
90 | t.Error("application/json should be searchable")
91 | }
92 | if !New(configFixture(true), "text/plain").Searchable() {
93 | t.Error("text/plain should be searchable")
94 | }
95 |
96 | }
97 |
98 | func configFixture(jsonEnabled bool) *config.Config {
99 | return &config.Config{
100 | General: config.GeneralOptions{
101 | FormatJSON: jsonEnabled,
102 | },
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/formatter/html.go:
--------------------------------------------------------------------------------
1 | package formatter
2 |
3 | import (
4 | "bytes"
5 | "errors"
6 | "io"
7 |
8 | "github.com/PuerkitoBio/goquery"
9 | "github.com/x86kernel/htmlcolor"
10 | )
11 |
12 | type htmlFormatter struct {
13 | parsedBody goquery.Document
14 | TextFormatter
15 | }
16 |
17 | func (f *htmlFormatter) Format(writer io.Writer, data []byte) error {
18 | htmlFormatter := htmlcolor.NewFormatter()
19 | buf := bytes.NewBuffer(make([]byte, 0, len(data)))
20 | err := htmlFormatter.Format(buf, data)
21 |
22 | if err == io.EOF {
23 | writer.Write(buf.Bytes())
24 | return nil
25 | }
26 |
27 | return errors.New("html formatter error")
28 | }
29 |
30 | func (f *htmlFormatter) Title() string {
31 | return "[html]"
32 | }
33 |
34 | func (f *htmlFormatter) Search(q string, body []byte) ([]string, error) {
35 | if q == "" {
36 | buf := bytes.NewBuffer(make([]byte, 0, len(body)))
37 | err := f.Format(buf, body)
38 | return []string{buf.String()}, err
39 | }
40 | doc, err := goquery.NewDocumentFromReader(bytes.NewBuffer(body))
41 | if err != nil {
42 | return nil, err
43 | }
44 |
45 | results := make([]string, 0, 8)
46 | doc.Find(q).Each(func(_ int, s *goquery.Selection) {
47 | htmlResult, err := goquery.OuterHtml(s)
48 | if err == nil {
49 | results = append(results, htmlResult)
50 | }
51 | })
52 |
53 | return results, nil
54 | }
55 |
--------------------------------------------------------------------------------
/formatter/json.go:
--------------------------------------------------------------------------------
1 | package formatter
2 |
3 | import (
4 | "bytes"
5 | "errors"
6 | "io"
7 |
8 | "github.com/nwidger/jsoncolor"
9 | "github.com/tidwall/gjson"
10 | )
11 |
12 | type jsonFormatter struct {
13 | parsedBody gjson.Result
14 | TextFormatter
15 | }
16 |
17 | func (f *jsonFormatter) Format(writer io.Writer, data []byte) error {
18 | jsonFormatter := jsoncolor.NewFormatter()
19 | jsonFormatter.Indent = " "
20 | buf := bytes.NewBuffer(make([]byte, 0, len(data)))
21 | err := jsonFormatter.Format(buf, data)
22 | if err == nil {
23 | writer.Write(buf.Bytes())
24 | return nil
25 | }
26 | return errors.New("json formatter error")
27 | }
28 |
29 | func (f *jsonFormatter) Title() string {
30 | return "[json]"
31 | }
32 |
33 | func (f *jsonFormatter) Search(q string, body []byte) ([]string, error) {
34 | if q != "" {
35 | if f.parsedBody.Type != gjson.JSON {
36 | f.parsedBody = gjson.ParseBytes(body)
37 | }
38 | searchResult := f.parsedBody.Get(q)
39 | if searchResult.Type == gjson.Null {
40 | return nil, errors.New("Invalid gjson query or no results found")
41 | }
42 | if searchResult.Type != gjson.JSON {
43 | return []string{searchResult.String()}, nil
44 | }
45 | body = []byte(searchResult.String())
46 | }
47 | jsonFormatter := jsoncolor.NewFormatter()
48 | jsonFormatter.Indent = " "
49 | buf := bytes.NewBuffer(make([]byte, 0, len(body)))
50 | err := jsonFormatter.Format(buf, body)
51 | if err != nil {
52 | return nil, errors.New("Invalid results")
53 | }
54 | return []string{string(buf.Bytes())}, nil
55 | }
56 |
--------------------------------------------------------------------------------
/formatter/text.go:
--------------------------------------------------------------------------------
1 | package formatter
2 |
3 | import (
4 | "io"
5 | "regexp"
6 | )
7 |
8 | type TextFormatter struct {
9 | }
10 |
11 | func (f *TextFormatter) Format(writer io.Writer, data []byte) error {
12 | _, err := writer.Write(data)
13 | return err
14 | }
15 |
16 | func (f *TextFormatter) Title() string {
17 | return "[text]"
18 | }
19 |
20 | func (f *TextFormatter) Searchable() bool {
21 | return true
22 | }
23 |
24 | func (f *TextFormatter) Search(q string, body []byte) ([]string, error) {
25 | search_re, err := regexp.Compile(q)
26 | if err != nil {
27 | return nil, err
28 | }
29 | ret := make([]string, 0, 16)
30 | for _, match := range search_re.FindAll(body, 1000) {
31 | ret = append(ret, string(match))
32 | }
33 | return ret, nil
34 | }
35 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/asciimoo/wuzz
2 |
3 | go 1.14
4 |
5 | require (
6 | github.com/BurntSushi/toml v0.3.1
7 | github.com/PuerkitoBio/goquery v1.5.1
8 | github.com/alessio/shellescape v1.2.2
9 | github.com/andybalholm/cascadia v1.2.0 // indirect
10 | github.com/jroimartin/gocui v0.4.0
11 | github.com/mattn/go-colorable v0.1.6 // indirect
12 | github.com/mattn/go-runewidth v0.0.9
13 | github.com/mitchellh/go-homedir v1.1.0
14 | github.com/nsf/termbox-go v0.0.0-20200418040025-38ba6e5628f1
15 | github.com/nwidger/jsoncolor v0.3.0
16 | github.com/stretchr/testify v1.10.0 // indirect
17 | github.com/tidwall/gjson v1.6.0
18 | github.com/tidwall/pretty v1.0.1 // indirect
19 | github.com/x86kernel/htmlcolor v0.0.0-20190529101448-c589f58466d0
20 | golang.org/x/net v0.0.0-20200602114024-627f9648deb9
21 | golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980 // indirect
22 | )
23 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
3 | github.com/PuerkitoBio/goquery v1.5.1 h1:PSPBGne8NIUWw+/7vFBV+kG2J/5MOjbzc7154OaKCSE=
4 | github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
5 | github.com/alessio/shellescape v1.2.2 h1:8LnL+ncxhWT2TR00dfJRT25JWWrhkMZXneHVWnetDZg=
6 | github.com/alessio/shellescape v1.2.2/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30=
7 | github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
8 | github.com/andybalholm/cascadia v1.2.0 h1:vuRCkM5Ozh/BfmsaTm26kbjm0mIOM3yS5Ek/F5h18aE=
9 | github.com/andybalholm/cascadia v1.2.0/go.mod h1:YCyR8vOZT9aZ1CHEd8ap0gMVm2aFgxBp0T0eFw1RUQY=
10 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
11 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
12 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
13 | github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
14 | github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
15 | github.com/jroimartin/gocui v0.4.0 h1:52jnalstgmc25FmtGcWqa0tcbMEWS6RpFLsOIO+I+E8=
16 | github.com/jroimartin/gocui v0.4.0/go.mod h1:7i7bbj99OgFHzo7kB2zPb8pXLqMBSQegY7azfqXMkyY=
17 | github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
18 | github.com/mattn/go-colorable v0.1.6 h1:6Su7aK7lXmJ/U79bYtBjLNaha4Fs1Rg9plHpcH+vvnE=
19 | github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
20 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
21 | github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
22 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
23 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
24 | github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
25 | github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
26 | github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
27 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
28 | github.com/nsf/termbox-go v0.0.0-20200418040025-38ba6e5628f1 h1:lh3PyZvY+B9nFliSGTn5uFuqQQJGuNrD0MLCokv09ag=
29 | github.com/nsf/termbox-go v0.0.0-20200418040025-38ba6e5628f1/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ=
30 | github.com/nwidger/jsoncolor v0.3.0 h1:VdTH8Dc0SJoq4pJ8pRxxFZW0/5Ng5akbN4YToCBJDSU=
31 | github.com/nwidger/jsoncolor v0.3.0/go.mod h1:Cs34umxLbJvgBMnVNVqhji9BhoT/N/KinHqZptQ7cf4=
32 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
33 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
34 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
35 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
36 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
37 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
38 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
39 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
40 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
41 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
42 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
43 | github.com/tidwall/gjson v1.6.0 h1:9VEQWz6LLMUsUl6PueE49ir4Ka6CzLymOAZDxpFsTDc=
44 | github.com/tidwall/gjson v1.6.0/go.mod h1:P256ACg0Mn+j1RXIDXoss50DeIABTYK1PULOJHhxOls=
45 | github.com/tidwall/match v1.0.1 h1:PnKP62LPNxHKTwvHHZZzdOAOCtsJTjo6dZLCwpKm5xc=
46 | github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E=
47 | github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
48 | github.com/tidwall/pretty v1.0.1 h1:WE4RBSZ1x6McVVC8S/Md+Qse8YUv6HRObAx6ke00NY8=
49 | github.com/tidwall/pretty v1.0.1/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
50 | github.com/x86kernel/htmlcolor v0.0.0-20190529101448-c589f58466d0 h1:eViiK7U+LXJuAEcnOdp+5jIDp7j9iE2FE8YfWoLExTE=
51 | github.com/x86kernel/htmlcolor v0.0.0-20190529101448-c589f58466d0/go.mod h1:pUZuomyrQzbA0SQPSwAnDB3TgChnUMfZnSSfcAzpVh8=
52 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
53 | golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
54 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
55 | golang.org/x/net v0.0.0-20200602114024-627f9648deb9 h1:pNX+40auqi2JqRfOP1akLGtYcn15TUbkhwuCO3foqqM=
56 | golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
57 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
58 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
59 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
60 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
61 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
62 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
63 | golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980 h1:OjiUf46hAmXblsZdnoSXsEUSKU8r1UEzcL5RVZ4gO9Y=
64 | golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
65 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
66 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
67 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
68 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
69 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
70 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
71 |
--------------------------------------------------------------------------------
/keys.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/jroimartin/gocui"
5 | )
6 |
7 | var KEYS = map[string]gocui.Key{
8 | "F1": gocui.KeyF1,
9 | "F2": gocui.KeyF2,
10 | "F3": gocui.KeyF3,
11 | "F4": gocui.KeyF4,
12 | "F5": gocui.KeyF5,
13 | "F6": gocui.KeyF6,
14 | "F7": gocui.KeyF7,
15 | "F8": gocui.KeyF8,
16 | "F9": gocui.KeyF9,
17 | "F10": gocui.KeyF10,
18 | "F11": gocui.KeyF11,
19 | "F12": gocui.KeyF12,
20 | "Insert": gocui.KeyInsert,
21 | "Delete": gocui.KeyDelete,
22 | "Home": gocui.KeyHome,
23 | "End": gocui.KeyEnd,
24 | "PageUp": gocui.KeyPgup,
25 | "PageDown": gocui.KeyPgdn,
26 | "ArrowUp": gocui.KeyArrowUp,
27 | "ArrowDown": gocui.KeyArrowDown,
28 | "ArrowLeft": gocui.KeyArrowLeft,
29 | "ArrowRight": gocui.KeyArrowRight,
30 | "CtrlTilde": gocui.KeyCtrlTilde,
31 | "Ctrl2": gocui.KeyCtrl2,
32 | "CtrlSpace": gocui.KeyCtrlSpace,
33 | "CtrlA": gocui.KeyCtrlA,
34 | "CtrlB": gocui.KeyCtrlB,
35 | "CtrlC": gocui.KeyCtrlC,
36 | "CtrlD": gocui.KeyCtrlD,
37 | "CtrlE": gocui.KeyCtrlE,
38 | "CtrlF": gocui.KeyCtrlF,
39 | "CtrlG": gocui.KeyCtrlG,
40 | "Backspace": gocui.KeyBackspace,
41 | "CtrlH": gocui.KeyCtrlH,
42 | "Tab": gocui.KeyTab,
43 | "CtrlI": gocui.KeyCtrlI,
44 | "CtrlJ": gocui.KeyCtrlJ,
45 | "CtrlK": gocui.KeyCtrlK,
46 | "CtrlL": gocui.KeyCtrlL,
47 | "Enter": gocui.KeyEnter,
48 | "CtrlM": gocui.KeyCtrlM,
49 | "CtrlN": gocui.KeyCtrlN,
50 | "CtrlO": gocui.KeyCtrlO,
51 | "CtrlP": gocui.KeyCtrlP,
52 | "CtrlQ": gocui.KeyCtrlQ,
53 | "CtrlR": gocui.KeyCtrlR,
54 | "CtrlS": gocui.KeyCtrlS,
55 | "CtrlT": gocui.KeyCtrlT,
56 | "CtrlU": gocui.KeyCtrlU,
57 | "CtrlV": gocui.KeyCtrlV,
58 | "CtrlW": gocui.KeyCtrlW,
59 | "CtrlX": gocui.KeyCtrlX,
60 | "CtrlY": gocui.KeyCtrlY,
61 | "CtrlZ": gocui.KeyCtrlZ,
62 | "Esc": gocui.KeyEsc,
63 | "CtrlLsqBracket": gocui.KeyCtrlLsqBracket,
64 | "Ctrl3": gocui.KeyCtrl3,
65 | "Ctrl4": gocui.KeyCtrl4,
66 | "CtrlBackslash": gocui.KeyCtrlBackslash,
67 | "Ctrl5": gocui.KeyCtrl5,
68 | "CtrlRsqBracket": gocui.KeyCtrlRsqBracket,
69 | "Ctrl6": gocui.KeyCtrl6,
70 | "Ctrl7": gocui.KeyCtrl7,
71 | "CtrlSlash": gocui.KeyCtrlSlash,
72 | "CtrlUnderscore": gocui.KeyCtrlUnderscore,
73 | "Space": gocui.KeySpace,
74 | "Backspace2": gocui.KeyBackspace2,
75 | "Ctrl8": gocui.KeyCtrl8,
76 | }
77 |
--------------------------------------------------------------------------------
/request-headers.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | var REQUEST_HEADERS = []string{
4 | "Accept",
5 | "Accept-Charset",
6 | "Accept-Encoding",
7 | "Accept-Language",
8 | "Accept-Datetime",
9 | "Authorization",
10 | "Cache-Control",
11 | "Connection",
12 | "Cookie",
13 | "Content-Length",
14 | "Content-MD5",
15 | "Content-Type",
16 | "Date",
17 | "Expect",
18 | "Forwarded",
19 | "From",
20 | "Host",
21 | "If-Match",
22 | "If-Modified-Since",
23 | "If-None-Match",
24 | "If-Range",
25 | "If-Unmodified-Since",
26 | "Max-Forwards",
27 | "Origin",
28 | "Pragma",
29 | "Proxy-Authorization",
30 | "Range",
31 | "Referer",
32 | "TE",
33 | "User-Agent",
34 | "Upgrade",
35 | "Via",
36 | "Warning",
37 | }
38 |
--------------------------------------------------------------------------------
/sample-config.toml:
--------------------------------------------------------------------------------
1 | [general]
2 | timeout = "1m"
3 | formatJSON = true
4 | insecure = false
5 | preserveScrollPosition = true
6 | followRedirects = true
7 | defaultURLScheme = "https"
8 | statusLine = "[wuzz {{.Version}}] [Response time: {{.Duration}}]"
9 | editor = "vim"
10 |
11 | # KEYBINDINGS
12 | [keys.global]
13 | CtrlR = "submit"
14 | CtrlC = "quit"
15 | CtrlS = "saveResponse"
16 | CtrlD = "deleteLine"
17 | CtrlW = "deleteWord"
18 | CtrlF = "loadRequest"
19 | CtrlE = "saveRequest"
20 | CtrlT = "toggleContextSpecificSearch"
21 | CtrlX = "clearHistory"
22 | Tab = "nextView"
23 | CtrlJ = "nextView"
24 | CtrlK = "prevView"
25 | AltH = "history"
26 | F2 = "focus url"
27 | F3 = "focus get"
28 | F4 = "focus method"
29 | F5 = "focus data"
30 | F6 = "focus headers"
31 | F7 = "focus search"
32 | F8 = "focus response-headers"
33 | F9 = "focus response-body"
34 | F11 = "redirects restriction mode"
35 |
36 | [keys.url]
37 | Enter = "submit"
38 |
39 | [keys.response-headers]
40 | ArrowUp = "scrollUp"
41 | ArrowDown = "scrollDown"
42 | PageUp = "pageUp"
43 | PageDown = "pageDown"
44 |
45 | [keys.response-body]
46 | ArrowUp = "scrollUp"
47 | ArrowDown = "scrollDown"
48 | PageUp = "pageUp"
49 | PageDown = "pageDown"
50 |
51 | [keys.help]
52 | ArrowUp = "scrollUp"
53 | ArrowDown = "scrollDown"
54 | PageUp = "pageUp"
55 | PageDown = "pageDown"
56 |
--------------------------------------------------------------------------------
/status-line.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "strconv"
6 | "text/template"
7 |
8 | "github.com/jroimartin/gocui"
9 | )
10 |
11 | type StatusLine struct {
12 | tpl *template.Template
13 | }
14 |
15 | type StatusLineFunctions struct {
16 | app *App
17 | }
18 |
19 | func (_ *StatusLineFunctions) Version() string {
20 | return VERSION
21 | }
22 |
23 | func (s *StatusLineFunctions) Duration() string {
24 | if len(s.app.history) == 0 {
25 | return ""
26 | }
27 | return s.app.history[s.app.historyIndex].Duration.String()
28 | }
29 |
30 | func (s *StatusLineFunctions) HistorySize() string {
31 | return strconv.Itoa(len(s.app.history))
32 | }
33 |
34 | func (s *StatusLineFunctions) RequestNumber() string {
35 | i := s.app.historyIndex
36 | if len(s.app.history) > 0 {
37 | i += 1
38 | }
39 | return strconv.Itoa(i)
40 | }
41 |
42 | func (s *StatusLineFunctions) SearchType() string {
43 | if len(s.app.history) > 0 && !s.app.history[s.app.historyIndex].Formatter.Searchable() {
44 | return "none"
45 | }
46 | if s.app.config.General.ContextSpecificSearch {
47 | return "response specific"
48 | }
49 | return "regex"
50 | }
51 |
52 | func (s *StatusLine) Update(v *gocui.View, a *App) {
53 | v.Clear()
54 | err := s.tpl.Execute(v, &StatusLineFunctions{app: a})
55 | if err != nil {
56 | fmt.Fprintf(v, "StatusLine update error: %v", err)
57 | }
58 | }
59 |
60 | func (s *StatusLineFunctions) DisableRedirect() string {
61 | if s.app.config.General.FollowRedirects {
62 | return ""
63 | }
64 | return "Activated"
65 | }
66 |
67 | func NewStatusLine(format string) (*StatusLine, error) {
68 | tpl, err := template.New("status line").Parse(format)
69 | if err != nil {
70 | return nil, err
71 | }
72 | return &StatusLine{
73 | tpl: tpl,
74 | }, nil
75 | }
76 |
--------------------------------------------------------------------------------
/wuzz.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "compress/gzip"
6 | "crypto/tls"
7 | "encoding/json"
8 | "errors"
9 | "fmt"
10 | "io"
11 | "io/ioutil"
12 | "log"
13 | "mime/multipart"
14 | "net/http"
15 | "net/url"
16 | "os"
17 | "path"
18 | "regexp"
19 | "runtime"
20 | "sort"
21 | "strconv"
22 | "strings"
23 | "time"
24 |
25 | "golang.org/x/net/proxy"
26 |
27 | "github.com/asciimoo/wuzz/config"
28 | "github.com/asciimoo/wuzz/formatter"
29 |
30 | "github.com/alessio/shellescape"
31 | "github.com/jroimartin/gocui"
32 | "github.com/mattn/go-runewidth"
33 | "github.com/nsf/termbox-go"
34 | )
35 |
36 | const VERSION = "0.5.0"
37 |
38 | const TIMEOUT_DURATION = 5 // in seconds
39 | const WINDOWS_OS = "windows"
40 | const SEARCH_PROMPT = "search> "
41 |
42 | const (
43 | ALL_VIEWS = ""
44 |
45 | URL_VIEW = "url"
46 | URL_PARAMS_VIEW = "get"
47 | REQUEST_METHOD_VIEW = "method"
48 | REQUEST_DATA_VIEW = "data"
49 | REQUEST_HEADERS_VIEW = "headers"
50 | STATUSLINE_VIEW = "status-line"
51 | SEARCH_VIEW = "search"
52 | RESPONSE_HEADERS_VIEW = "response-headers"
53 | RESPONSE_BODY_VIEW = "response-body"
54 |
55 | SEARCH_PROMPT_VIEW = "prompt"
56 | POPUP_VIEW = "popup_view"
57 | AUTOCOMPLETE_VIEW = "autocomplete_view"
58 | ERROR_VIEW = "error_view"
59 | HISTORY_VIEW = "history"
60 | SAVE_DIALOG_VIEW = "save-dialog"
61 | SAVE_RESPONSE_DIALOG_VIEW = "save-response-dialog"
62 | LOAD_REQUEST_DIALOG_VIEW = "load-request-dialog"
63 | SAVE_REQUEST_FORMAT_DIALOG_VIEW = "save-request-format-dialog"
64 | SAVE_REQUEST_DIALOG_VIEW = "save-request-dialog"
65 | SAVE_RESULT_VIEW = "save-result"
66 | METHOD_LIST_VIEW = "method-list"
67 | HELP_VIEW = "help"
68 | )
69 |
70 | var VIEW_TITLES = map[string]string{
71 | POPUP_VIEW: "Info",
72 | ERROR_VIEW: "Error",
73 | HISTORY_VIEW: "History",
74 | SAVE_RESPONSE_DIALOG_VIEW: "Save Response (enter to submit, ctrl+q to cancel)",
75 | LOAD_REQUEST_DIALOG_VIEW: "Load Request (enter to submit, ctrl+q to cancel)",
76 | SAVE_REQUEST_DIALOG_VIEW: "Save Request (enter to submit, ctrl+q to cancel)",
77 | SAVE_REQUEST_FORMAT_DIALOG_VIEW: "Choose export format",
78 | SAVE_RESULT_VIEW: "Save Result (press enter to close)",
79 | METHOD_LIST_VIEW: "Methods",
80 | HELP_VIEW: "Help",
81 | }
82 |
83 | type position struct {
84 | // value = prc * MAX + abs
85 | pct float32
86 | abs int
87 | }
88 |
89 | type viewPosition struct {
90 | x0, y0, x1, y1 position
91 | }
92 |
93 | var VIEW_POSITIONS = map[string]viewPosition{
94 | URL_VIEW: {
95 | position{0.0, 0},
96 | position{0.0, 0},
97 | position{1.0, -2},
98 | position{0.0, 3}},
99 | URL_PARAMS_VIEW: {
100 | position{0.0, 0},
101 | position{0.0, 3},
102 | position{0.3, 0},
103 | position{0.25, 0}},
104 | REQUEST_METHOD_VIEW: {
105 | position{0.0, 0},
106 | position{0.25, 0},
107 | position{0.3, 0},
108 | position{0.25, 2}},
109 | REQUEST_DATA_VIEW: {
110 | position{0.0, 0},
111 | position{0.25, 2},
112 | position{0.3, 0},
113 | position{0.5, 1}},
114 | REQUEST_HEADERS_VIEW: {
115 | position{0.0, 0},
116 | position{0.5, 1},
117 | position{0.3, 0},
118 | position{1.0, -3}},
119 | RESPONSE_HEADERS_VIEW: {
120 | position{0.3, 0},
121 | position{0.0, 3},
122 | position{1.0, -2},
123 | position{0.25, 2}},
124 | RESPONSE_BODY_VIEW: {
125 | position{0.3, 0},
126 | position{0.25, 2},
127 | position{1.0, -2},
128 | position{1.0, -3}},
129 | STATUSLINE_VIEW: {
130 | position{0.0, -1},
131 | position{1.0, -4},
132 | position{1.0, 0},
133 | position{1.0, -1}},
134 | SEARCH_VIEW: {
135 | position{0.0, 7},
136 | position{1.0, -3},
137 | position{1.0, -1},
138 | position{1.0, -1}},
139 | ERROR_VIEW: {
140 | position{0.0, 0},
141 | position{0.0, 0},
142 | position{1.0, -2},
143 | position{1.0, -2}},
144 | SEARCH_PROMPT_VIEW: {
145 | position{0.0, -1},
146 | position{1.0, -3},
147 | position{0.0, 8},
148 | position{1.0, -1}},
149 | POPUP_VIEW: {
150 | position{0.5, -9999}, // set before usage using len(msg)
151 | position{0.5, -1},
152 | position{0.5, -9999}, // set before usage using len(msg)
153 | position{0.5, 1}},
154 | AUTOCOMPLETE_VIEW: {
155 | position{0, -9999},
156 | position{0, -9999},
157 | position{0, -9999},
158 | position{0, -9999}},
159 | }
160 |
161 | type viewProperties struct {
162 | title string
163 | frame bool
164 | editable bool
165 | wrap bool
166 | editor gocui.Editor
167 | text string
168 | }
169 |
170 | var VIEW_PROPERTIES = map[string]viewProperties{
171 | URL_VIEW: {
172 | title: "URL - press F1 for help",
173 | frame: true,
174 | editable: true,
175 | wrap: false,
176 | editor: &singleLineEditor{&defaultEditor},
177 | },
178 | URL_PARAMS_VIEW: {
179 | title: "URL params",
180 | frame: true,
181 | editable: true,
182 | wrap: false,
183 | editor: &defaultEditor,
184 | },
185 | REQUEST_METHOD_VIEW: {
186 | title: "Method",
187 | frame: true,
188 | editable: true,
189 | wrap: false,
190 | editor: &singleLineEditor{&defaultEditor},
191 | text: DEFAULT_METHOD,
192 | },
193 | REQUEST_DATA_VIEW: {
194 | title: "Request data (POST/PUT/PATCH)",
195 | frame: true,
196 | editable: true,
197 | wrap: false,
198 | editor: &defaultEditor,
199 | },
200 | REQUEST_HEADERS_VIEW: {
201 | title: "Request headers",
202 | frame: true,
203 | editable: true,
204 | wrap: false,
205 | editor: &AutocompleteEditor{&defaultEditor, func(str string) []string {
206 | return completeFromSlice(str, REQUEST_HEADERS)
207 | }, []string{}, false},
208 | },
209 | RESPONSE_HEADERS_VIEW: {
210 | title: "Response headers",
211 | frame: true,
212 | editable: true,
213 | wrap: true,
214 | editor: nil, // should be set using a.getViewEditor(g)
215 | },
216 | RESPONSE_BODY_VIEW: {
217 | title: "Response body",
218 | frame: true,
219 | editable: true,
220 | wrap: true,
221 | editor: nil, // should be set using a.getViewEditor(g)
222 | },
223 | SEARCH_VIEW: {
224 | title: "",
225 | frame: false,
226 | editable: true,
227 | wrap: false,
228 | editor: &singleLineEditor{&SearchEditor{&defaultEditor}},
229 | },
230 | STATUSLINE_VIEW: {
231 | title: "",
232 | frame: false,
233 | editable: false,
234 | wrap: false,
235 | editor: nil,
236 | text: "",
237 | },
238 | SEARCH_PROMPT_VIEW: {
239 | title: "",
240 | frame: false,
241 | editable: false,
242 | wrap: false,
243 | editor: nil,
244 | text: SEARCH_PROMPT,
245 | },
246 | POPUP_VIEW: {
247 | title: "Info",
248 | frame: true,
249 | editable: false,
250 | wrap: false,
251 | editor: nil,
252 | },
253 | AUTOCOMPLETE_VIEW: {
254 | title: "",
255 | frame: false,
256 | editable: false,
257 | wrap: false,
258 | editor: nil,
259 | },
260 | }
261 |
262 | var METHODS = []string{
263 | http.MethodGet,
264 | http.MethodPost,
265 | http.MethodPut,
266 | http.MethodDelete,
267 | http.MethodPatch,
268 | http.MethodOptions,
269 | http.MethodTrace,
270 | http.MethodConnect,
271 | http.MethodHead,
272 | }
273 |
274 | var EXPORT_FORMATS = []struct {
275 | name string
276 | export func(r Request) []byte
277 | }{
278 | {
279 | name: "JSON",
280 | export: exportJSON,
281 | },
282 | {
283 | name: "curl",
284 | export: exportCurl,
285 | },
286 | }
287 |
288 | const DEFAULT_METHOD = http.MethodGet
289 |
290 | var DEFAULT_FORMATTER = &formatter.TextFormatter{}
291 |
292 | var CLIENT = &http.Client{
293 | Timeout: time.Duration(TIMEOUT_DURATION * time.Second),
294 | }
295 | var TRANSPORT = &http.Transport{
296 | Proxy: http.ProxyFromEnvironment,
297 | }
298 |
299 | var VIEWS = []string{
300 | URL_VIEW,
301 | URL_PARAMS_VIEW,
302 | REQUEST_METHOD_VIEW,
303 | REQUEST_DATA_VIEW,
304 | REQUEST_HEADERS_VIEW,
305 | SEARCH_VIEW,
306 | RESPONSE_HEADERS_VIEW,
307 | RESPONSE_BODY_VIEW,
308 | }
309 |
310 | var TLS_VERSIONS = map[string]uint16{
311 | "SSL3.0": tls.VersionSSL30,
312 | "TLS1.0": tls.VersionTLS10,
313 | "TLS1.1": tls.VersionTLS11,
314 | "TLS1.2": tls.VersionTLS12,
315 | }
316 |
317 | var defaultEditor ViewEditor
318 |
319 | const (
320 | MIN_WIDTH = 60
321 | MIN_HEIGHT = 20
322 | )
323 |
324 | type Request struct {
325 | Url string
326 | Method string
327 | GetParams string
328 | Data string
329 | Headers string
330 | ResponseHeaders string
331 | RawResponseBody []byte
332 | ContentType string
333 | Duration time.Duration
334 | Formatter formatter.ResponseFormatter
335 | }
336 |
337 | type App struct {
338 | viewIndex int
339 | historyIndex int
340 | currentPopup string
341 | history []*Request
342 | config *config.Config
343 | statusLine *StatusLine
344 | }
345 |
346 | type ViewEditor struct {
347 | app *App
348 | g *gocui.Gui
349 | backTabEscape bool
350 | origEditor gocui.Editor
351 | }
352 |
353 | type AutocompleteEditor struct {
354 | wuzzEditor *ViewEditor
355 | completions func(string) []string
356 | currentCompletions []string
357 | isAutocompleting bool
358 | }
359 |
360 | type SearchEditor struct {
361 | wuzzEditor *ViewEditor
362 | }
363 |
364 | // The singleLineEditor removes multi lines capabilities
365 | type singleLineEditor struct {
366 | wuzzEditor gocui.Editor
367 | }
368 |
369 | func init() {
370 | TRANSPORT.DisableCompression = true
371 | CLIENT.Transport = TRANSPORT
372 | }
373 |
374 | // Editor funcs
375 |
376 | func (e *ViewEditor) Edit(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) {
377 | // handle back-tab (\033[Z) sequence
378 | if e.backTabEscape {
379 | if ch == 'Z' {
380 | e.app.PrevView(e.g, nil)
381 | e.backTabEscape = false
382 | return
383 | } else {
384 | e.origEditor.Edit(v, 0, '[', gocui.ModAlt)
385 | }
386 | }
387 | if ch == '[' && mod == gocui.ModAlt {
388 | e.backTabEscape = true
389 | return
390 | }
391 |
392 | // disable infinite down scroll
393 | if key == gocui.KeyArrowDown && mod == gocui.ModNone {
394 | _, cY := v.Cursor()
395 | _, err := v.Line(cY)
396 | if err != nil {
397 | return
398 | }
399 | }
400 |
401 | e.origEditor.Edit(v, key, ch, mod)
402 | }
403 |
404 | var symbolPattern = regexp.MustCompile("[a-zA-Z0-9-]+$")
405 |
406 | func getLastSymbol(str string) string {
407 | return symbolPattern.FindString(str)
408 | }
409 |
410 | func completeFromSlice(str string, completions []string) []string {
411 | completed := []string{}
412 | if str == "" || strings.TrimRight(str, " \n") != str {
413 | return completed
414 | }
415 | for _, completion := range completions {
416 | if strings.HasPrefix(completion, str) && str != completion {
417 | completed = append(completed, completion)
418 | }
419 | }
420 | return completed
421 | }
422 |
423 | func (e *AutocompleteEditor) Edit(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) {
424 | if key != gocui.KeyEnter {
425 | e.wuzzEditor.Edit(v, key, ch, mod)
426 | }
427 |
428 | cx, cy := v.Cursor()
429 | line, err := v.Line(cy)
430 | trimmedLine := line[:cx]
431 |
432 | if err != nil {
433 | e.wuzzEditor.Edit(v, key, ch, mod)
434 | return
435 | }
436 |
437 | lastSymbol := getLastSymbol(trimmedLine)
438 | if key == gocui.KeyEnter && e.isAutocompleting {
439 | currentCompletion := e.currentCompletions[0]
440 | shouldDelete := true
441 | if len(e.currentCompletions) == 1 {
442 | shouldDelete = false
443 | }
444 |
445 | if shouldDelete {
446 | for range lastSymbol {
447 | v.EditDelete(true)
448 | }
449 | }
450 | for _, char := range currentCompletion {
451 | v.EditWrite(char)
452 | }
453 | closeAutocomplete(e.wuzzEditor.g)
454 | e.isAutocompleting = false
455 | return
456 | } else if key == gocui.KeyEnter {
457 | e.wuzzEditor.Edit(v, key, ch, mod)
458 | }
459 |
460 | closeAutocomplete(e.wuzzEditor.g)
461 | e.isAutocompleting = false
462 |
463 | completions := e.completions(lastSymbol)
464 | e.currentCompletions = completions
465 |
466 | cx, cy = v.Cursor()
467 | sx, _ := v.Size()
468 | ox, oy, _, _, _ := e.wuzzEditor.g.ViewPosition(v.Name())
469 |
470 | maxWidth := sx - cx
471 | maxHeight := 10
472 |
473 | if len(completions) > 0 {
474 | comps := completions
475 | x := ox + cx
476 | y := oy + cy
477 | if len(comps) == 1 {
478 | comps[0] = comps[0][len(lastSymbol):]
479 | } else {
480 | y += 1
481 | x -= len(lastSymbol)
482 | maxWidth += len(lastSymbol)
483 | }
484 | showAutocomplete(comps, x, y, maxWidth, maxHeight, e.wuzzEditor.g)
485 | e.isAutocompleting = true
486 | }
487 | }
488 |
489 | func (e *SearchEditor) Edit(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) {
490 | e.wuzzEditor.Edit(v, key, ch, mod)
491 | e.wuzzEditor.g.Update(func(g *gocui.Gui) error {
492 | e.wuzzEditor.app.PrintBody(g)
493 | return nil
494 | })
495 | }
496 |
497 | // The singleLineEditor removes multi lines capabilities
498 | func (e singleLineEditor) Edit(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) {
499 | switch {
500 | case (ch != 0 || key == gocui.KeySpace) && mod == 0:
501 | e.wuzzEditor.Edit(v, key, ch, mod)
502 | // At the end of the line the default gcui editor adds a whitespace
503 | // Force him to remove
504 | ox, _ := v.Cursor()
505 | if ox > 1 && ox >= len(v.Buffer())-2 {
506 | v.EditDelete(false)
507 | }
508 | return
509 | case key == gocui.KeyEnter:
510 | return
511 | case key == gocui.KeyArrowRight:
512 | ox, _ := v.Cursor()
513 | if ox >= len(v.Buffer())-1 {
514 | return
515 | }
516 | case key == gocui.KeyHome || key == gocui.KeyArrowUp:
517 | v.SetCursor(0, 0)
518 | v.SetOrigin(0, 0)
519 | return
520 | case key == gocui.KeyEnd || key == gocui.KeyArrowDown:
521 | width, _ := v.Size()
522 | lineWidth := len(v.Buffer()) - 1
523 | if lineWidth > width {
524 | v.SetOrigin(lineWidth-width, 0)
525 | lineWidth = width - 1
526 | }
527 | v.SetCursor(lineWidth, 0)
528 | return
529 | }
530 | e.wuzzEditor.Edit(v, key, ch, mod)
531 | }
532 |
533 | //
534 |
535 | func (a *App) getResponseViewEditor(g *gocui.Gui) gocui.Editor {
536 | return &ViewEditor{a, g, false, gocui.EditorFunc(func(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) {
537 | return
538 | })}
539 | }
540 |
541 | func (p position) getCoordinate(max int) int {
542 | return int(p.pct*float32(max)) + p.abs
543 | }
544 |
545 | func setView(g *gocui.Gui, viewName string) (*gocui.View, error) {
546 | maxX, maxY := g.Size()
547 | position := VIEW_POSITIONS[viewName]
548 | return g.SetView(viewName,
549 | position.x0.getCoordinate(maxX+1),
550 | position.y0.getCoordinate(maxY+1),
551 | position.x1.getCoordinate(maxX+1),
552 | position.y1.getCoordinate(maxY+1))
553 | }
554 |
555 | func setViewProperties(v *gocui.View, name string) {
556 | v.Title = VIEW_PROPERTIES[name].title
557 | v.Frame = VIEW_PROPERTIES[name].frame
558 | v.Editable = VIEW_PROPERTIES[name].editable
559 | v.Wrap = VIEW_PROPERTIES[name].wrap
560 | v.Editor = VIEW_PROPERTIES[name].editor
561 | setViewTextAndCursor(v, VIEW_PROPERTIES[name].text)
562 | }
563 |
564 | func (a *App) Layout(g *gocui.Gui) error {
565 | maxX, maxY := g.Size()
566 |
567 | if maxX < MIN_WIDTH || maxY < MIN_HEIGHT {
568 | if v, err := setView(g, ERROR_VIEW); err != nil {
569 | if err != gocui.ErrUnknownView {
570 | return err
571 | }
572 | setViewDefaults(v)
573 | v.Title = VIEW_TITLES[ERROR_VIEW]
574 | g.Cursor = false
575 | fmt.Fprintln(v, "Terminal is too small")
576 | }
577 | return nil
578 | }
579 | if _, err := g.View(ERROR_VIEW); err == nil {
580 | g.DeleteView(ERROR_VIEW)
581 | g.Cursor = true
582 | a.setView(g)
583 | }
584 |
585 | for _, name := range []string{RESPONSE_HEADERS_VIEW, RESPONSE_BODY_VIEW} {
586 | vp := VIEW_PROPERTIES[name]
587 | vp.editor = a.getResponseViewEditor(g)
588 | VIEW_PROPERTIES[name] = vp
589 | }
590 |
591 | if a.config.General.DefaultURLScheme != "" && !strings.HasSuffix(a.config.General.DefaultURLScheme, "://") {
592 | p := VIEW_PROPERTIES[URL_VIEW]
593 | p.text = a.config.General.DefaultURLScheme + "://"
594 | VIEW_PROPERTIES[URL_VIEW] = p
595 | }
596 |
597 | for _, name := range []string{
598 | URL_VIEW,
599 | URL_PARAMS_VIEW,
600 | REQUEST_METHOD_VIEW,
601 | REQUEST_DATA_VIEW,
602 | REQUEST_HEADERS_VIEW,
603 | RESPONSE_HEADERS_VIEW,
604 | RESPONSE_BODY_VIEW,
605 | STATUSLINE_VIEW,
606 | SEARCH_PROMPT_VIEW,
607 | SEARCH_VIEW,
608 | } {
609 | if v, err := setView(g, name); err != nil {
610 | if err != gocui.ErrUnknownView {
611 | return err
612 | }
613 | setViewProperties(v, name)
614 | }
615 | }
616 | refreshStatusLine(a, g)
617 |
618 | return nil
619 | }
620 |
621 | func (a *App) NextView(g *gocui.Gui, v *gocui.View) error {
622 | a.viewIndex = (a.viewIndex + 1) % len(VIEWS)
623 | return a.setView(g)
624 | }
625 |
626 | func (a *App) PrevView(g *gocui.Gui, v *gocui.View) error {
627 | a.viewIndex = (a.viewIndex - 1 + len(VIEWS)) % len(VIEWS)
628 | return a.setView(g)
629 | }
630 |
631 | func (a *App) setView(g *gocui.Gui) error {
632 | a.closePopup(g, a.currentPopup)
633 | _, err := g.SetCurrentView(VIEWS[a.viewIndex])
634 | return err
635 | }
636 |
637 | func (a *App) setViewByName(g *gocui.Gui, name string) error {
638 | for i, v := range VIEWS {
639 | if v == name {
640 | a.viewIndex = i
641 | return a.setView(g)
642 | }
643 | }
644 | return fmt.Errorf("View not found")
645 | }
646 |
647 | func popup(g *gocui.Gui, msg string) {
648 | pos := VIEW_POSITIONS[POPUP_VIEW]
649 | pos.x0.abs = -len(msg)/2 - 1
650 | pos.x1.abs = len(msg)/2 + 1
651 | VIEW_POSITIONS[POPUP_VIEW] = pos
652 |
653 | p := VIEW_PROPERTIES[POPUP_VIEW]
654 | p.text = msg
655 | VIEW_PROPERTIES[POPUP_VIEW] = p
656 |
657 | if v, err := setView(g, POPUP_VIEW); err != nil {
658 | if err != gocui.ErrUnknownView {
659 | return
660 | }
661 | setViewProperties(v, POPUP_VIEW)
662 | g.SetViewOnTop(POPUP_VIEW)
663 | }
664 | }
665 |
666 | func minInt(x, y int) int {
667 | if x < y {
668 | return x
669 | }
670 | return y
671 | }
672 |
673 | func closeAutocomplete(g *gocui.Gui) {
674 | g.DeleteView(AUTOCOMPLETE_VIEW)
675 | }
676 |
677 | func showAutocomplete(completions []string, left, top, maxWidth, maxHeight int, g *gocui.Gui) {
678 | // Get the width of the widest completion
679 | completionsWidth := 0
680 | for _, completion := range completions {
681 | thisCompletionWidth := len(completion)
682 | if thisCompletionWidth > completionsWidth {
683 | completionsWidth = thisCompletionWidth
684 | }
685 | }
686 |
687 | // Get the width and height of the autocomplete window
688 | width := minInt(completionsWidth, maxWidth)
689 | height := minInt(len(completions), maxHeight)
690 |
691 | newPos := viewPosition{
692 | x0: position{0, left},
693 | y0: position{0, top},
694 | x1: position{0, left + width + 1},
695 | y1: position{0, top + height + 1},
696 | }
697 |
698 | VIEW_POSITIONS[AUTOCOMPLETE_VIEW] = newPos
699 |
700 | p := VIEW_PROPERTIES[AUTOCOMPLETE_VIEW]
701 | p.text = strings.Join(completions, "\n")
702 | VIEW_PROPERTIES[AUTOCOMPLETE_VIEW] = p
703 |
704 | if v, err := setView(g, AUTOCOMPLETE_VIEW); err != nil {
705 | if err != gocui.ErrUnknownView {
706 | return
707 | }
708 | setViewProperties(v, AUTOCOMPLETE_VIEW)
709 | v.BgColor = gocui.ColorBlue
710 | v.FgColor = gocui.ColorDefault
711 | g.SetViewOnTop(AUTOCOMPLETE_VIEW)
712 | }
713 | }
714 |
715 | func writeSortedHeaders(output io.Writer, h http.Header) {
716 | hkeys := make([]string, 0, len(h))
717 | for hname := range h {
718 | hkeys = append(hkeys, hname)
719 | }
720 |
721 | sort.Strings(hkeys)
722 |
723 | for _, hname := range hkeys {
724 | fmt.Fprintf(output, "\x1b[0;33m%v:\x1b[0;0m %v\n", hname, strings.Join(h[hname], ","))
725 | }
726 | }
727 |
728 | func (a *App) SubmitRequest(g *gocui.Gui, _ *gocui.View) error {
729 | vrb, _ := g.View(RESPONSE_BODY_VIEW)
730 | vrb.Clear()
731 | vrh, _ := g.View(RESPONSE_HEADERS_VIEW)
732 | vrh.Clear()
733 | popup(g, "Sending request..")
734 |
735 | var r *Request = &Request{}
736 |
737 | go func(g *gocui.Gui, a *App, r *Request) error {
738 | defer g.DeleteView(POPUP_VIEW)
739 | // parse url
740 | r.Url = getViewValue(g, URL_VIEW)
741 | u, err := url.Parse(r.Url)
742 | if err != nil {
743 | g.Update(func(g *gocui.Gui) error {
744 | vrb, _ := g.View(RESPONSE_BODY_VIEW)
745 | fmt.Fprintf(vrb, "URL parse error: %v", err)
746 | return nil
747 | })
748 | return nil
749 | }
750 |
751 | q, err := url.ParseQuery(strings.Replace(getViewValue(g, URL_PARAMS_VIEW), "\n", "&", -1))
752 | if err != nil {
753 | g.Update(func(g *gocui.Gui) error {
754 | vrb, _ := g.View(RESPONSE_BODY_VIEW)
755 | fmt.Fprintf(vrb, "Invalid GET parameters: %v", err)
756 | return nil
757 | })
758 | return nil
759 | }
760 | originalQuery := u.Query()
761 | for k, v := range q {
762 | for _, qp := range v {
763 | originalQuery.Add(k, qp)
764 | }
765 | }
766 | u.RawQuery = originalQuery.Encode()
767 | r.GetParams = u.RawQuery
768 |
769 | // parse method
770 | r.Method = getViewValue(g, REQUEST_METHOD_VIEW)
771 |
772 | // set headers
773 | headers := http.Header{}
774 | headers.Set("User-Agent", "")
775 | r.Headers = getViewValue(g, REQUEST_HEADERS_VIEW)
776 | for _, header := range strings.Split(r.Headers, "\n") {
777 | if header != "" {
778 | header_parts := strings.SplitN(header, ": ", 2)
779 | if len(header_parts) != 2 {
780 | g.Update(func(g *gocui.Gui) error {
781 | vrb, _ := g.View(RESPONSE_BODY_VIEW)
782 | fmt.Fprintf(vrb, "Invalid header: %v", header)
783 | return nil
784 | })
785 | return nil
786 | }
787 | headers.Set(header_parts[0], header_parts[1])
788 | }
789 | }
790 |
791 | var body io.Reader
792 |
793 | // parse POST/PUT/PATCH data
794 | if r.Method == http.MethodPost || r.Method == http.MethodPut || r.Method == http.MethodPatch {
795 | bodyStr := getViewValue(g, REQUEST_DATA_VIEW)
796 | r.Data = bodyStr
797 | if headers.Get("Content-Type") != "multipart/form-data" {
798 | if headers.Get("Content-Type") == "application/x-www-form-urlencoded" {
799 | bodyStr = strings.Replace(bodyStr, "\n", "&", -1)
800 | }
801 | body = bytes.NewBufferString(bodyStr)
802 | } else {
803 | var bodyBytes bytes.Buffer
804 | multiWriter := multipart.NewWriter(&bodyBytes)
805 | defer multiWriter.Close()
806 | postData, err := url.ParseQuery(strings.Replace(getViewValue(g, REQUEST_DATA_VIEW), "\n", "&", -1))
807 | if err != nil {
808 | return err
809 | }
810 | for postKey, postValues := range postData {
811 | for i := range postValues {
812 | if len([]rune(postValues[i])) > 0 && postValues[i][0] == '@' {
813 | file, err := os.Open(postValues[i][1:])
814 | if err != nil {
815 | g.Update(func(g *gocui.Gui) error {
816 | vrb, _ := g.View(RESPONSE_BODY_VIEW)
817 | fmt.Fprintf(vrb, "Error: %v", err)
818 | return nil
819 | })
820 | return err
821 | }
822 | defer file.Close()
823 | fw, err := multiWriter.CreateFormFile(postKey, path.Base(postValues[i][1:]))
824 | if err != nil {
825 | return err
826 | }
827 | if _, err := io.Copy(fw, file); err != nil {
828 | return err
829 | }
830 | } else {
831 | fw, err := multiWriter.CreateFormField(postKey)
832 | if err != nil {
833 | return err
834 | }
835 | if _, err := fw.Write([]byte(postValues[i])); err != nil {
836 | return err
837 | }
838 | }
839 | }
840 | }
841 | body = bytes.NewReader(bodyBytes.Bytes())
842 | }
843 | }
844 |
845 | // create request
846 | req, err := http.NewRequest(r.Method, u.String(), body)
847 | if err != nil {
848 | g.Update(func(g *gocui.Gui) error {
849 | vrb, _ := g.View(RESPONSE_BODY_VIEW)
850 | fmt.Fprintf(vrb, "Request error: %v", err)
851 | return nil
852 | })
853 | return nil
854 | }
855 | req.Header = headers
856 |
857 | // set the `Host` header
858 | if headers.Get("Host") != "" {
859 | req.Host = headers.Get("Host")
860 | }
861 |
862 | // do request
863 | start := time.Now()
864 | response, err := CLIENT.Do(req)
865 | r.Duration = time.Since(start)
866 | if err != nil {
867 | g.Update(func(g *gocui.Gui) error {
868 | vrb, _ := g.View(RESPONSE_BODY_VIEW)
869 | fmt.Fprintf(vrb, "Response error: %v", err)
870 | return nil
871 | })
872 | return nil
873 | }
874 | defer response.Body.Close()
875 |
876 | // extract body
877 | r.ContentType = response.Header.Get("Content-Type")
878 | if response.Header.Get("Content-Encoding") == "gzip" {
879 | reader, err := gzip.NewReader(response.Body)
880 | if err == nil {
881 | defer reader.Close()
882 | response.Body = reader
883 | } else {
884 | g.Update(func(g *gocui.Gui) error {
885 | vrb, _ := g.View(RESPONSE_BODY_VIEW)
886 | fmt.Fprintf(vrb, "Cannot uncompress response: %v", err)
887 | return nil
888 | })
889 | return nil
890 | }
891 | }
892 |
893 | bodyBytes, err := ioutil.ReadAll(response.Body)
894 | if err == nil {
895 | r.RawResponseBody = bodyBytes
896 | }
897 |
898 | r.Formatter = formatter.New(a.config, r.ContentType)
899 |
900 | // add to history
901 | a.history = append(a.history, r)
902 | a.historyIndex = len(a.history) - 1
903 |
904 | // render response
905 | g.Update(func(g *gocui.Gui) error {
906 | vrh, _ := g.View(RESPONSE_HEADERS_VIEW)
907 |
908 | a.PrintBody(g)
909 |
910 | // print status code
911 | status_color := 32
912 | if response.StatusCode != 200 {
913 | status_color = 31
914 | }
915 | header := &strings.Builder{}
916 | fmt.Fprintf(
917 | header,
918 | "\x1b[0;%dmHTTP/1.1 %v %v\x1b[0;0m\n",
919 | status_color,
920 | response.StatusCode,
921 | http.StatusText(response.StatusCode),
922 | )
923 |
924 | writeSortedHeaders(header, response.Header)
925 |
926 | // According to the Go documentation, the Trailer maps trailer
927 | // keys to values in the same format as Header
928 | writeSortedHeaders(header, response.Trailer)
929 |
930 | r.ResponseHeaders = header.String()
931 |
932 | fmt.Fprint(vrh, r.ResponseHeaders)
933 | if _, err := vrh.Line(0); err != nil {
934 | vrh.SetOrigin(0, 0)
935 | }
936 |
937 | return nil
938 | })
939 | return nil
940 | }(g, a, r)
941 |
942 | return nil
943 | }
944 |
945 | func (a *App) PrintBody(g *gocui.Gui) {
946 | g.Update(func(g *gocui.Gui) error {
947 | if len(a.history) == 0 {
948 | return nil
949 | }
950 | req := a.history[a.historyIndex]
951 | if req.RawResponseBody == nil {
952 | return nil
953 | }
954 | vrb, _ := g.View(RESPONSE_BODY_VIEW)
955 | vrb.Clear()
956 |
957 | var responseFormatter formatter.ResponseFormatter
958 | responseFormatter = req.Formatter
959 |
960 | vrb.Title = VIEW_PROPERTIES[vrb.Name()].title + " " + responseFormatter.Title()
961 |
962 | search_text := getViewValue(g, "search")
963 | if search_text == "" || !responseFormatter.Searchable() {
964 | err := responseFormatter.Format(vrb, req.RawResponseBody)
965 | if err != nil {
966 | fmt.Fprintf(vrb, "Error: cannot decode response body: %v", err)
967 | return nil
968 | }
969 | if _, err := vrb.Line(0); !a.config.General.PreserveScrollPosition || err != nil {
970 | vrb.SetOrigin(0, 0)
971 | }
972 | return nil
973 | }
974 | if !a.config.General.ContextSpecificSearch {
975 | responseFormatter = DEFAULT_FORMATTER
976 | }
977 | vrb.SetOrigin(0, 0)
978 | results, err := responseFormatter.Search(search_text, req.RawResponseBody)
979 | if err != nil {
980 | fmt.Fprint(vrb, "Search error: ", err)
981 | return nil
982 | }
983 | if len(results) == 0 {
984 | vrb.Title = "No results"
985 | fmt.Fprint(vrb, "Error: no results")
986 | return nil
987 | }
988 | vrb.Title = fmt.Sprintf("%d results", len(results))
989 | for _, result := range results {
990 | fmt.Fprintf(vrb, "-----\n%s\n", result)
991 | }
992 | return nil
993 | })
994 | }
995 |
996 | func parseKey(k string) (interface{}, gocui.Modifier, error) {
997 | mod := gocui.ModNone
998 | if strings.Index(k, "Alt") == 0 {
999 | mod = gocui.ModAlt
1000 | k = k[3:]
1001 | }
1002 | switch len(k) {
1003 | case 0:
1004 | return 0, 0, errors.New("Empty key string")
1005 | case 1:
1006 | if mod != gocui.ModNone {
1007 | k = strings.ToLower(k)
1008 | }
1009 | return rune(k[0]), mod, nil
1010 | }
1011 |
1012 | key, found := KEYS[k]
1013 | if !found {
1014 | return 0, 0, fmt.Errorf("Unknown key: %v", k)
1015 | }
1016 | return key, mod, nil
1017 | }
1018 |
1019 | func (a *App) setKey(g *gocui.Gui, keyStr, commandStr, viewName string) error {
1020 | if commandStr == "" {
1021 | return nil
1022 | }
1023 | key, mod, err := parseKey(keyStr)
1024 | if err != nil {
1025 | return err
1026 | }
1027 | commandParts := strings.SplitN(commandStr, " ", 2)
1028 | command := commandParts[0]
1029 | var commandArgs string
1030 | if len(commandParts) == 2 {
1031 | commandArgs = commandParts[1]
1032 | }
1033 | keyFnGen, found := COMMANDS[command]
1034 | if !found {
1035 | return fmt.Errorf("Unknown command: %v", command)
1036 | }
1037 | keyFn := keyFnGen(commandArgs, a)
1038 | if err := g.SetKeybinding(viewName, key, mod, keyFn); err != nil {
1039 | return fmt.Errorf("Failed to set key '%v': %v", keyStr, err)
1040 | }
1041 | return nil
1042 | }
1043 |
1044 | func (a *App) printViewKeybindings(v io.Writer, viewName string) {
1045 | keys, found := a.config.Keys[viewName]
1046 | if !found {
1047 | return
1048 | }
1049 | mk := make([]string, len(keys))
1050 | i := 0
1051 | for k := range keys {
1052 | mk[i] = k
1053 | i++
1054 | }
1055 | sort.Strings(mk)
1056 | fmt.Fprintf(v, "\n %v\n", viewName)
1057 | for _, key := range mk {
1058 | fmt.Fprintf(v, " %-15v %v\n", key, keys[key])
1059 | }
1060 | }
1061 |
1062 | func (a *App) SetKeys(g *gocui.Gui) error {
1063 | // load config keybindings
1064 | for viewName, keys := range a.config.Keys {
1065 | if viewName == "global" {
1066 | viewName = ALL_VIEWS
1067 | }
1068 | for keyStr, commandStr := range keys {
1069 | if err := a.setKey(g, keyStr, commandStr, viewName); err != nil {
1070 | return err
1071 | }
1072 | }
1073 | }
1074 |
1075 | g.SetKeybinding(ALL_VIEWS, gocui.KeyF1, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
1076 | if a.currentPopup == HELP_VIEW {
1077 | a.closePopup(g, HELP_VIEW)
1078 | return nil
1079 | }
1080 |
1081 | help, err := a.CreatePopupView(HELP_VIEW, 60, 40, g)
1082 | if err != nil {
1083 | return err
1084 | }
1085 | help.Title = VIEW_TITLES[HELP_VIEW]
1086 | help.Highlight = false
1087 | fmt.Fprint(help, "Keybindings:\n")
1088 | a.printViewKeybindings(help, "global")
1089 | for _, viewName := range VIEWS {
1090 | if _, found := a.config.Keys[viewName]; !found {
1091 | continue
1092 | }
1093 | a.printViewKeybindings(help, viewName)
1094 | }
1095 | g.SetViewOnTop(HELP_VIEW)
1096 | g.SetCurrentView(HELP_VIEW)
1097 | return nil
1098 | })
1099 |
1100 | g.SetKeybinding(ALL_VIEWS, gocui.KeyF11, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
1101 | a.config.General.FollowRedirects = !a.config.General.FollowRedirects
1102 | refreshStatusLine(a, g)
1103 | return nil
1104 | })
1105 |
1106 | g.SetKeybinding(REQUEST_METHOD_VIEW, gocui.KeyEnter, gocui.ModNone, a.ToggleMethodList)
1107 |
1108 | cursDown := func(g *gocui.Gui, v *gocui.View) error {
1109 | cx, cy := v.Cursor()
1110 | v.SetCursor(cx, cy+1)
1111 | return nil
1112 | }
1113 | cursUp := func(g *gocui.Gui, v *gocui.View) error {
1114 | cx, cy := v.Cursor()
1115 | if cy > 0 {
1116 | cy -= 1
1117 | }
1118 | v.SetCursor(cx, cy)
1119 | return nil
1120 | }
1121 | // history key bindings
1122 | g.SetKeybinding(HISTORY_VIEW, gocui.KeyArrowDown, gocui.ModNone, cursDown)
1123 | g.SetKeybinding(HISTORY_VIEW, gocui.KeyArrowUp, gocui.ModNone, cursUp)
1124 | g.SetKeybinding(HISTORY_VIEW, gocui.KeyEnter, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
1125 | _, cy := v.Cursor()
1126 | // TODO error
1127 | if len(a.history) <= cy {
1128 | return nil
1129 | }
1130 | a.restoreRequest(g, cy)
1131 | return nil
1132 | })
1133 |
1134 | // method key bindings
1135 | g.SetKeybinding(REQUEST_METHOD_VIEW, gocui.KeyArrowDown, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
1136 | value := strings.TrimSpace(v.Buffer())
1137 | for i, val := range METHODS {
1138 | if val == value && i != len(METHODS)-1 {
1139 | setViewTextAndCursor(v, METHODS[i+1])
1140 | }
1141 | }
1142 | return nil
1143 | })
1144 |
1145 | g.SetKeybinding(REQUEST_METHOD_VIEW, gocui.KeyArrowUp, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
1146 | value := strings.TrimSpace(v.Buffer())
1147 | for i, val := range METHODS {
1148 | if val == value && i != 0 {
1149 | setViewTextAndCursor(v, METHODS[i-1])
1150 | }
1151 | }
1152 | return nil
1153 | })
1154 | g.SetKeybinding(METHOD_LIST_VIEW, gocui.KeyArrowDown, gocui.ModNone, cursDown)
1155 | g.SetKeybinding(METHOD_LIST_VIEW, gocui.KeyArrowUp, gocui.ModNone, cursUp)
1156 | g.SetKeybinding(METHOD_LIST_VIEW, gocui.KeyEnter, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
1157 | _, cy := v.Cursor()
1158 | v, _ = g.View(REQUEST_METHOD_VIEW)
1159 | setViewTextAndCursor(v, METHODS[cy])
1160 | a.closePopup(g, METHOD_LIST_VIEW)
1161 | return nil
1162 | })
1163 | g.SetKeybinding(SAVE_REQUEST_FORMAT_DIALOG_VIEW, gocui.KeyArrowDown, gocui.ModNone, cursDown)
1164 | g.SetKeybinding(SAVE_REQUEST_FORMAT_DIALOG_VIEW, gocui.KeyArrowUp, gocui.ModNone, cursUp)
1165 |
1166 | g.SetKeybinding(SAVE_DIALOG_VIEW, gocui.KeyCtrlQ, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
1167 | a.closePopup(g, SAVE_DIALOG_VIEW)
1168 | return nil
1169 | })
1170 |
1171 | g.SetKeybinding(SAVE_RESULT_VIEW, gocui.KeyEnter, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
1172 | a.closePopup(g, SAVE_RESULT_VIEW)
1173 | return nil
1174 | })
1175 | return nil
1176 | }
1177 |
1178 | func (a *App) closePopup(g *gocui.Gui, viewname string) {
1179 | _, err := g.View(viewname)
1180 | if err == nil {
1181 | a.currentPopup = ""
1182 | g.DeleteView(viewname)
1183 | g.SetCurrentView(VIEWS[a.viewIndex%len(VIEWS)])
1184 | g.Cursor = true
1185 | }
1186 | }
1187 |
1188 | // CreatePopupView create a popup like view
1189 | func (a *App) CreatePopupView(name string, width, height int, g *gocui.Gui) (v *gocui.View, err error) {
1190 | // Remove any concurrent popup
1191 | a.closePopup(g, a.currentPopup)
1192 |
1193 | g.Cursor = false
1194 | maxX, maxY := g.Size()
1195 | if height > maxY-4 {
1196 | height = maxY - 4
1197 | }
1198 | if width > maxX-4 {
1199 | width = maxX - 4
1200 | }
1201 | v, err = g.SetView(name, maxX/2-width/2-1, maxY/2-height/2-1, maxX/2+width/2, maxY/2+height/2+1)
1202 | if err != nil && err != gocui.ErrUnknownView {
1203 | return
1204 | }
1205 | err = nil
1206 | v.Wrap = false
1207 | v.Frame = true
1208 | v.Highlight = true
1209 | v.SelFgColor = gocui.ColorYellow
1210 | v.SelBgColor = gocui.ColorDefault
1211 | a.currentPopup = name
1212 | return
1213 | }
1214 |
1215 | func (a *App) LoadRequest(g *gocui.Gui, loadLocation string) (err error) {
1216 | requestJson, ioErr := ioutil.ReadFile(loadLocation)
1217 | if ioErr != nil {
1218 | g.Update(func(g *gocui.Gui) error {
1219 | vrb, _ := g.View(RESPONSE_BODY_VIEW)
1220 | vrb.Clear()
1221 | fmt.Fprintf(vrb, "File reading error: %v", ioErr)
1222 | return nil
1223 | })
1224 | return nil
1225 | }
1226 |
1227 | var requestMap map[string]string
1228 | jsonErr := json.Unmarshal(requestJson, &requestMap)
1229 | if jsonErr != nil {
1230 | g.Update(func(g *gocui.Gui) error {
1231 | vrb, _ := g.View(RESPONSE_BODY_VIEW)
1232 | vrb.Clear()
1233 | fmt.Fprintf(vrb, "JSON decoding error: %v", jsonErr)
1234 | return nil
1235 | })
1236 | return nil
1237 | }
1238 |
1239 | var v *gocui.View
1240 | url, exists := requestMap[URL_VIEW]
1241 | if exists {
1242 | v, _ = g.View(URL_VIEW)
1243 | setViewTextAndCursor(v, url)
1244 | }
1245 |
1246 | method, exists := requestMap[REQUEST_METHOD_VIEW]
1247 | if exists {
1248 | v, _ = g.View(REQUEST_METHOD_VIEW)
1249 | setViewTextAndCursor(v, method)
1250 | }
1251 |
1252 | params, exists := requestMap[URL_PARAMS_VIEW]
1253 | if exists {
1254 | v, _ = g.View(URL_PARAMS_VIEW)
1255 | setViewTextAndCursor(v, params)
1256 | }
1257 |
1258 | data, exists := requestMap[REQUEST_DATA_VIEW]
1259 | if exists {
1260 | g.Update(func(g *gocui.Gui) error {
1261 | v, _ = g.View(REQUEST_DATA_VIEW)
1262 | v.Clear()
1263 | fmt.Fprintf(v, "%v", data)
1264 | return nil
1265 | })
1266 | }
1267 |
1268 | headers, exists := requestMap[REQUEST_HEADERS_VIEW]
1269 | if exists {
1270 | v, _ = g.View(REQUEST_HEADERS_VIEW)
1271 | setViewTextAndCursor(v, headers)
1272 | }
1273 | return nil
1274 | }
1275 |
1276 | func (a *App) ToggleHistory(g *gocui.Gui, _ *gocui.View) (err error) {
1277 | // Destroy if present
1278 | if a.currentPopup == HISTORY_VIEW {
1279 | a.closePopup(g, HISTORY_VIEW)
1280 | return
1281 | }
1282 |
1283 | history, err := a.CreatePopupView(HISTORY_VIEW, 100, len(a.history), g)
1284 | if err != nil {
1285 | return
1286 | }
1287 |
1288 | history.Title = VIEW_TITLES[HISTORY_VIEW]
1289 |
1290 | if len(a.history) == 0 {
1291 | setViewTextAndCursor(history, "[!] No items in history")
1292 | return
1293 | }
1294 | for i, r := range a.history {
1295 | req_str := fmt.Sprintf("[%02d] %v %v", i, r.Method, r.Url)
1296 | if r.GetParams != "" {
1297 | req_str += fmt.Sprintf("?%v", strings.Replace(r.GetParams, "\n", "&", -1))
1298 | }
1299 | if r.Data != "" {
1300 | req_str += fmt.Sprintf(" %v", strings.Replace(r.Data, "\n", "&", -1))
1301 | }
1302 | if r.Headers != "" {
1303 | req_str += fmt.Sprintf(" %v", strings.Replace(r.Headers, "\n", ";", -1))
1304 | }
1305 | fmt.Fprintln(history, req_str)
1306 | }
1307 | g.SetViewOnTop(HISTORY_VIEW)
1308 | g.SetCurrentView(HISTORY_VIEW)
1309 | history.SetCursor(0, a.historyIndex)
1310 | return
1311 | }
1312 |
1313 | func (a *App) SaveRequest(g *gocui.Gui, _ *gocui.View) (err error) {
1314 | // Destroy if present
1315 | if a.currentPopup == SAVE_REQUEST_FORMAT_DIALOG_VIEW {
1316 | a.closePopup(g, SAVE_REQUEST_FORMAT_DIALOG_VIEW)
1317 | return
1318 | }
1319 | // Create the view listing the possible formats
1320 | popup, err := a.CreatePopupView(SAVE_REQUEST_FORMAT_DIALOG_VIEW, 30, len(EXPORT_FORMATS), g)
1321 | if err != nil {
1322 | return err
1323 | }
1324 |
1325 | popup.Title = VIEW_TITLES[SAVE_REQUEST_FORMAT_DIALOG_VIEW]
1326 |
1327 | // Populate the popup witht the available formats
1328 | for _, r := range EXPORT_FORMATS {
1329 | fmt.Fprintln(popup, r.name)
1330 | }
1331 |
1332 | g.SetViewOnTop(SAVE_REQUEST_FORMAT_DIALOG_VIEW)
1333 | g.SetCurrentView(SAVE_REQUEST_FORMAT_DIALOG_VIEW)
1334 | popup.SetCursor(0, 0)
1335 |
1336 | // Bind the enter key, when the format is chosen, save the choice and open
1337 | // the save popup
1338 | g.SetKeybinding(SAVE_REQUEST_FORMAT_DIALOG_VIEW, gocui.KeyEnter, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
1339 | // Save the format index
1340 | _, format := v.Cursor()
1341 | // Open the Save popup
1342 | return a.OpenSaveDialog(VIEW_TITLES[SAVE_REQUEST_DIALOG_VIEW], g,
1343 | func(g *gocui.Gui, _ *gocui.View) error {
1344 | defer a.closePopup(g, SAVE_DIALOG_VIEW)
1345 | saveLocation := getViewValue(g, SAVE_DIALOG_VIEW)
1346 |
1347 | r := Request{
1348 | Url: getViewValue(g, URL_VIEW),
1349 | Method: getViewValue(g, REQUEST_METHOD_VIEW),
1350 | GetParams: getViewValue(g, URL_PARAMS_VIEW),
1351 | Data: getViewValue(g, REQUEST_DATA_VIEW),
1352 | Headers: getViewValue(g, REQUEST_HEADERS_VIEW),
1353 | }
1354 |
1355 | // Export the request using the chosent format
1356 | request := EXPORT_FORMATS[format].export(r)
1357 |
1358 | // Write the file
1359 | ioerr := ioutil.WriteFile(saveLocation, []byte(request), 0644)
1360 |
1361 | saveResult := fmt.Sprintf("Request saved successfully in %s", EXPORT_FORMATS[format].name)
1362 | if ioerr != nil {
1363 | saveResult = "Error saving request: " + ioerr.Error()
1364 | }
1365 | viewErr := a.OpenSaveResultView(saveResult, g)
1366 |
1367 | return viewErr
1368 | },
1369 | )
1370 | })
1371 |
1372 | return
1373 | }
1374 |
1375 | func (a *App) ToggleMethodList(g *gocui.Gui, _ *gocui.View) (err error) {
1376 | // Destroy if present
1377 | if a.currentPopup == METHOD_LIST_VIEW {
1378 | a.closePopup(g, METHOD_LIST_VIEW)
1379 | return
1380 | }
1381 |
1382 | method, err := a.CreatePopupView(METHOD_LIST_VIEW, 50, len(METHODS), g)
1383 | if err != nil {
1384 | return
1385 | }
1386 | method.Title = VIEW_TITLES[METHOD_LIST_VIEW]
1387 |
1388 | cur := getViewValue(g, REQUEST_METHOD_VIEW)
1389 |
1390 | for i, r := range METHODS {
1391 | fmt.Fprintln(method, r)
1392 | if cur == r {
1393 | method.SetCursor(0, i)
1394 | }
1395 | }
1396 | g.SetViewOnTop(METHOD_LIST_VIEW)
1397 | g.SetCurrentView(METHOD_LIST_VIEW)
1398 | return
1399 | }
1400 |
1401 | func (a *App) OpenSaveDialog(title string, g *gocui.Gui, save func(g *gocui.Gui, v *gocui.View) error) error {
1402 | dialog, err := a.CreatePopupView(SAVE_DIALOG_VIEW, 60, 1, g)
1403 | if err != nil {
1404 | return err
1405 | }
1406 | g.Cursor = true
1407 |
1408 | dialog.Title = title
1409 | dialog.Editable = true
1410 | dialog.Wrap = false
1411 |
1412 | currentDir, err := os.Getwd()
1413 | if err != nil {
1414 | currentDir = ""
1415 | }
1416 | currentDir += "/"
1417 |
1418 | setViewTextAndCursor(dialog, currentDir)
1419 |
1420 | g.SetViewOnTop(SAVE_DIALOG_VIEW)
1421 | g.SetCurrentView(SAVE_DIALOG_VIEW)
1422 | dialog.SetCursor(0, len(currentDir))
1423 | g.DeleteKeybinding(SAVE_DIALOG_VIEW, gocui.KeyEnter, gocui.ModNone)
1424 | g.SetKeybinding(SAVE_DIALOG_VIEW, gocui.KeyEnter, gocui.ModNone, save)
1425 | return nil
1426 | }
1427 |
1428 | func (a *App) OpenSaveResultView(saveResult string, g *gocui.Gui) (err error) {
1429 | popupTitle := VIEW_TITLES[SAVE_RESULT_VIEW]
1430 | saveResHeight := 1
1431 | saveResWidth := len(saveResult) + 1
1432 | if len(popupTitle)+2 > saveResWidth {
1433 | saveResWidth = len(popupTitle) + 2
1434 | }
1435 | maxX, _ := g.Size()
1436 | if saveResWidth > maxX {
1437 | saveResHeight = saveResWidth/maxX + 1
1438 | saveResWidth = maxX
1439 | }
1440 |
1441 | saveResultPopup, err := a.CreatePopupView(SAVE_RESULT_VIEW, saveResWidth, saveResHeight, g)
1442 | saveResultPopup.Title = popupTitle
1443 | setViewTextAndCursor(saveResultPopup, saveResult)
1444 | g.SetViewOnTop(SAVE_RESULT_VIEW)
1445 | g.SetCurrentView(SAVE_RESULT_VIEW)
1446 | return err
1447 | }
1448 |
1449 | func (a *App) restoreRequest(g *gocui.Gui, idx int) {
1450 | if idx < 0 || idx >= len(a.history) {
1451 | return
1452 | }
1453 | a.closePopup(g, HISTORY_VIEW)
1454 | a.historyIndex = idx
1455 | r := a.history[idx]
1456 |
1457 | v, _ := g.View(URL_VIEW)
1458 | setViewTextAndCursor(v, r.Url)
1459 |
1460 | v, _ = g.View(REQUEST_METHOD_VIEW)
1461 | setViewTextAndCursor(v, r.Method)
1462 |
1463 | v, _ = g.View(URL_PARAMS_VIEW)
1464 | setViewTextAndCursor(v, r.GetParams)
1465 |
1466 | v, _ = g.View(REQUEST_DATA_VIEW)
1467 | setViewTextAndCursor(v, r.Data)
1468 |
1469 | v, _ = g.View(REQUEST_HEADERS_VIEW)
1470 | setViewTextAndCursor(v, r.Headers)
1471 |
1472 | v, _ = g.View(RESPONSE_HEADERS_VIEW)
1473 | setViewTextAndCursor(v, r.ResponseHeaders)
1474 |
1475 | a.PrintBody(g)
1476 |
1477 | }
1478 |
1479 | func (a *App) LoadConfig(configPath string) error {
1480 | if configPath == "" {
1481 | // Load config from default path
1482 | configPath = config.GetDefaultConfigLocation()
1483 | }
1484 |
1485 | // If the config file doesn't exist, load the default config
1486 | if _, err := os.Stat(configPath); os.IsNotExist(err) {
1487 | a.config = &config.DefaultConfig
1488 | a.config.Keys = config.DefaultKeys
1489 | a.statusLine, _ = NewStatusLine(a.config.General.StatusLine)
1490 | return nil
1491 | }
1492 |
1493 | conf, err := config.LoadConfig(configPath)
1494 | if err != nil {
1495 | a.config = &config.DefaultConfig
1496 | a.config.Keys = config.DefaultKeys
1497 | return err
1498 | }
1499 |
1500 | a.config = conf
1501 | sl, err := NewStatusLine(conf.General.StatusLine)
1502 | if err != nil {
1503 | a.config = &config.DefaultConfig
1504 | a.config.Keys = config.DefaultKeys
1505 | return err
1506 | }
1507 | a.statusLine = sl
1508 | return nil
1509 | }
1510 |
1511 | func (a *App) ParseArgs(g *gocui.Gui, args []string) error {
1512 | a.Layout(g)
1513 | g.SetCurrentView(VIEWS[a.viewIndex])
1514 | vheader, err := g.View(REQUEST_HEADERS_VIEW)
1515 | if err != nil {
1516 | return errors.New("Too small screen")
1517 | }
1518 | vheader.Clear()
1519 | vget, _ := g.View(URL_PARAMS_VIEW)
1520 | vget.Clear()
1521 | content_type := ""
1522 | set_data := false
1523 | set_method := false
1524 | set_binary_data := false
1525 | arg_index := 1
1526 | args_len := len(args)
1527 | accept_types := make([]string, 0, 8)
1528 | var body_data []string
1529 | for arg_index < args_len {
1530 | arg := args[arg_index]
1531 | switch arg {
1532 | case "-H", "--header":
1533 | if arg_index == args_len-1 {
1534 | return errors.New("No header value specified")
1535 | }
1536 | arg_index += 1
1537 | header := args[arg_index]
1538 | fmt.Fprintf(vheader, "%v\n", header)
1539 | case "-d", "--data", "--data-binary", "--data-urlencode":
1540 | if arg_index == args_len-1 {
1541 | return errors.New("No POST/PUT/PATCH value specified")
1542 | }
1543 |
1544 | arg_index += 1
1545 | set_data = true
1546 | set_binary_data = arg == "--data-binary"
1547 | arg_data := args[arg_index]
1548 |
1549 | if !set_binary_data {
1550 | content_type = "form"
1551 | }
1552 |
1553 | if arg == "--data-urlencode" {
1554 | // TODO: Replace with `url.PathEscape(..)` in Go 1.8
1555 | arg_data_url := &url.URL{Path: arg_data}
1556 | arg_data = arg_data_url.String()
1557 | }
1558 |
1559 | body_data = append(body_data, arg_data)
1560 | case "-j", "--json":
1561 | if arg_index == args_len-1 {
1562 | return errors.New("No POST/PUT/PATCH value specified")
1563 | }
1564 |
1565 | arg_index += 1
1566 | json_str := args[arg_index]
1567 | content_type = "json"
1568 | accept_types = append(accept_types, config.ContentTypes["json"])
1569 | set_data = true
1570 | vdata, _ := g.View(REQUEST_DATA_VIEW)
1571 | setViewTextAndCursor(vdata, json_str)
1572 | case "-X", "--request":
1573 | if arg_index == args_len-1 {
1574 | return errors.New("No HTTP method specified")
1575 | }
1576 | arg_index++
1577 | set_method = true
1578 | method := args[arg_index]
1579 | if content_type == "" && (method == http.MethodPost || method == http.MethodPut || method == http.MethodPatch) {
1580 | content_type = "form"
1581 | }
1582 | vmethod, _ := g.View(REQUEST_METHOD_VIEW)
1583 | setViewTextAndCursor(vmethod, method)
1584 | case "-t", "--timeout":
1585 | if arg_index == args_len-1 {
1586 | return errors.New("No timeout value specified")
1587 | }
1588 | arg_index += 1
1589 | timeout, err := strconv.Atoi(args[arg_index])
1590 | if err != nil || timeout <= 0 {
1591 | return errors.New("Invalid timeout value")
1592 | }
1593 | a.config.General.Timeout = config.Duration{Duration: time.Duration(timeout) * time.Millisecond}
1594 | case "--compressed":
1595 | vh, _ := g.View(REQUEST_HEADERS_VIEW)
1596 | if !strings.Contains(getViewValue(g, REQUEST_HEADERS_VIEW), "Accept-Encoding") {
1597 | fmt.Fprintln(vh, "Accept-Encoding: gzip, deflate")
1598 | }
1599 | case "-e", "--editor":
1600 | if arg_index == args_len-1 {
1601 | return errors.New("No timeout value specified")
1602 | }
1603 | arg_index += 1
1604 | a.config.General.Editor = args[arg_index]
1605 | case "-k", "--insecure":
1606 | a.config.General.Insecure = true
1607 | case "-R", "--disable-redirects":
1608 | a.config.General.FollowRedirects = false
1609 | case "--tlsv1.0":
1610 | a.config.General.TLSVersionMin = tls.VersionTLS10
1611 | a.config.General.TLSVersionMax = tls.VersionTLS10
1612 | case "--tlsv1.1":
1613 | a.config.General.TLSVersionMin = tls.VersionTLS11
1614 | a.config.General.TLSVersionMax = tls.VersionTLS11
1615 | case "--tlsv1.2":
1616 | a.config.General.TLSVersionMin = tls.VersionTLS12
1617 | a.config.General.TLSVersionMax = tls.VersionTLS12
1618 | case "-1", "--tlsv1":
1619 | a.config.General.TLSVersionMin = tls.VersionTLS10
1620 | a.config.General.TLSVersionMax = tls.VersionTLS12
1621 | case "-T", "--tls":
1622 | if arg_index >= args_len-1 {
1623 | return errors.New("Missing TLS version range: MIN,MAX")
1624 | }
1625 | arg_index++
1626 | arg := args[arg_index]
1627 | v := strings.Split(arg, ",")
1628 | min := v[0]
1629 | max := min
1630 | if len(v) > 1 {
1631 | max = v[1]
1632 | }
1633 | minV, minFound := TLS_VERSIONS[min]
1634 | if !minFound {
1635 | return errors.New("Minimum TLS version not found: " + min)
1636 | }
1637 | maxV, maxFound := TLS_VERSIONS[max]
1638 | if !maxFound {
1639 | return errors.New("Maximum TLS version not found: " + max)
1640 | }
1641 | a.config.General.TLSVersionMin = minV
1642 | a.config.General.TLSVersionMax = maxV
1643 | case "-x", "--proxy":
1644 | if arg_index == args_len-1 {
1645 | return errors.New("Missing proxy URL")
1646 | }
1647 | arg_index += 1
1648 | u, err := url.Parse(args[arg_index])
1649 | if err != nil {
1650 | return fmt.Errorf("Invalid proxy URL: %v", err)
1651 | }
1652 | switch u.Scheme {
1653 | case "", "http", "https":
1654 | TRANSPORT.Proxy = http.ProxyURL(u)
1655 | case "socks", "socks5":
1656 | dialer, err := proxy.SOCKS5("tcp", u.Host, nil, proxy.Direct)
1657 | if err != nil {
1658 | return fmt.Errorf("Can't connect to proxy: %v", err)
1659 | }
1660 | TRANSPORT.Dial = dialer.Dial
1661 | default:
1662 | return errors.New("Unknown proxy protocol")
1663 | }
1664 | case "-F", "--form":
1665 | if arg_index == args_len-1 {
1666 | return errors.New("No POST/PUT/PATCH value specified")
1667 | }
1668 |
1669 | arg_index += 1
1670 | form_str := args[arg_index]
1671 | content_type = "multipart"
1672 | set_data = true
1673 | vdata, _ := g.View(REQUEST_DATA_VIEW)
1674 | setViewTextAndCursor(vdata, form_str)
1675 | case "-f", "--file":
1676 | if arg_index == args_len-1 {
1677 | return errors.New("-f or --file requires a file path be provided as an argument")
1678 | }
1679 | arg_index += 1
1680 | loadLocation := args[arg_index]
1681 | a.LoadRequest(g, loadLocation)
1682 | default:
1683 | u := args[arg_index]
1684 | if strings.Index(u, "http://") != 0 && strings.Index(u, "https://") != 0 {
1685 | u = fmt.Sprintf("%v://%v", a.config.General.DefaultURLScheme, u)
1686 | }
1687 | parsed_url, err := url.Parse(u)
1688 | if err != nil || parsed_url.Host == "" {
1689 | return errors.New("Invalid url")
1690 | }
1691 | if parsed_url.Path == "" {
1692 | parsed_url.Path = "/"
1693 | }
1694 | vurl, _ := g.View(URL_VIEW)
1695 | vurl.Clear()
1696 | for k, v := range parsed_url.Query() {
1697 | for _, vv := range v {
1698 | fmt.Fprintf(vget, "%v=%v\n", k, vv)
1699 | }
1700 | }
1701 | parsed_url.RawQuery = ""
1702 | setViewTextAndCursor(vurl, parsed_url.String())
1703 | }
1704 | arg_index += 1
1705 | }
1706 |
1707 | if set_data && !set_method {
1708 | vmethod, _ := g.View(REQUEST_METHOD_VIEW)
1709 | setViewTextAndCursor(vmethod, http.MethodPost)
1710 | }
1711 |
1712 | if !set_binary_data && content_type != "" && !a.hasHeader(g, "Content-Type") {
1713 | fmt.Fprintf(vheader, "Content-Type: %v\n", config.ContentTypes[content_type])
1714 | }
1715 |
1716 | if len(accept_types) > 0 && !a.hasHeader(g, "Accept") {
1717 | fmt.Fprintf(vheader, "Accept: %v\n", strings.Join(accept_types, ","))
1718 | }
1719 |
1720 | var merged_body_data string
1721 | if set_data && !set_binary_data {
1722 | merged_body_data = strings.Join(body_data, "&")
1723 | }
1724 |
1725 | vdata, _ := g.View(REQUEST_DATA_VIEW)
1726 | setViewTextAndCursor(vdata, merged_body_data)
1727 |
1728 | return nil
1729 | }
1730 |
1731 | func (a *App) hasHeader(g *gocui.Gui, h string) bool {
1732 | for _, header := range strings.Split(getViewValue(g, REQUEST_HEADERS_VIEW), "\n") {
1733 | if header == "" {
1734 | continue
1735 | }
1736 | header_parts := strings.SplitN(header, ": ", 2)
1737 | if len(header_parts) != 2 {
1738 | continue
1739 | }
1740 | if header_parts[0] == h {
1741 | return true
1742 | }
1743 | }
1744 | return false
1745 | }
1746 |
1747 | // Apply startup config values. This is run after a.ParseArgs, so that
1748 | // args can override the provided config values
1749 | func (a *App) InitConfig() {
1750 | CLIENT.Timeout = a.config.General.Timeout.Duration
1751 | TRANSPORT.TLSClientConfig = &tls.Config{
1752 | InsecureSkipVerify: a.config.General.Insecure,
1753 | MinVersion: a.config.General.TLSVersionMin,
1754 | MaxVersion: a.config.General.TLSVersionMax,
1755 | }
1756 | CLIENT.CheckRedirect = func(_ *http.Request, _ []*http.Request) error {
1757 | if a.config.General.FollowRedirects {
1758 | return nil
1759 | }
1760 | return http.ErrUseLastResponse
1761 | }
1762 | }
1763 |
1764 | func refreshStatusLine(a *App, g *gocui.Gui) {
1765 | sv, _ := g.View(STATUSLINE_VIEW)
1766 | sv.BgColor = gocui.ColorDefault | gocui.AttrReverse
1767 | sv.FgColor = gocui.ColorDefault | gocui.AttrReverse
1768 | a.statusLine.Update(sv, a)
1769 | }
1770 |
1771 | func initApp(a *App, g *gocui.Gui) {
1772 | g.Cursor = true
1773 | g.InputEsc = false
1774 | g.BgColor = gocui.ColorDefault
1775 | g.FgColor = gocui.ColorDefault
1776 | g.SetManagerFunc(a.Layout)
1777 | }
1778 |
1779 | func getViewValue(g *gocui.Gui, name string) string {
1780 | v, err := g.View(name)
1781 | if err != nil {
1782 | return ""
1783 | }
1784 | return strings.TrimSpace(v.Buffer())
1785 | }
1786 |
1787 | func setViewDefaults(v *gocui.View) {
1788 | v.Frame = true
1789 | v.Wrap = false
1790 | }
1791 |
1792 | func setViewTextAndCursor(v *gocui.View, s string) {
1793 | v.Clear()
1794 | fmt.Fprint(v, s)
1795 | v.SetCursor(len(s), 0)
1796 | }
1797 |
1798 | func help() {
1799 | fmt.Println(`wuzz - Interactive cli tool for HTTP inspection
1800 |
1801 | Usage: wuzz [-H|--header HEADER]... [-d|--data|--data-binary DATA] [-X|--request METHOD] [-t|--timeout MSECS] [URL]
1802 |
1803 | Other command line options:
1804 | -c, --config PATH Specify custom configuration file
1805 | -e, --editor EDITOR Specify external editor command
1806 | -f, --file REQUEST Load a previous request
1807 | -F, --form DATA Add multipart form request data and set related request headers
1808 | If the value starts with @ it will be handled as a file path for upload
1809 | -h, --help Show this
1810 | -j, --json JSON Add JSON request data and set related request headers
1811 | -k, --insecure Allow insecure SSL certs
1812 | -R, --disable-redirects Do not follow HTTP redirects
1813 | -T, --tls MIN,MAX Restrict allowed TLS versions (values: SSL3.0,TLS1.0,TLS1.1,TLS1.2)
1814 | Examples: wuzz -T TLS1.1 (TLS1.1 only)
1815 | wuzz -T TLS1.0,TLS1.1 (from TLS1.0 up to TLS1.1)
1816 | --tlsv1.0 Forces TLS1.0 only
1817 | --tlsv1.1 Forces TLS1.1 only
1818 | --tlsv1.2 Forces TLS1.2 only
1819 | -1, --tlsv1 Forces TLS version 1.x (1.0, 1.1 or 1.2)
1820 | -v, --version Display version number
1821 | -x, --proxy URL Set HTTP(S) or SOCKS5 proxy
1822 |
1823 | Key bindings:
1824 | ctrl+r Send request
1825 | ctrl+s Save response
1826 | ctrl+e Save request
1827 | ctrl+f Load request
1828 | tab, ctrl+j Next window
1829 | shift+tab, ctrl+k Previous window
1830 | alt+h Show history
1831 | pageUp Scroll up the current window
1832 | pageDown Scroll down the current window`,
1833 | )
1834 | }
1835 |
1836 | func main() {
1837 | configPath := ""
1838 | args := os.Args
1839 | for i, arg := range os.Args {
1840 | switch arg {
1841 | case "-h", "--help":
1842 | help()
1843 | return
1844 | case "-v", "--version":
1845 | fmt.Printf("wuzz %v\n", VERSION)
1846 | return
1847 | case "-c", "--config":
1848 | configPath = os.Args[i+1]
1849 | args = append(os.Args[:i], os.Args[i+2:]...)
1850 | if _, err := os.Stat(configPath); os.IsNotExist(err) {
1851 | log.Fatal("Config file specified but does not exist: \"" + configPath + "\"")
1852 | }
1853 | }
1854 | }
1855 | var g *gocui.Gui
1856 | var err error
1857 | for _, outputMode := range []gocui.OutputMode{gocui.Output256, gocui.OutputNormal, gocui.OutputMode(termbox.OutputGrayscale)} {
1858 | g, err = gocui.NewGui(outputMode)
1859 | if err == nil {
1860 | break
1861 | }
1862 | }
1863 | if err != nil {
1864 | log.Panicln(err)
1865 | }
1866 |
1867 | if runtime.GOOS == WINDOWS_OS && runewidth.IsEastAsian() {
1868 | g.ASCII = true
1869 | }
1870 |
1871 | app := &App{history: make([]*Request, 0, 31)}
1872 |
1873 | // overwrite default editor
1874 | defaultEditor = ViewEditor{app, g, false, gocui.DefaultEditor}
1875 |
1876 | initApp(app, g)
1877 |
1878 | // load config (must be done *before* app.ParseArgs, as arguments
1879 | // should be able to override config values). An empty string passed
1880 | // to LoadConfig results in LoadConfig loading the default config
1881 | // location. If there is no config, the values in
1882 | // config.DefaultConfig will be used.
1883 | err = app.LoadConfig(configPath)
1884 | if err != nil {
1885 | g.Close()
1886 | log.Fatalf("Error loading config file: %v", err)
1887 | }
1888 |
1889 | err = app.ParseArgs(g, args)
1890 |
1891 | // Some of the values in the config need to have some startup
1892 | // behavior associated with them. This is run after ParseArgs so
1893 | // that command-line arguments can override configuration values.
1894 | app.InitConfig()
1895 |
1896 | if err != nil {
1897 | g.Close()
1898 | fmt.Println("Error!", err)
1899 | os.Exit(1)
1900 | }
1901 |
1902 | err = app.SetKeys(g)
1903 |
1904 | if err != nil {
1905 | g.Close()
1906 | fmt.Println("Error!", err)
1907 | os.Exit(1)
1908 | }
1909 |
1910 | defer g.Close()
1911 |
1912 | if err := g.MainLoop(); err != nil && err != gocui.ErrQuit {
1913 | log.Panicln(err)
1914 | }
1915 | }
1916 |
1917 | func exportJSON(r Request) []byte {
1918 | requestMap := map[string]string{
1919 | URL_VIEW: r.Url,
1920 | REQUEST_METHOD_VIEW: r.Method,
1921 | URL_PARAMS_VIEW: r.GetParams,
1922 | REQUEST_DATA_VIEW: r.Data,
1923 | REQUEST_HEADERS_VIEW: r.Headers,
1924 | }
1925 |
1926 | request, err := json.Marshal(requestMap)
1927 | if err != nil {
1928 | return []byte{}
1929 | }
1930 | return request
1931 | }
1932 |
1933 | func exportCurl(r Request) []byte {
1934 | var headers, params string
1935 | for _, header := range strings.Split(r.Headers, "\n") {
1936 | if header == "" {
1937 | continue
1938 | }
1939 | headers = fmt.Sprintf("%s -H %s", headers, shellescape.Quote(header))
1940 | }
1941 | if r.GetParams != "" {
1942 | params = fmt.Sprintf("?%s", r.GetParams)
1943 | }
1944 | return []byte(fmt.Sprintf("curl %s -X %s -d %s %s\n", headers, r.Method, shellescape.Quote(r.Data), shellescape.Quote(r.Url+params)))
1945 | }
1946 |
--------------------------------------------------------------------------------