├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── Gopkg.lock ├── Gopkg.toml ├── LICENSE ├── README.md ├── ROADMAP.md ├── cli ├── cmd.go ├── cmd_test.go ├── help.go └── init.go ├── command.go ├── command_test.go ├── compare └── trial.go ├── config ├── common.go ├── common_test.go ├── save.go └── save_test.go ├── decode ├── TODO.txt ├── b16.go ├── b16_test.go ├── b32.go ├── b32_test.go ├── b64.go ├── b64_test.go ├── codec.go ├── decoder.go ├── gzip.go ├── gzip_test.go ├── init.go ├── main.go ├── main_test.go ├── tocompress ├── url.go └── url_test.go ├── documentation ├── InterceptSequenceDiagram.sdedit ├── README.md ├── informationFlow.dot └── screenshot.png ├── fuzz ├── json.go ├── json_test.go └── main.go ├── intercept ├── common_test.go ├── editorActions.go ├── editorActions_test.go ├── history.go ├── historyActions.go ├── historyMetaData.go ├── historyMetaData_test.go ├── history_test.go ├── interceptHop.go ├── interceptHop_test.go ├── intercept_test.go ├── plugin.go ├── plugin_test.go ├── queues.go ├── requests.go ├── requests_test.go ├── responses.go ├── responsesGenerator.go ├── responses_test.go ├── settingsActions.go └── settingsActions_test.go ├── makefile ├── mitm ├── CA.go ├── CA_test.go ├── cert.go ├── cert_test.go ├── mitm.go └── mitm_test.go ├── mocksy ├── burp.xml ├── burp_b64.xml ├── burpimporter.go ├── burpimporter_test.go ├── init.go ├── main.go ├── matcher.go ├── matcher_test.go ├── server.go └── test.xml ├── pics ├── history.png └── intercept.png ├── project.vim ├── repeat ├── history.go ├── history_test.go ├── queues.go ├── queues_test.go ├── repeat.go ├── repeatActions.go └── repeat_test.go ├── sequence ├── pool.go └── pool_test.go ├── sitemap ├── .gitignore ├── LICENSE ├── README.md └── mocksy.go ├── status.go ├── ui ├── apis │ ├── README.md │ ├── command.go │ ├── constants.go │ ├── history.go │ └── utils.go ├── gopherjs │ ├── domelement.go │ ├── history.go │ ├── main.go │ ├── proxy.go │ ├── repeat.go │ └── tabgroup.go ├── server.go ├── static │ ├── bootstrap.min.css │ ├── bootstrap.min.js │ ├── colresizable.css │ ├── colresizable.min.js │ ├── index.css │ ├── index.css.bak │ ├── index.html │ ├── index.jsbak │ ├── jquery.min.js │ ├── tablesorter.min.js │ └── tmplates.md ├── subs.go ├── templates │ ├── history.html │ ├── index.tmpl │ ├── proxy.html │ └── repeat.html ├── webui.go └── webui_test.go └── wapty.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.pem 2 | *.swp 3 | *.log 4 | *.out 5 | *.cf 6 | /*.test 7 | rice-box.go 8 | gopherjs.js 9 | gopherjs.js.map 10 | tmp.request 11 | tmp.response 12 | Wapty 13 | wapty 14 | Wapty.debug 15 | gopherjs.js 16 | *.map 17 | *.sh 18 | ignored/* 19 | vendor/ 20 | bin/ 21 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.10 4 | install: 5 | - make installdeps 6 | - go get github.com/AlekSi/gocoverutil 7 | - go get github.com/mattn/goveralls 8 | - go get github.com/empijei/cli/lg 9 | script: 10 | - make testv 11 | - gocoverutil test ./... 12 | - goveralls -coverprofile=cover.out -repotoken $COVERALLS_TOKEN 13 | env: 14 | global: 15 | secure: eDwuzolXdZFOFSRVbuQ4V/4YKSSXINfnpSfpbScmeBDh+LtlLznil14+rZTpjAY5UtBnboyZKYn+Cp+wDoVZhCFPPn4FVwLzI8nVMxVUkmnZRynHzjdCR3Wyz5+vCMiOlkNjFv4MC48gDlOIR3JqiijLcrMQzwTuLuondLEcHbewtM5jEaWlDQUZ5R1vtxPb7zdvM/ilXcYsip+Q+mnkA2VbW7Sm2Ks4S+/+I9uqvDyl8Wg5jR0EGmg7FZTZKIR0ArVV410QcluGNnq3PaBdJ9YNnjQQJ+2bDM25/f/dMiiAUf1LdGFL4fFHC38QzzGgMIlmlUMZOiha6BqGlzcIgMJiPoLIkBFsa0x/bDjUutQHMb2Mi/4BMOuZIGG5H+KVsjYyxPHdpUNMmlMvF6OvW/2zZiyqbBmrJouQlO058ni6W7PTKZLnA2dmAXexSKXWo5wxBtPuzgGAo8ol0qqyzyMhE7hMudYi7hHBrUu9Ju91rxx8sRkYE62L7NEATKc7rJfq9bFWnAvqHt8EI4pSTiBZuMMqRNp9GAJj5meQBAKvtTM9Y1Aultucon8abUeeJtFykGKbndibmY5fF5lmobqJfQBl174GzQ1I/nMCNCHm4eJmsdiM9FFJ7Mw4O6GqP0XlsfyQhLqACBWu4/Qkuy8yRAog2wHHjxVnxl4ml5M= 16 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | When contributing to this repository, please first discuss the change you wish to make via issue, 4 | email, telegram or any other method with the owners of this repository before making a change. 5 | 6 | Please note we have a code of conduct, please follow it in all your interactions with the project. 7 | 8 | ## Pull Request Process 9 | 10 | 1. Checkout a new branch: ` git checkout -b my-awesome-feature ` 11 | 1. Add your code/documentation 12 | 1. If you added code, please also add tests that cover at least 80% of it. 13 | 1. If you added code, please document exported APIs and, if you can, also unexported ones. 14 | 1. Run all tests and make sure they pass: `make testv` 15 | 1. Make the Pull Request 16 | 1. Be thanked by the devs :blush: 17 | 18 | If you happen to make several PRs that follow those guidelines and you like the project there is a good chance you will be welcomed as a developer. 19 | 20 | ## For Contributors that have write access 21 | We would like to keep the history as linear as possible, so please when you have to pull from remote use 22 | ``` 23 | git fetch 24 | git rebase 25 | ``` 26 | instead of `git pull` if you have conflicts to solve. 27 | 28 | Unless the commit changes a good portion of the code and it is meaningful to have a merge commit please avoid branching history. 29 | 30 | ## Code of Conduct 31 | 32 | ### Our Pledge 33 | 34 | In the interest of fostering an open and welcoming environment, we as 35 | contributors and maintainers pledge to making participation in our project and 36 | our community a harassment-free experience for everyone, regardless of age, body 37 | size, disability, ethnicity, gender identity and expression, level of experience, 38 | nationality, personal appearance, race, religion, or sexual identity and 39 | orientation. 40 | 41 | ### Our Standards 42 | 43 | Examples of behavior that contributes to creating a positive environment 44 | include: 45 | 46 | * Using welcoming and inclusive language 47 | * Being respectful of differing viewpoints and experiences 48 | * Gracefully accepting constructive criticism 49 | * Focusing on what is best for the community 50 | * Showing empathy towards other community members 51 | 52 | Examples of unacceptable behavior by participants include: 53 | 54 | * The use of sexualized language or imagery and unwelcome sexual attention or 55 | advances 56 | * Trolling, insulting/derogatory comments, and personal or political attacks 57 | * Public or private harassment 58 | * Publishing others' private information, such as a physical or electronic 59 | address, without explicit permission 60 | * Other conduct which could reasonably be considered inappropriate in a 61 | professional setting 62 | 63 | ### Our Responsibilities 64 | 65 | Project maintainers are responsible for clarifying the standards of acceptable 66 | behavior and are expected to take appropriate and fair corrective action in 67 | response to any instances of unacceptable behavior. 68 | 69 | Project maintainers have the right and responsibility to remove, edit, or 70 | reject comments, commits, code, wiki edits, issues, and other contributions 71 | that are not aligned to this Code of Conduct, or to ban temporarily or 72 | permanently any contributor for other behaviors that they deem inappropriate, 73 | threatening, offensive, or harmful. 74 | 75 | ### Scope 76 | 77 | This Code of Conduct applies both within project spaces and in public spaces 78 | when an individual is representing the project or its community. Examples of 79 | representing a project or community include using an official project e-mail 80 | address, posting via an official social media account, or acting as an appointed 81 | representative at an online or offline event. Representation of a project may be 82 | further defined and clarified by project maintainers. 83 | 84 | ### Enforcement 85 | 86 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 87 | reported by contacting the project team at [INSERT EMAIL ADDRESS]. All 88 | complaints will be reviewed and investigated and will result in a response that 89 | is deemed necessary and appropriate to the circumstances. The project team is 90 | obligated to maintain confidentiality with regard to the reporter of an incident. 91 | Further details of specific enforcement policies may be posted separately. 92 | 93 | Project maintainers who do not follow or enforce the Code of Conduct in good 94 | faith may face temporary or permanent repercussions as determined by other 95 | members of the project's leadership. 96 | 97 | ### Attribution 98 | 99 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 100 | available at [http://contributor-covenant.org/version/1/4][version] 101 | 102 | [homepage]: http://contributor-covenant.org 103 | [version]: http://contributor-covenant.org/version/1/4/ 104 | -------------------------------------------------------------------------------- /Gopkg.lock: -------------------------------------------------------------------------------- 1 | # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. 2 | 3 | 4 | [[projects]] 5 | branch = "master" 6 | name = "github.com/GeertJohan/go.rice" 7 | packages = [".","embedded"] 8 | revision = "c02ca9a983da5807ddf7d796784928f5be4afd09" 9 | 10 | [[projects]] 11 | branch = "master" 12 | name = "github.com/daaku/go.zipexe" 13 | packages = ["."] 14 | revision = "a5fe2436ffcb3236e175e5149162b41cd28bd27d" 15 | 16 | [[projects]] 17 | name = "github.com/fatih/color" 18 | packages = ["."] 19 | revision = "570b54cabe6b8eb0bc2dfce68d964677d63b5260" 20 | version = "v1.5.0" 21 | 22 | [[projects]] 23 | branch = "master" 24 | name = "github.com/kardianos/osext" 25 | packages = ["."] 26 | revision = "ae77be60afb1dcacde03767a8c37337fad28ac14" 27 | 28 | [[projects]] 29 | name = "github.com/mattn/go-colorable" 30 | packages = ["."] 31 | revision = "167de6bfdfba052fa6b2d3664c8f5272e23c9072" 32 | version = "v0.0.9" 33 | 34 | [[projects]] 35 | name = "github.com/mattn/go-isatty" 36 | packages = ["."] 37 | revision = "fc9e8d8ef48496124e79ae0df75490096eccf6fe" 38 | version = "v0.0.2" 39 | 40 | [[projects]] 41 | name = "github.com/pmezard/go-difflib" 42 | packages = ["difflib"] 43 | revision = "792786c7400a136282c1664665ae0a8db921c6c2" 44 | version = "v1.0.0" 45 | 46 | [[projects]] 47 | branch = "master" 48 | name = "golang.org/x/net" 49 | packages = ["websocket"] 50 | revision = "66aacef3dd8a676686c7ae3716979581e8b03c47" 51 | 52 | [[projects]] 53 | branch = "master" 54 | name = "golang.org/x/sys" 55 | packages = ["unix"] 56 | revision = "9aade4d3a3b7e6d876cd3823ad20ec45fc035402" 57 | 58 | [solve-meta] 59 | analyzer-name = "dep" 60 | analyzer-version = 1 61 | inputs-digest = "e87a44148674cfea26a7f50fc25fdc9ea66d1779a5e1962cb948ab544da5485b" 62 | solver-name = "gps-cdcl" 63 | solver-version = 1 64 | -------------------------------------------------------------------------------- /Gopkg.toml: -------------------------------------------------------------------------------- 1 | 2 | # Gopkg.toml example 3 | # 4 | # Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md 5 | # for detailed Gopkg.toml documentation. 6 | # 7 | # required = ["github.com/user/thing/cmd/thing"] 8 | # ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] 9 | # 10 | # [[constraint]] 11 | # name = "github.com/user/project" 12 | # version = "1.0.0" 13 | # 14 | # [[constraint]] 15 | # name = "github.com/user/project2" 16 | # branch = "dev" 17 | # source = "github.com/myfork/project2" 18 | # 19 | # [[override]] 20 | # name = "github.com/x/y" 21 | # version = "2.4.0" 22 | 23 | 24 | [[constraint]] 25 | branch = "master" 26 | name = "github.com/GeertJohan/go.rice" 27 | 28 | [[constraint]] 29 | name = "github.com/fatih/color" 30 | version = "1.5.0" 31 | 32 | [[constraint]] 33 | name = "github.com/pmezard/go-difflib" 34 | version = "1.0.0" 35 | 36 | [[constraint]] 37 | branch = "master" 38 | name = "golang.org/x/net" 39 | -------------------------------------------------------------------------------- /ROADMAP.md: -------------------------------------------------------------------------------- 1 | # Roadmap 2 | ## Implemented Features 3 | 4 | * [x] Proxy 5 | * [x] Backend 6 | * [x] UI 7 | * [x] History 8 | * [x] Backend 9 | * [x] UI 10 | * [ ] Repeat 11 | * [x] Backend 12 | * [ ] UI 13 | * [ ] Decode 14 | * [x] Backend 15 | * [x] CLI 16 | * [ ] UI 17 | * Codecs 18 | * [x] Base 16 19 | * [x] Base 32 20 | * [x] Base 64 21 | * [x] URL 22 | * [x] Gzip 23 | * [ ] Binary 24 | * [ ] HTML Entities 25 | * [ ] Javascript-Escape 26 | * [ ] Save/Load 27 | * [ ] Intrude 28 | * [ ] Sequence 29 | * [ ] Websocket 30 | * [ ] Compare 31 | * [ ] Crawl 32 | * [ ] Discover 33 | * [ ] Scan 34 | * [ ] Mock 35 | * [ ] Importer 36 | * [ ] Matcher 37 | * [ ] Server 38 | * [ ] User Documentation 39 | * [ ] Extend 40 | * [ ] Dashboard 41 | * [ ] Help 42 | 43 | # Detailed TODOs 44 | ## Initial stage 45 | This stage will be the first stage for wapty, before this is finished wapty will likely have unstable APIs and won't be really usable. 46 | 47 | * [x] Implement Proxy 48 | * [x] Implement History 49 | * [x] Use a formal approach to fuzzy decoding 50 | * [x] Refactor Decode package 51 | * [x] Rewrite UI in gopherjs 52 | * [x] Simplify server-side code for UI 53 | * [x] use templates for UI 54 | * [x] Use https://gocover.io to compute coverage 55 | * [x] Add intercept checker in the right spots 56 | * [x] Add functionality: releasing the intercept should forward all pending requests 57 | * [x] Add saving functionality 58 | * [x] Use proper Logging 59 | * [x] Add configurations 60 | * [ ] Load configurations on load 61 | * [ ] finish Repeat tests 62 | * [ ] Add scoping 63 | * [ ] Add history filtering/sorting 64 | * [ ] Ignore recursive connect 65 | * [ ] Allow the user to change the destination endpoint 66 | * [ ] Add Intruder 67 | * [ ] Send the whole status on ui connect 68 | * [ ] Make a wapty proxy a struct and provide methods to close/rebind it 69 | * UI 70 | * [ ] Add req ID to editor and warn when receive unexpected requests 71 | * [ ] Sanitize metadata 72 | * [ ] show already pending request/response upon connection 73 | * [ ] error log 74 | * [ ] auto-open ui in browser on launch? 75 | * [ ] monospace textareas 76 | * [ ] resizable splits 77 | * [ ] Add UI to repeater 78 | * [ ] Add UI to decoder 79 | 80 | The following is just some general polishing before calling this a proper project 81 | * [ ] Look for fixmes and todos in the code 82 | * [ ] Improve README 83 | * [ ] Handle panics within the package 84 | * [ ] Move all constant strings to actual constants 85 | * [ ] Analyze all -race warnings 86 | * [ ] All the deferred closes if err!= nil send that, otherwise propagate the new one 87 | * [ ] Doc comment should be a complete sentence that starts with the name being declared. 88 | * [ ] Lint the code, improve score on goreportcard 89 | * [ ] general code polish, doc and and testing 90 | 91 | ## Moving to Release 92 | This is meant to be mostly an improvement, adding features that are less used in burpsuite but are still there and should end up in wapty before it is called a proper replacement for burp 93 | * [ ] Allow for creating multiple proxies, change ports. 94 | * [ ] Keep track of which proxy intercepted the request in metadata. 95 | * [ ] Serve the certificates on a specific fake host/path 96 | * [ ] Add internal router 97 | * [ ] Add AutoEdit 98 | * [ ] Add cURL converter 99 | * [ ] Default to bare sockets on error 100 | * [ ] profile the code, try to find limit-cases 101 | * [ ] Add Spider (remember to add timeouts §8.10) 102 | * [ ] Add Scanner 103 | * [ ] Add Sequencer 104 | * [ ] Add recursive intruder with flows 105 | * [ ] Add syntax highlight for relevant buffers 106 | * [ ] Move away from http.DumpRequest, use the original buffer instead 107 | * [ ] Test transparent proxying 108 | * [ ] Allow to transparently remap a local port to another one with custom certificate. see [tlsmitm](https://github.com/empijei/tlsmitm) as a reference 109 | 110 | ## Release 111 | * [ ] Have penetration testers use wapty for a while, collect feedback 112 | * [ ] Implement fixes, add suggestions to a feature list 113 | * [ ] Advertise and publish the project on a broader scale 114 | 115 | ## Improvements 116 | This section contains the features that burpsuite lacks but that will make this project different :) 117 | 118 | These features will probably be implemented along with the ones in the other stages. 119 | 120 | * [ ] Add Mocksy 121 | * [ ] Add websocket support, with buffers to "stop" data and the chance to add data in both outgoing and incoming sockets 122 | * [ ] UI add preview of blocked requests queue with a chance to perform some actions on them 123 | * [ ] Add pre-engagement 124 | * [ ] analysis/recon 125 | * [ ] detect technologies used/versions 126 | * [ ] Deserializer of java/flash/php serialized objects (maybe editor?) 127 | * [ ] Add SAML, JWT decoder/editor 128 | * [ ] Add a Pathfinder feature to spider that allows to backtrace how a certain URL was discovered 129 | * [ ] Add a Plugin manager / Make plugin behave as package testing, just plug the stuff 130 | * [ ] Add a SQLmap invoker 131 | * [ ] Add fuzzing payloads generator 132 | * [ ] Add TUI 133 | * [ ] Add scripting engine (JS/Lua) 134 | 135 | ## Misc: 136 | These are the feature we are still discussing 137 | 138 | (PRs are welcome :grin: ) 139 | 140 | * [ ] Add Content-Length override 141 | * [ ] Add Beautifier 142 | * [ ] Decompress HTTP2 instead of disabling it 143 | * [ ] [UI] Make operations unblocking and detect ui freezes/deaths. If channel is full and not being read, kill the client. 144 | -------------------------------------------------------------------------------- /cli/cmd.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | "strings" 8 | ) 9 | 10 | // WaptyCommands is the list of all wapty commands available. 11 | // Each command `cmd` is invoked via `wapty cmd` 12 | var WaptyCommands []*Cmd 13 | 14 | // DefaultCommand is the prefixed command set at the beginning of execution 15 | var DefaultCommand *Cmd 16 | 17 | // Cmd is used by any package exposing a runnable command to gather information 18 | // about command name, usage and flagset. 19 | type Cmd struct { 20 | // Name is the name of the command. It's what comes after `wapty`. 21 | Name string 22 | 23 | // Run is the command entrypoint. 24 | Run func(...string) 25 | 26 | // UsageLine is the header of what's printed by flag.PrintDefaults. 27 | UsageLine string 28 | 29 | // Short is a one-line description of what the command does. 30 | Short string 31 | 32 | // Long is the detailed description of what the command does. 33 | Long string 34 | 35 | // Flag is the set of flags accepted by the command. This should be initialized in 36 | // the command's module's `init` function. The parsing of these flags is issued 37 | // by the main wapty entrypoint, so each command doesn't have to do it itself. 38 | Flag flag.FlagSet 39 | } 40 | 41 | // AddCommand allows packages to setup their own command. In order for them to 42 | // be compiled, they must be imported by the main package with the "_" alias 43 | func AddCommand(c *Cmd) { 44 | WaptyCommands = append(WaptyCommands, c) 45 | } 46 | 47 | // Usage prints out the usage helper 48 | func (c *Cmd) Usage() { 49 | fmt.Fprintf(os.Stderr, "usage: %s\n\n", c.UsageLine) 50 | c.Flag.PrintDefaults() 51 | os.Exit(2) 52 | } 53 | 54 | // FindCommand takes a string and searches for a command whose name has that string as 55 | // prefix. If more than 1 command name has that string as a prefix (and no command name 56 | // equals that string), an error is returned. If no suitable command is found, an error 57 | // is returned. 58 | func FindCommand(name string) (command *Cmd, err error) { 59 | for _, cmd := range WaptyCommands { 60 | if cmd.Name == name { 61 | command = cmd 62 | // If there were several commands beginning with this string, but I 63 | // have an exact match, the error should not be returned. 64 | err = nil 65 | return 66 | } 67 | if strings.HasPrefix(cmd.Name, name) { 68 | if command != nil { 69 | err = fmt.Errorf("Ambiguous command: '%s'.", name) 70 | } else { 71 | command = cmd 72 | } 73 | } 74 | } 75 | if command == nil { 76 | err = fmt.Errorf("Command not found: '%s'.", name) 77 | } 78 | return 79 | } 80 | 81 | func callCommand(command *Cmd) { 82 | command.Flag.Usage = command.Usage 83 | //TODO handle this error 84 | _ = command.Flag.Parse(os.Args[1:]) 85 | command.Run(command.Flag.Args()...) 86 | return 87 | } 88 | -------------------------------------------------------------------------------- /cli/cmd_test.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import "testing" 4 | 5 | var findCommandTests = []struct { 6 | in string 7 | out string 8 | }{ 9 | {"pref", "pref"}, 10 | {"prefixs", "prefixsuffix"}, 11 | {"prefix", ""}, 12 | {"suffixes", ""}, 13 | {"prefixd", "prefixdiffsuffix"}, 14 | } 15 | 16 | func TestFindCommand(t *testing.T) { 17 | bk := WaptyCommands 18 | defer func() { WaptyCommands = bk }() 19 | 20 | var out string 21 | WaptyCommands = []*Cmd{ 22 | { 23 | Name: "prefixsuffix", 24 | Run: func(_ ...string) { 25 | out = "prefixsuffix" 26 | }, 27 | }, 28 | { 29 | Name: "prefixdiffsuffix", 30 | Run: func(_ ...string) { 31 | out = "prefixdiffsuffix" 32 | }, 33 | }, 34 | { 35 | Name: "pref", 36 | Run: func(_ ...string) { 37 | out = "pref" 38 | }, 39 | }, 40 | { 41 | Name: "suffix", 42 | Run: func(_ ...string) { 43 | out = "suffix" 44 | }, 45 | }, 46 | } 47 | for _, tt := range findCommandTests { 48 | out = "" 49 | c, e := FindCommand(tt.in) 50 | if e != nil { 51 | if tt.out != "" { 52 | t.Errorf("Expected command <%s> but got error: <%s> instead", tt.out, e.Error()) 53 | continue 54 | } 55 | continue 56 | } 57 | c.Run() 58 | if tt.out != out { 59 | t.Errorf("Expected <%s> but got <%s>", tt.out, out) 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /cli/help.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | "text/template" 8 | "unicode" 9 | "unicode/utf8" 10 | ) 11 | 12 | const docTemplate = `{{.Name | capitalize}}: {{.Short}} 13 | 14 | Usage: 15 | wapty {{.UsageLine}} 16 | 17 | {{.Long | trim}} 18 | ` 19 | 20 | var outw = os.Stderr 21 | 22 | var cmdHelp = &Cmd{ 23 | Name: "help", 24 | Run: helpMain, 25 | UsageLine: "help", 26 | Short: "display help information for wapty commands", 27 | Long: "", 28 | } 29 | 30 | func init() { 31 | AddCommand(cmdHelp) 32 | } 33 | 34 | func helpMain(...string) { 35 | requestedCmd := "help" 36 | if len(os.Args) > 1 { 37 | requestedCmd = os.Args[1] 38 | } 39 | 40 | if command, err := FindCommand(requestedCmd); err == nil { 41 | tmpl := template.New("help") 42 | tmpl.Funcs(template.FuncMap{"trim": strings.TrimSpace, "capitalize": capitalize}) 43 | template.Must(tmpl.Parse(docTemplate)) 44 | _ = tmpl.Execute(outw, command) 45 | } else { 46 | fmt.Fprintf(outw, "help: error processing command: %s\n", err.Error()) 47 | } 48 | } 49 | 50 | func capitalize(s string) string { 51 | if s == "" { 52 | return s 53 | } 54 | r, n := utf8.DecodeRuneInString(s) 55 | return string(unicode.ToTitle(r)) + s[n:] 56 | } 57 | -------------------------------------------------------------------------------- /cli/init.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "text/template" 8 | 9 | "github.com/empijei/cli/lg" 10 | ) 11 | 12 | func init() { 13 | 14 | // Setup fallback version and commit in case wapty wasn't "properly" compiled 15 | if len(Version) == 0 { 16 | Version = "Unknown, please compile wapty with 'make'" 17 | } 18 | if len(Commit) == 0 { 19 | Commit = "Unknown, please compile wapty with 'make'" 20 | } 21 | AddCommand(cmdVersion) 22 | } 23 | 24 | const banner = ` _ 25 | __ ____ _ _ __ | |_ _ _ 26 | \ \ /\ / / _' | '_ \| __| | | | Version: {{.Version}} 27 | \ V V / (_| | |_) | |_| |_| | Commit: {{.Commit}} 28 | \_/\_/ \__,_| .__/ \__|\__, | Build: {{.Build}} 29 | |_| |___/ 30 | 31 | ` 32 | 33 | var ( 34 | //Version is taken by the build flags, represent current version as 35 | //.. 36 | Version string 37 | 38 | //Commit is the output of `git rev-parse HEAD` at the moment of the build 39 | Commit string 40 | 41 | //Build contains info about the scope of the build and should either be Debug or Release 42 | Build string = "Debug" 43 | ) 44 | 45 | var cmdVersion = &Cmd{ 46 | Name: "version", 47 | Run: func(_ ...string) { 48 | lg.Infof("Version: %s\nCommit: %s", Version, Commit) 49 | }, 50 | UsageLine: "version", 51 | Short: "print version and exit", 52 | Long: "print version and exit", 53 | } 54 | 55 | // Printbanner prints the initial banner 56 | func Printbanner() { 57 | tmpl := template.New("banner") 58 | template.Must(tmpl.Parse(banner)) 59 | _ = tmpl.Execute(os.Stderr, struct{ Version, Commit, Build string }{Version, Commit, Build}) 60 | } 61 | 62 | // Init is the first function executed. It prints the banner and 63 | // sets the default command. 64 | func Init() { 65 | if Build == "Release" { 66 | lg.CurLevel = lg.Level_Info 67 | lg.SetFlags(log.Ltime) 68 | } 69 | 70 | stderrinfo, err := os.Stderr.Stat() 71 | if err == nil && stderrinfo.Mode()&os.ModeCharDevice == 0 { 72 | // Output is a pipe, turn off colors 73 | lg.Color = false 74 | } else { 75 | // Output is to terminal, print banner 76 | Printbanner() 77 | } 78 | 79 | if len(os.Args) > 1 { 80 | //read the first argument 81 | directive := os.Args[1] 82 | if len(os.Args) > 2 { 83 | //shift parameters left, but keep argv[0] 84 | os.Args = append(os.Args[:1], os.Args[2:]...) 85 | } else { 86 | os.Args = os.Args[:1] 87 | } 88 | command, err := FindCommand(directive) 89 | if err == nil { 90 | callCommand(command) 91 | } else { 92 | fmt.Fprintf(os.Stderr, "%s\n", err.Error()) 93 | fmt.Fprintln(os.Stderr, "Available commands are:") 94 | fmt.Fprintln(os.Stderr) 95 | for _, cmd := range WaptyCommands { 96 | fmt.Fprintln(os.Stderr, "\t"+cmd.Name+"\t\t"+cmd.Short) 97 | } 98 | fmt.Fprintln(os.Stderr, "\nDefault command is: ", DefaultCommand.Name) 99 | } 100 | } else { 101 | callCommand(DefaultCommand) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /command.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | "strings" 8 | ) 9 | 10 | // WaptyCommands is the list of all wapty commands available. 11 | // Each command `cmd` is invoked via `wapty cmd` 12 | var WaptyCommands []*Command 13 | 14 | // Command is used by any package exposing a runnable command to gather information 15 | // about command name, usage and flagset. 16 | type Command struct { 17 | // Name is the name of the command. It's what comes after `wapty`. 18 | Name string 19 | 20 | // Run is the command entrypoint. 21 | Run func(...string) 22 | 23 | // UsageLine is the header of what's printed by flag.PrintDefaults. 24 | UsageLine string 25 | 26 | // Short is a one-line description of what the command does. 27 | Short string 28 | 29 | // Long is the detailed description of what the command does. 30 | Long string 31 | 32 | // Flag is the set of flags accepted by the command. This should be initialized in 33 | // the command's module's `init` function. The parsing of these flags is issued 34 | // by the main wapty entrypoint, so each command doesn't have to do it itself. 35 | Flag flag.FlagSet 36 | } 37 | 38 | // Usage prints out the usage helper 39 | func (c *Command) Usage() { 40 | fmt.Fprintf(os.Stderr, "usage: %s\n\n", c.UsageLine) 41 | c.Flag.PrintDefaults() 42 | os.Exit(2) 43 | } 44 | 45 | // FindCommand takes a string and searches for a command whose name has that string as 46 | // prefix. If more than 1 command name has that string as a prefix (and no command name 47 | // equals that string), an error is returned. If no suitable command is found, an error 48 | // is returned. 49 | func FindCommand(name string) (command *Command, err error) { 50 | for _, cmd := range WaptyCommands { 51 | if cmd.Name == name { 52 | command = cmd 53 | // If there were several commands beginning with this string, but I 54 | // have an exact match, the error should not be returned. 55 | err = nil 56 | return 57 | } 58 | if strings.HasPrefix(cmd.Name, name) { 59 | if command != nil { 60 | err = fmt.Errorf("Ambiguous command: '%s'.", name) 61 | } else { 62 | command = cmd 63 | } 64 | } 65 | } 66 | if command == nil { 67 | err = fmt.Errorf("Command not found: '%s'.", name) 68 | } 69 | return 70 | } 71 | -------------------------------------------------------------------------------- /command_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "testing" 4 | 5 | var findCommandTests = []struct { 6 | in string 7 | out string 8 | }{ 9 | {"pref", "pref"}, 10 | {"prefixs", "prefixsuffix"}, 11 | {"prefix", ""}, 12 | {"suffixes", ""}, 13 | {"prefixd", "prefixdiffsuffix"}, 14 | } 15 | 16 | func TestFindCommand(t *testing.T) { 17 | bk := WaptyCommands 18 | defer func() { WaptyCommands = bk }() 19 | 20 | var out string 21 | WaptyCommands = []*Command{ 22 | { 23 | Name: "prefixsuffix", 24 | Run: func(_ ...string) { 25 | out = "prefixsuffix" 26 | }, 27 | }, 28 | { 29 | Name: "prefixdiffsuffix", 30 | Run: func(_ ...string) { 31 | out = "prefixdiffsuffix" 32 | }, 33 | }, 34 | { 35 | Name: "pref", 36 | Run: func(_ ...string) { 37 | out = "pref" 38 | }, 39 | }, 40 | { 41 | Name: "suffix", 42 | Run: func(_ ...string) { 43 | out = "suffix" 44 | }, 45 | }, 46 | } 47 | for _, tt := range findCommandTests { 48 | out = "" 49 | c, e := FindCommand(tt.in) 50 | if e != nil { 51 | if tt.out != "" { 52 | t.Errorf("Expected command <%s> but got error: <%s> instead", tt.out, e.Error()) 53 | continue 54 | } 55 | continue 56 | } 57 | c.Run() 58 | if tt.out != out { 59 | t.Errorf("Expected <%s> but got <%s>", tt.out, out) 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /compare/trial.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "unicode" 5 | 6 | "github.com/empijei/cli/lg" 7 | "github.com/fatih/color" 8 | "github.com/pmezard/go-difflib/difflib" 9 | ) 10 | 11 | func main() { 12 | //sa := "aaaa fooo bbbb cc jkj" 13 | //sb := "aaaa bar bar bbbb ./ cc kjk" 14 | sa := "14:36:02" 15 | sb := "14:46:02" 16 | a, ia := wordsplit(sa) 17 | b, ib := wordsplit(sb) 18 | s := difflib.NewMatcher(a, b) 19 | mb := s.GetMatchingBlocks() 20 | lg.Infof("%v", mb[:len(mb)-1]) 21 | lg.Infof("%#v\n%#v\n%#v\n%#v\n", a, ia, b, ib) 22 | type section struct { 23 | begin, end int 24 | } 25 | var secsa []section 26 | for _, m := range mb { 27 | if m.Size == 0 { 28 | break 29 | } 30 | secsa = append(secsa, 31 | section{ 32 | ia[m.A], 33 | ia[m.A+m.Size-1] + len(a[m.A+m.Size-1])}) 34 | } 35 | 36 | lg.Info() 37 | prev := 0 38 | color.Set(color.FgRed) 39 | for _, sec := range secsa { 40 | lg.Info(sa[prev:sec.begin]) 41 | color.Set(color.FgGreen) 42 | lg.Info(sa[sec.begin:sec.end]) 43 | color.Set(color.FgRed) 44 | prev = sec.end 45 | } 46 | if prev != len(sa) { 47 | lg.Info(sa[prev:]) 48 | } 49 | lg.Info() 50 | 51 | var secsb []section 52 | for _, m := range mb { 53 | if m.Size == 0 { 54 | break 55 | } 56 | secsb = append(secsb, 57 | section{ 58 | ib[m.B], 59 | ib[m.B+m.Size-1] + len(b[m.B+m.Size-1])}) 60 | } 61 | color.Unset() 62 | 63 | lg.Info() 64 | color.Set(color.FgRed) 65 | prev = 0 66 | for _, sec := range secsb { 67 | lg.Info(sb[prev:sec.begin]) 68 | color.Set(color.FgGreen) 69 | lg.Info(sb[sec.begin:sec.end]) 70 | color.Set(color.FgRed) 71 | prev = sec.end 72 | } 73 | if prev != len(sb) { 74 | lg.Info(sb[prev:]) 75 | } 76 | } 77 | 78 | func wordsplit(s string) ([]string, []int) { 79 | //dummy := make([]int, len(s)) 80 | //for i, _ := range dummy { 81 | //dummy[i] = i 82 | //} 83 | //return strings.Split(s, ""), dummy 84 | isWord := func(r rune) bool { 85 | switch { 86 | case r == '_': 87 | fallthrough 88 | case unicode.IsLetter(r): 89 | fallthrough 90 | case unicode.IsDigit(r): 91 | return true 92 | default: 93 | return false 94 | } 95 | } 96 | 97 | //This was copy-pasted from strings.FieldsFunc and then edited to keep the 98 | //position of the fields in the original string 99 | 100 | // Now create them. 101 | var a []string 102 | var indexes []int 103 | fieldStart := -1 // Set to -1 when looking for start of field. 104 | for i, r := range s { 105 | if isWord(r) { 106 | if fieldStart == -1 { 107 | fieldStart = i 108 | indexes = append(indexes, i) 109 | } 110 | } else { 111 | if fieldStart >= 0 { 112 | a = append(a, s[fieldStart:i]) 113 | fieldStart = -1 114 | } 115 | a = append(a, s[i:i+1]) 116 | indexes = append(indexes, i) 117 | } 118 | } 119 | if fieldStart >= 0 { // Last field might end at EOF. 120 | a = append(a, s[fieldStart:]) 121 | } 122 | return a, indexes 123 | 124 | } 125 | -------------------------------------------------------------------------------- /config/common.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "os" 7 | "path" 8 | "runtime" 9 | ) 10 | 11 | //TODO? give a way to run in "forensics" mode and do not touch disk 12 | func init() { 13 | err := os.MkdirAll(ConfDir, 0700) 14 | if err != nil { 15 | panic(err) 16 | } 17 | } 18 | 19 | var ( 20 | // ConfDir holds the path for wapty configuration directory 21 | ConfDir = path.Join(getUserHomeDir(), ".wapty") 22 | // ConfName holds the basename of the configuration file 23 | ConfName = "wapty-conf.json" 24 | ) 25 | 26 | var conf Configuration 27 | 28 | // Configuration represents the struct holding all of wapty settings but not 29 | // the saved status 30 | type Configuration struct { 31 | RecentProjects []string 32 | Workspaces []string 33 | } 34 | 35 | func getConfPath() string { 36 | return ConfDir + string(os.PathSeparator) + ConfName 37 | } 38 | 39 | // LoadConf loads the configuration from the default conf file 40 | // WARNING: this function may panic 41 | func LoadConf() { 42 | buf, err := ioutil.ReadFile(getConfPath()) 43 | switch { 44 | case os.IsNotExist(err): 45 | case err != nil: 46 | panic(err) 47 | } 48 | err = json.Unmarshal(buf, &conf) 49 | if err != nil { 50 | panic(err) 51 | } 52 | } 53 | 54 | // SaveConf saves the current configuration into the default conf file 55 | // WARNING: this function may panic 56 | func SaveConf() { 57 | buf, err := json.Marshal(&conf) 58 | if err != nil { 59 | panic(err) 60 | } 61 | err = ioutil.WriteFile(getConfPath(), buf, 0660) 62 | if err != nil { 63 | panic(err) 64 | } 65 | } 66 | 67 | func getUserHomeDir() string { 68 | if runtime.GOOS == "windows" { 69 | home := os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH") 70 | if home == "" { 71 | home = os.Getenv("USERPROFILE") 72 | } 73 | return home 74 | } 75 | return os.Getenv("HOME") 76 | } 77 | -------------------------------------------------------------------------------- /config/common_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "os" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | var ( 10 | tmpConfDir string 11 | savedConf Configuration 12 | ) 13 | 14 | func setup() { 15 | tmpConfDir = ConfDir 16 | ConfDir = os.TempDir() 17 | savedConf = conf 18 | } 19 | 20 | func shutdown() { 21 | ConfDir = tmpConfDir 22 | conf = savedConf 23 | } 24 | 25 | func TestMain(m *testing.M) { 26 | setup() 27 | code := m.Run() 28 | shutdown() 29 | os.Exit(code) 30 | } 31 | 32 | func TestSaveLoadConf(t *testing.T) { 33 | conf = Configuration{ 34 | RecentProjects: []string{"foo", "bar"}, 35 | Workspaces: []string{"lol", "lal"}, 36 | } 37 | compare := conf 38 | SaveConf() 39 | conf = *new(Configuration) 40 | LoadConf() 41 | if !reflect.DeepEqual(compare, conf) { 42 | t.Errorf("Failed Save or Load: expected %#v but got %#v", compare, conf) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /config/save.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "archive/zip" 5 | "bytes" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "os" 10 | ) 11 | 12 | // Project is a collection of SaveLoadStringers 13 | type Project []SaveLoadStringer 14 | 15 | // NewProject returns a Project as a collection of the given SaveLoadStringers 16 | func NewProject(SLSs ...SaveLoadStringer) Project { 17 | return Project(SLSs) 18 | } 19 | 20 | // SaveLoadStringer represents a unit of wapty that allows resuming a previous state 21 | type SaveLoadStringer interface { 22 | Save(io.Writer) error 23 | Load(io.Reader) error 24 | // for debug purposes 25 | fmt.Stringer 26 | } 27 | 28 | // SaveAll invokes all the "Save" methods of the project, creating a zip file containing the status. 29 | // The old file will be removed only on successful save. 30 | func (p Project) SaveAll(workspace string) error { 31 | out, err := os.OpenFile(workspace+".status.zip", os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0660) 32 | if err != nil { 33 | return err 34 | } 35 | 36 | var errorlist []error 37 | w := zip.NewWriter(out) 38 | defer func() { 39 | _ = w.Flush() 40 | _ = w.Close() 41 | }() 42 | 43 | for _, sls := range p { 44 | f, err := w.Create(sls.String()) 45 | if err != nil { 46 | errorlist = append(errorlist, err) 47 | continue 48 | } 49 | 50 | err = sls.Save(f) 51 | if err != nil { 52 | errorlist = append(errorlist, err) 53 | continue 54 | } 55 | 56 | err = w.Flush() 57 | if err != nil { 58 | errorlist = append(errorlist, err) 59 | continue 60 | } 61 | } 62 | 63 | if len(errorlist) > 0 { 64 | buf := bytes.NewBuffer(nil) 65 | for _, err := range errorlist { 66 | buf.WriteString(err.Error() + "\n") 67 | } 68 | return errors.New(string(buf.Bytes())) 69 | } 70 | 71 | return os.Rename(workspace+".status.zip", workspace+"status.zip") 72 | } 73 | 74 | // LoadAll invokes all the "Load" methods of the project, deflating a zip file containing the status. 75 | func (p Project) LoadAll(workspace string) error { 76 | var errorlist []error 77 | r, err := zip.OpenReader(workspace + "status.zip") 78 | 79 | if err != nil { 80 | return err 81 | } 82 | 83 | files := make(map[string]*zip.File) 84 | for _, f := range r.Reader.File { 85 | files[f.FileInfo().Name()] = f 86 | } 87 | 88 | for _, sls := range p { 89 | f, ok := files[sls.String()] 90 | if !ok { 91 | errorlist = append(errorlist, errors.New("Status does not contain "+sls.String())) 92 | continue 93 | } 94 | 95 | opened, err := f.Open() 96 | if err != nil { 97 | errorlist = append(errorlist, err) 98 | continue 99 | } 100 | 101 | err = sls.Load(opened) 102 | if err != nil { 103 | errorlist = append(errorlist, err) 104 | continue 105 | } 106 | } 107 | 108 | if len(errorlist) > 0 { 109 | buf := bytes.NewBuffer(nil) 110 | for _, err := range errorlist { 111 | buf.WriteString(err.Error() + "\n") 112 | } 113 | return errors.New(string(buf.Bytes())) 114 | } 115 | 116 | return nil 117 | } 118 | -------------------------------------------------------------------------------- /config/save_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "io" 5 | "io/ioutil" 6 | "os" 7 | "testing" 8 | ) 9 | 10 | var ( 11 | workspace string 12 | p *Project 13 | data string 14 | ) 15 | 16 | type mockSaveLoadStringer struct { 17 | data, name string 18 | } 19 | 20 | func (m *mockSaveLoadStringer) Save(w io.Writer) error { 21 | _, err := w.Write([]byte(m.data)) 22 | return err 23 | } 24 | 25 | func (m *mockSaveLoadStringer) Load(r io.Reader) error { 26 | tmp, err := ioutil.ReadAll(r) 27 | m.data = string(tmp) 28 | return err 29 | } 30 | 31 | func (m *mockSaveLoadStringer) String() string { 32 | return m.name 33 | } 34 | 35 | func TestSaveAll(t *testing.T) { 36 | // FIXME debug "not a valid zip file" error 37 | workspace = os.TempDir() + string(os.PathSeparator) 38 | 39 | pSave := Project{ 40 | &mockSaveLoadStringer{ 41 | name: "package1", 42 | data: "data of package1", 43 | }, 44 | &mockSaveLoadStringer{ 45 | name: "package2", 46 | data: "data of package2", 47 | }, 48 | } 49 | 50 | err := pSave.SaveAll(workspace) 51 | if err != nil { 52 | t.Error(err) 53 | } 54 | 55 | pLoad := Project{ 56 | &mockSaveLoadStringer{ 57 | name: "package1", 58 | }, 59 | &mockSaveLoadStringer{ 60 | name: "package2", 61 | }, 62 | } 63 | 64 | err = pLoad.LoadAll(workspace) 65 | if err != nil { 66 | t.Error(err) 67 | } 68 | 69 | for _, slsl := range pLoad { 70 | var found bool 71 | for _, slss := range pSave { 72 | if slss.String() == slsl.String() { 73 | found = true 74 | break 75 | } 76 | } 77 | if !found { 78 | t.Error("Laoded alien package " + slsl.String()) 79 | } 80 | } 81 | 82 | for _, slss := range pSave { 83 | var found bool 84 | for _, slsl := range pLoad { 85 | if slss.String() == slsl.String() { 86 | found = true 87 | break 88 | } 89 | } 90 | if !found { 91 | t.Error("Failed to load package " + slss.String()) 92 | } 93 | } 94 | 95 | } 96 | -------------------------------------------------------------------------------- /decode/TODO.txt: -------------------------------------------------------------------------------- 1 | html entity https://en.wikipedia.org/wiki/Character_encodings_in_HTML 2 | A numeric character reference in HTML refers to a character by its Universal Character Set/Unicode code point, and uses the format 3 | &#nnnn; 4 | or 5 | &#xhhhh; 6 | where nnnn is the code point in decimal form, and hhhh is the code point in hexadecimal form. The x must be lowercase in XML documents. The nnnn or hhhh may be any number of digits and may include leading zeros. The hhhh may mix uppercase and lowercase, though uppercase is the usual style. 7 | 8 | -------------------------------------------------------------------------------- /decode/b16.go: -------------------------------------------------------------------------------- 1 | package decode 2 | 3 | import ( 4 | "bytes" 5 | "encoding/hex" 6 | ) 7 | 8 | const b16Alphabet = "0123456789abcdefABCDEF" 9 | 10 | const b16name = "b16" 11 | 12 | // Base16 takes a decoder and an input string 13 | type Base16 struct { 14 | dec *decoder 15 | input string 16 | } 17 | 18 | // NewB16CodecC state machine to smartly decode a string with invalid chars 19 | // nolint: gocyclo 20 | func NewB16CodecC(in string) CodecC { 21 | const ( 22 | itemInvalid itemType = iota 23 | itemAlphabet 24 | ) 25 | 26 | // emit should write into output what was read up until this point 27 | // and move l.start to l.pos 28 | emit := func(d *decoder, t itemType) { 29 | token := d.input[d.start:d.pos] 30 | 31 | var decodefunc func(string) []byte 32 | 33 | switch t { 34 | case itemAlphabet: 35 | decodefunc = func(in string) []byte { 36 | if len(in) < 2 { 37 | return []byte(genInvalid(len(in))) 38 | } 39 | 40 | odd := false 41 | if len(in)%2 != 0 { 42 | in = in[:len(in)-1] 43 | odd = true 44 | } 45 | 46 | buf, err := hex.DecodeString(in) 47 | if err != nil { 48 | return []byte(err.Error()) 49 | } 50 | 51 | if odd { 52 | buf = append(buf, []byte(genInvalid(1))...) 53 | } 54 | return buf 55 | } 56 | 57 | case itemInvalid: 58 | decodefunc = func(in string) []byte { 59 | return []byte(genInvalid(len(in))) 60 | } 61 | } 62 | 63 | d.out.Write(decodefunc(token)) 64 | d.start = d.pos 65 | } 66 | 67 | var ( 68 | startState stateFn 69 | invalidState stateFn 70 | alphabetState stateFn 71 | ) 72 | 73 | startState = func(d *decoder) stateFn { 74 | switch n := d.peek(); { 75 | case bytes.ContainsRune([]byte(b16Alphabet), n): 76 | return alphabetState 77 | case n == eof: 78 | return nil 79 | default: 80 | return invalidState 81 | } 82 | } 83 | 84 | invalidState = func(d *decoder) stateFn { 85 | for { 86 | switch n := d.next(); { 87 | case bytes.ContainsRune([]byte(b16Alphabet), n): 88 | d.backup() 89 | emit(d, itemInvalid) 90 | return alphabetState 91 | 92 | case n == eof: 93 | emit(d, itemInvalid) 94 | return nil 95 | } 96 | } 97 | } 98 | 99 | alphabetState = func(d *decoder) stateFn { 100 | for { 101 | switch n := d.next(); { 102 | case bytes.ContainsRune([]byte(b16Alphabet), n): 103 | d.acceptRun(b16Alphabet) 104 | continue 105 | 106 | case n == eof: 107 | emit(d, itemAlphabet) 108 | return nil 109 | 110 | default: 111 | d.backup() 112 | emit(d, itemAlphabet) 113 | return invalidState 114 | } 115 | } 116 | } 117 | 118 | return &Base16{ 119 | dec: newDecoder(in, startState), 120 | input: in, 121 | } 122 | } 123 | 124 | // Name returns the name of the codec 125 | func (b *Base16) Name() string { 126 | return b16name 127 | } 128 | 129 | // Decode a valid b16 string 130 | func (b *Base16) Decode() (output string) { 131 | return string(b.dec.decode()) 132 | } 133 | 134 | // Encode a string into b16 135 | func (b *Base16) Encode() (output string) { 136 | return hex.EncodeToString([]byte(b.input)) 137 | } 138 | 139 | // Check returns the percentage of valid b16 characters in the input string 140 | func (b *Base16) Check() (acceptability float64) { 141 | var c int 142 | var tot int 143 | for _, r := range b.input { 144 | tot++ 145 | if bytes.ContainsRune([]byte(b16Alphabet), r) { 146 | c++ 147 | } 148 | } 149 | //Heuristic to consider uneven strings as less likely to be valid base16 150 | if delta := tot % 2; delta != 0 { 151 | tot += delta 152 | } 153 | return float64(c) / float64(tot) 154 | } 155 | -------------------------------------------------------------------------------- /decode/b16_test.go: -------------------------------------------------------------------------------- 1 | package decode 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | ) 7 | 8 | var Base16Test = []struct { 9 | in string 10 | eOut string 11 | eIsPrint bool 12 | }{ 13 | { 14 | "666F6F626172", 15 | "foobar", 16 | true, 17 | }, 18 | { 19 | "666f6f626172", 20 | "foobar", 21 | true, 22 | }, 23 | { 24 | "666F6F62617", 25 | "fooba" + genInvalid(1), 26 | false, 27 | }, 28 | { 29 | "666F6F626172.!666F6F626172", 30 | "foobar" + genInvalid(2) + "foobar", 31 | false, 32 | }, 33 | { 34 | "666F6F62617.!666F6F62617", 35 | "fooba" + genInvalid(3) + "fooba" + genInvalid(1), 36 | false, 37 | }, 38 | { 39 | "666F6F62617.!666F6F62617.", 40 | "fooba" + genInvalid(3) + "fooba" + genInvalid(2), 41 | false, 42 | }, 43 | { 44 | "666F6F62617.!666F6F62617.8", 45 | "fooba" + genInvalid(3) + "fooba" + genInvalid(3), 46 | false, 47 | }, 48 | { 49 | "6", 50 | genInvalid(1), 51 | false, 52 | }, 53 | { 54 | "", 55 | "", 56 | true, 57 | }, 58 | { 59 | ".", 60 | genInvalid(1), 61 | false, 62 | }, 63 | } 64 | 65 | var Base16EncodeTest = []struct { 66 | in string 67 | eOut string 68 | }{ 69 | { 70 | "foobar", 71 | "666F6F626172", 72 | }, 73 | { 74 | "fooba▶︎", 75 | "666F6F6261E296B6EFB88E", 76 | }, 77 | { 78 | "", 79 | "", 80 | }, 81 | } 82 | 83 | var Base16CheckTest = []struct { 84 | in string 85 | eOut float64 86 | }{ 87 | { 88 | "666F6F626172", 89 | 1, 90 | }, 91 | { 92 | "666F6F62617", 93 | 0.91, 94 | }, 95 | } 96 | 97 | func TestB16Decode(t *testing.T) { 98 | for _, tt := range Base16Test { 99 | d := NewB16CodecC(tt.in) 100 | out := d.Decode() 101 | if out != tt.eOut { 102 | t.Errorf("Expected decoded value: '%s' but got '%s'", tt.eOut, out) 103 | } 104 | if IsPrint(out) != tt.eIsPrint { 105 | t.Errorf("Expected printable: %v", tt.eIsPrint) 106 | } 107 | } 108 | } 109 | 110 | func TestB16Encode(t *testing.T) { 111 | for _, tt := range Base16EncodeTest { 112 | d := NewB16CodecC(tt.in) 113 | out := d.Encode() 114 | if strings.ToUpper(out) != tt.eOut { 115 | t.Errorf("Expected encoded value: '%s' but got '%s'", tt.eOut, out) 116 | } 117 | } 118 | } 119 | 120 | func TestB16Check(t *testing.T) { 121 | for _, tt := range Base16CheckTest { 122 | d := NewB16CodecC(tt.in) 123 | out := d.Check() 124 | if CompareFloat(out, tt.eOut, 0.1) != 0 { 125 | t.Errorf("Expected check value of '%f' but got '%f'", tt.eOut, out) 126 | } 127 | } 128 | } 129 | 130 | func CompareFloat(a, b float64, tolerance float64) int { 131 | if a < b-tolerance { 132 | return 1 133 | } 134 | if b > a+tolerance { 135 | return -1 136 | } 137 | return 0 138 | } 139 | -------------------------------------------------------------------------------- /decode/b32.go: -------------------------------------------------------------------------------- 1 | package decode 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base32" 6 | "strings" 7 | ) 8 | 9 | // TODO add state that handels = as padding and invalid chars 10 | 11 | const b32Alphabet = "abcdefghijklmnopqrstuvwxyz234567ABCDEFGHIJKLMNOPQRSTUVWXYZ=" 12 | 13 | const b32name = "b32" 14 | 15 | // Base32 takes a decoder and an input string 16 | type Base32 struct { 17 | dec *decoder 18 | input string 19 | } 20 | 21 | // NewB32CodecC state machine to smartly decode a string with invalid chars 22 | // nolint: gocyclo 23 | func NewB32CodecC(in string) CodecC { 24 | const ( 25 | itemInvalid itemType = iota 26 | itemAlphabet 27 | ) 28 | 29 | // emit should write into output what was read up until this point 30 | // and move l.start to l.pos 31 | emit := func(d *decoder, t itemType) { 32 | token := d.input[d.start:d.pos] 33 | 34 | var decodefunc func(string) []byte 35 | 36 | switch t { 37 | case itemAlphabet: 38 | decodefunc = func(in string) []byte { 39 | if len(in) < 2 { 40 | return []byte(genInvalid(len(in))) 41 | } 42 | 43 | in = strings.ToUpper(in) 44 | odd := false 45 | 46 | // checking if len(in) is correct, then add padding 47 | switch n := len(in) % 8; n { 48 | case 6, 3, 1: 49 | in = in[:len(in)-1] 50 | odd = true 51 | } 52 | 53 | pad := (8 - len(in)%8) % 8 54 | in = in + strings.Repeat("=", pad) 55 | 56 | encoding := base32.StdEncoding 57 | buf, err := encoding.DecodeString(in) 58 | if err != nil { 59 | return []byte(err.Error()) 60 | } 61 | 62 | if odd { 63 | buf = append(buf, []byte(genInvalid(1))...) 64 | } 65 | 66 | return buf 67 | } 68 | 69 | case itemInvalid: 70 | decodefunc = func(in string) []byte { 71 | return []byte(genInvalid(len(in))) 72 | } 73 | } 74 | 75 | d.out.Write(decodefunc(token)) 76 | d.start = d.pos 77 | } 78 | 79 | var ( 80 | startState stateFn 81 | invalidState stateFn 82 | alphabetState stateFn 83 | ) 84 | 85 | startState = func(d *decoder) stateFn { 86 | switch n := d.peek(); { 87 | case bytes.ContainsRune([]byte(b32Alphabet), n): 88 | return alphabetState 89 | case n == eof: 90 | return nil 91 | default: 92 | return invalidState 93 | } 94 | } 95 | 96 | invalidState = func(d *decoder) stateFn { 97 | for { 98 | switch n := d.next(); { 99 | case bytes.ContainsRune([]byte(b32Alphabet), n): 100 | d.backup() 101 | emit(d, itemInvalid) 102 | return alphabetState 103 | 104 | case n == eof: 105 | emit(d, itemInvalid) 106 | return nil 107 | } 108 | } 109 | } 110 | 111 | alphabetState = func(d *decoder) stateFn { 112 | for { 113 | switch n := d.next(); { 114 | case bytes.ContainsRune([]byte(b32Alphabet), n): 115 | d.acceptRun(b32Alphabet) 116 | continue 117 | 118 | case n == eof: 119 | emit(d, itemAlphabet) 120 | return nil 121 | 122 | default: 123 | d.backup() 124 | emit(d, itemAlphabet) 125 | return invalidState 126 | } 127 | } 128 | } 129 | 130 | return &Base32{ 131 | dec: newDecoder(in, startState), 132 | input: in, 133 | } 134 | } 135 | 136 | // Name returns the name of the codec 137 | func (b *Base32) Name() string { 138 | return b32name 139 | } 140 | 141 | // Decode a valid b32 string 142 | func (b *Base32) Decode() (output string) { 143 | return string(b.dec.decode()) 144 | } 145 | 146 | // Encode a string into b32 147 | func (b *Base32) Encode() (output string) { 148 | return base32.StdEncoding.EncodeToString([]byte(b.input)) 149 | } 150 | 151 | // Check returns the percentage of valid b32 characters in the input string 152 | func (b *Base32) Check() (acceptability float64) { 153 | var c int 154 | var tot int 155 | for _, r := range b.input { 156 | tot++ 157 | if bytes.ContainsRune([]byte(b32Alphabet), r) { 158 | c++ 159 | } 160 | } 161 | //Heuristic to consider uneven strings as less likely to be valid base32 162 | if delta := tot % 2; delta != 0 { 163 | tot += delta 164 | } 165 | return float64(c) / float64(tot) 166 | } 167 | -------------------------------------------------------------------------------- /decode/b32_test.go: -------------------------------------------------------------------------------- 1 | package decode 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | ) 7 | 8 | var base32Test = []struct { 9 | in string 10 | eOut string 11 | eIsPrint bool 12 | }{ 13 | { 14 | "GFTG633CMFZA====", 15 | "1foobar", 16 | true, 17 | }, 18 | { 19 | "mzxw6ytboi======", 20 | "foobar", 21 | true, 22 | }, 23 | { 24 | "mzxw6ytboi", 25 | "foobar", 26 | true, 27 | }, 28 | { 29 | "MZXW6YTBO", 30 | "fooba" + genInvalid(1), 31 | false, 32 | }, 33 | { 34 | "MZXW6YTBOI.!MZXW6YTBOI", 35 | "foobar" + genInvalid(2) + "foobar", 36 | false, 37 | }, 38 | { 39 | "MZXW6YTBO.!MZXW6YTBO", 40 | "fooba" + genInvalid(3) + "fooba" + genInvalid(1), 41 | false, 42 | }, 43 | { 44 | "MZXW6YTBO.!MZXW6YTBO.", 45 | "fooba" + genInvalid(3) + "fooba" + genInvalid(2), 46 | false, 47 | }, 48 | { 49 | "MZXW6YTBO.!MZXW6YTBO.8", 50 | "fooba" + genInvalid(3) + "fooba" + genInvalid(3), 51 | false, 52 | }, 53 | { 54 | "6", 55 | genInvalid(1), 56 | false, 57 | }, 58 | { 59 | "", 60 | "", 61 | true, 62 | }, 63 | { 64 | ".", 65 | genInvalid(1), 66 | false, 67 | }, 68 | } 69 | 70 | var base32EncodeTest = []struct { 71 | in string 72 | eOut string 73 | }{ 74 | { 75 | "foobar", 76 | "MZXW6YTBOI======", 77 | }, 78 | { 79 | "fooba▶︎", 80 | "MZXW6YTB4KLLN35YRY======", 81 | }, 82 | { 83 | "", 84 | "", 85 | }, 86 | } 87 | 88 | var base32CheckTest = []struct { 89 | in string 90 | eOut float64 91 | }{ 92 | { 93 | "MZXW6YTBOI", 94 | 1, 95 | }, 96 | { 97 | "MZXW6YTBO", 98 | 0.91, 99 | }, 100 | } 101 | 102 | func TestB32Decode(t *testing.T) { 103 | for _, tt := range base32Test { 104 | d := NewB32CodecC(tt.in) 105 | out := d.Decode() 106 | if out != tt.eOut { 107 | t.Errorf("Expected decoded value: '%s' but got '%s'", tt.eOut, out) 108 | } 109 | if IsPrint(out) != tt.eIsPrint { 110 | t.Errorf("Expected printable: %v", tt.eIsPrint) 111 | } 112 | } 113 | } 114 | 115 | func TestB32Encode(t *testing.T) { 116 | for _, tt := range base32EncodeTest { 117 | d := NewB32CodecC(tt.in) 118 | out := d.Encode() 119 | if strings.ToUpper(out) != tt.eOut { 120 | t.Errorf("Expected encoded value: '%s' but got '%s'", tt.eOut, out) 121 | } 122 | } 123 | } 124 | 125 | func TestB32Check(t *testing.T) { 126 | for _, tt := range base32CheckTest { 127 | d := NewB32CodecC(tt.in) 128 | out := d.Check() 129 | if CompareFloat(out, tt.eOut, 0.1) != 0 { 130 | t.Errorf("Expected check value of '%f' but got '%f'", tt.eOut, out) 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /decode/b64.go: -------------------------------------------------------------------------------- 1 | package decode 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base64" 6 | ) 7 | 8 | const b64Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" 9 | const b64Variant = "+/" 10 | const b64Padding = "=" 11 | const b64UrlVariant = "-_" 12 | 13 | const b64name = "b64" 14 | 15 | // Base64 takes a decoder and an input string 16 | type Base64 struct { 17 | dec *decoder 18 | input string 19 | } 20 | 21 | // NewB64CodecC state machine to smartly decode a string with invalid chars 22 | // and different variants 23 | // nolint: gocyclo 24 | func NewB64CodecC(in string) CodecC { 25 | const ( 26 | itemInvalid itemType = iota 27 | itemAlphabet 28 | itemVariant 29 | itemUrlVariant 30 | ) 31 | 32 | ignorePadding := func(d *decoder, start Pos) { 33 | for { 34 | if d.peek() != '=' { 35 | return 36 | } 37 | switch (d.pos - start) % 4 { 38 | case 2, 3: 39 | d.next() 40 | d.ignore() 41 | 42 | default: 43 | return 44 | } 45 | } 46 | } 47 | 48 | // emit writes into output what was read up until this point and move l.start to l.pos 49 | emit := func(d *decoder, t itemType) { 50 | token := d.input[d.start:d.pos] 51 | 52 | var decodefunc func(string) []byte 53 | 54 | switch t { 55 | case itemAlphabet, itemVariant: 56 | decodefunc = func(in string) []byte { 57 | if len(in) < 2 { 58 | return []byte(genInvalid(len(in))) 59 | } 60 | encoding := base64.RawStdEncoding 61 | buf, err := encoding.DecodeString(in) 62 | if err != nil { 63 | return []byte(err.Error()) 64 | } 65 | return buf 66 | } 67 | 68 | case itemUrlVariant: 69 | decodefunc = func(in string) []byte { 70 | if len(in) < 2 { 71 | return []byte(genInvalid(len(in))) 72 | } 73 | encoding := base64.RawURLEncoding 74 | buf, err := encoding.DecodeString(in) 75 | if err != nil { 76 | return []byte(err.Error()) 77 | } 78 | return buf 79 | } 80 | 81 | case itemInvalid: 82 | decodefunc = func(in string) []byte { 83 | return []byte(genInvalid(len(in))) 84 | } 85 | } 86 | 87 | d.out.Write(decodefunc(token)) 88 | d.start = d.pos 89 | } 90 | 91 | var ( 92 | startState stateFn 93 | invalidState stateFn 94 | variantState stateFn 95 | alphabetState stateFn 96 | urlVariantState stateFn 97 | ) 98 | 99 | startState = func(d *decoder) stateFn { 100 | switch n := d.peek(); { 101 | case bytes.ContainsRune([]byte(b64Alphabet), n): 102 | return alphabetState 103 | case bytes.ContainsRune([]byte(b64Variant), n): 104 | return variantState 105 | case bytes.ContainsRune([]byte(b64UrlVariant), n): 106 | return urlVariantState 107 | case n == eof: 108 | return nil 109 | default: 110 | return invalidState 111 | } 112 | } 113 | 114 | invalidState = func(d *decoder) stateFn { 115 | for { 116 | switch n := d.next(); { 117 | case bytes.ContainsRune([]byte(b64Alphabet), n): 118 | d.backup() 119 | emit(d, itemInvalid) 120 | return alphabetState 121 | 122 | case bytes.ContainsRune([]byte(b64Variant), n): 123 | d.backup() 124 | emit(d, itemInvalid) 125 | return variantState 126 | 127 | case bytes.ContainsRune([]byte(b64UrlVariant), n): 128 | d.backup() 129 | emit(d, itemInvalid) 130 | return urlVariantState 131 | 132 | case n == eof: 133 | emit(d, itemInvalid) 134 | return nil 135 | } 136 | } 137 | } 138 | 139 | alphabetState = func(d *decoder) stateFn { 140 | for { 141 | switch n := d.next(); { 142 | case bytes.ContainsRune([]byte(b64Alphabet), n): 143 | d.acceptRun(b64Alphabet) 144 | continue 145 | 146 | case bytes.ContainsRune([]byte(b64Variant), n): 147 | d.backup() 148 | return variantState 149 | 150 | case bytes.ContainsRune([]byte(b64UrlVariant), n): 151 | d.backup() 152 | return urlVariantState 153 | 154 | case n == eof: 155 | emit(d, itemAlphabet) 156 | return nil 157 | 158 | default: 159 | d.backup() 160 | start := d.start 161 | emit(d, itemAlphabet) 162 | ignorePadding(d, start) 163 | return invalidState 164 | } 165 | } 166 | } 167 | 168 | variantState = func(d *decoder) stateFn { 169 | for { 170 | switch n := d.next(); { 171 | case bytes.ContainsRune([]byte(b64Alphabet+b64Variant), n): 172 | d.acceptRun(b64Alphabet + b64Variant) 173 | continue 174 | 175 | case n == eof: 176 | emit(d, itemVariant) 177 | return nil 178 | 179 | default: 180 | d.backup() 181 | start := d.start 182 | emit(d, itemVariant) 183 | ignorePadding(d, start) 184 | return invalidState 185 | } 186 | } 187 | } 188 | 189 | urlVariantState = func(d *decoder) stateFn { 190 | for { 191 | switch n := d.next(); { 192 | case bytes.ContainsRune([]byte(b64Alphabet+b64UrlVariant), n): 193 | d.acceptRun(b64Alphabet + b64UrlVariant) 194 | continue 195 | 196 | case n == eof: 197 | emit(d, itemUrlVariant) 198 | return nil 199 | 200 | default: 201 | d.backup() 202 | start := d.start 203 | emit(d, itemUrlVariant) 204 | ignorePadding(d, start) 205 | return invalidState 206 | } 207 | } 208 | } 209 | 210 | return &Base64{ 211 | dec: newDecoder(in, startState), 212 | input: in, 213 | } 214 | } 215 | 216 | // Name returns the name of the codec 217 | func (b *Base64) Name() string { 218 | return b64name 219 | } 220 | 221 | // Decode a valid b64 string 222 | func (b *Base64) Decode() (output string) { 223 | return string(b.dec.decode()) 224 | } 225 | 226 | // Encode a string into b64 with StdEncodig set 227 | func (b *Base64) Encode() (output string) { 228 | //TODO allow user to decide which encoder 229 | return base64.StdEncoding.EncodeToString([]byte(b.input)) 230 | } 231 | 232 | // Check returns the percentage of valid b16 characters in the input string 233 | func (b *Base64) Check() (acceptability float64) { 234 | var c int 235 | var tot int 236 | for _, r := range b.input { 237 | tot++ 238 | if bytes.ContainsRune([]byte(b64Alphabet+b64Variant+b64UrlVariant+b64Padding), r) { 239 | c++ 240 | } 241 | } 242 | //Heuristic to consider uneven strings as less likely to be valid base64 243 | if delta := tot % 4; delta != 0 { 244 | tot += delta 245 | } 246 | return float64(c) / float64(tot) 247 | } 248 | -------------------------------------------------------------------------------- /decode/b64_test.go: -------------------------------------------------------------------------------- 1 | package decode 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | var B64Test = []struct { 8 | in string 9 | eOut string 10 | eIsPrint bool 11 | }{ 12 | { 13 | "Zm9vYmFy", 14 | "foobar", 15 | true, 16 | }, 17 | { 18 | "Zm9vYm", 19 | "foob", 20 | true, 21 | }, 22 | { 23 | "Zm9vYm==", 24 | "foob", 25 | true, 26 | }, 27 | { 28 | "!Zm!Ym", 29 | genInvalid(1) + "f" + genInvalid(1) + "b", 30 | false, 31 | }, 32 | { 33 | "!Zm9vYm", 34 | genInvalid(1) + "foob", 35 | false, 36 | }, 37 | 38 | { 39 | "Zm9vYmFy.!Zm9vYmFy", 40 | "foobar" + genInvalid(2) + "foobar", 41 | false, 42 | }, 43 | { 44 | "Zm9vYmF.!Zm9vYmF", 45 | "fooba" + genInvalid(2) + "fooba", 46 | false, 47 | }, 48 | { 49 | "Zm9vYmF.!Zm9vYmFy.", 50 | "fooba" + genInvalid(2) + "foobar" + genInvalid(1), 51 | false, 52 | }, 53 | { 54 | "Zm9vYmF.!Zm9vYmF.8", 55 | "fooba" + genInvalid(2) + "fooba" + genInvalid(2), 56 | false, 57 | }, 58 | { 59 | "6", 60 | genInvalid(1), 61 | false, 62 | }, 63 | { 64 | "", 65 | "", 66 | true, 67 | }, 68 | { 69 | "8J+Hq/Cfh7c", 70 | "\xf0\x9f\x87\xab\xf0\x9f\x87\xb7", 71 | true, 72 | }, 73 | { 74 | "8J-Hq_Cfh7c", 75 | "\xf0\x9f\x87\xab\xf0\x9f\x87\xb7", 76 | true, 77 | }, 78 | 79 | { 80 | "8J+Hq/$8J+Hq/", 81 | "\xf0\x9f\x87\xab" + genInvalid(1) + "\xf0\x9f\x87\xab", 82 | false, 83 | }, 84 | { 85 | "8J-Hq_Cfh7c$8J-Hq_Cfh7c", 86 | "\xf0\x9f\x87\xab\xf0\x9f\x87\xb7" + genInvalid(1) + "\xf0\x9f\x87\xab\xf0\x9f\x87\xb7", 87 | false, 88 | }, 89 | { 90 | "$+Hq/Cfh7cg", 91 | genInvalid(1) + "\xf8z\xbf\t\xf8{r", 92 | false, 93 | }, 94 | { 95 | "$-Hq_Cfh7cg", 96 | genInvalid(1) + "\xf8z\xbf\t\xf8{r", 97 | false, 98 | }, 99 | { 100 | "+Hq/Cfh7cg", 101 | "\xf8z\xbf\t\xf8{r", 102 | false, 103 | }, 104 | { 105 | "-Hq_Cfh7cg", 106 | "\xf8z\xbf\t\xf8{r", 107 | false, 108 | }, 109 | { 110 | "/", 111 | genInvalid(1), 112 | false, 113 | }, 114 | { 115 | "_", 116 | genInvalid(1), 117 | false, 118 | }, 119 | } 120 | 121 | var Base64EncodeTest = []struct { 122 | in string 123 | eOut string 124 | }{ 125 | { 126 | "foobar", 127 | "Zm9vYmFy", 128 | }, 129 | { 130 | "fooba▶︎", 131 | "Zm9vYmHilrbvuI4=", 132 | }, 133 | { 134 | "", 135 | "", 136 | }, 137 | } 138 | 139 | var Base64CheckTest = []struct { 140 | in string 141 | eOut float64 142 | }{ 143 | { 144 | "Zm9vYmFy", 145 | 1, 146 | }, 147 | { 148 | "Zm9vYmF", 149 | 0.7, 150 | }, 151 | } 152 | 153 | func TestB64Decode(t *testing.T) { 154 | for _, tt := range B64Test { 155 | d := NewB64CodecC(tt.in) 156 | out := d.Decode() 157 | if out != tt.eOut { 158 | t.Errorf("Expected decoded value: '%s' but got '%s'", tt.eOut, out) 159 | } 160 | if IsPrint(out) != tt.eIsPrint { 161 | t.Errorf("Expected printable: %v", tt.eIsPrint) 162 | } 163 | } 164 | } 165 | 166 | func TestB64Encode(t *testing.T) { 167 | for _, tt := range Base64EncodeTest { 168 | d := NewB64CodecC(tt.in) 169 | out := d.Encode() 170 | if out != tt.eOut { 171 | t.Errorf("Expected encoded value: '%s' but got '%s'", tt.eOut, out) 172 | } 173 | } 174 | } 175 | 176 | func TestB64Check(t *testing.T) { 177 | for _, tt := range Base64CheckTest { 178 | d := NewB64CodecC(tt.in) 179 | out := d.Check() 180 | if CompareFloat(out, tt.eOut, 0.1) != 0 { 181 | t.Errorf("Expected check value of '%f' but got '%f'", tt.eOut, out) 182 | } 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /decode/codec.go: -------------------------------------------------------------------------------- 1 | package decode 2 | 3 | import ( 4 | "strings" 5 | "unicode" 6 | 7 | "github.com/empijei/cli/lg" 8 | ) 9 | 10 | type codecConstructor func(string) CodecC 11 | 12 | var codecs = []struct { 13 | name string 14 | codecCons codecConstructor 15 | }{ 16 | { 17 | b16name, 18 | codecConstructor(NewB16CodecC), 19 | }, 20 | { 21 | b32name, 22 | codecConstructor(NewB32CodecC), 23 | }, 24 | { 25 | b64name, 26 | codecConstructor(NewB64CodecC), 27 | }, 28 | { 29 | urlname, 30 | codecConstructor(NewURLCodecC), 31 | }, 32 | { 33 | gzipname, 34 | codecConstructor(NewGzipCodecC), 35 | }, 36 | } 37 | 38 | //Decoder decodes the string and returns a decoded value that tries to skip 39 | //invalid input and to decode as much as possible. 40 | //Returns if the decoded string can be printed as valid unicode. 41 | type Decoder interface { 42 | Decode() (output string) 43 | } 44 | 45 | //Encoder encodes the string 46 | type Encoder interface { 47 | Encode() (output string) 48 | } 49 | 50 | //Checker returns a metric to determine how likely it is for the given string 51 | //to be a valid value for the specified Checker Type. 52 | //The likelihood always ranges between 0 and 1 53 | type Checker interface { 54 | Check() (acceptability float64) 55 | } 56 | 57 | //CodecC creates an interface of interfaces usable by other codecs 58 | type CodecC interface { 59 | Decoder 60 | Encoder 61 | Checker 62 | Name() string 63 | } 64 | 65 | //SmartDecode loops through the available CodecCs 66 | //and determine which one is the best one to use 67 | func SmartDecode(input string) (c CodecC) { 68 | var curvalue float64 69 | //FIXME add a null codecC if no codecC is selected 70 | for _, cc := range codecs { 71 | tmp := cc.codecCons(input) 72 | if t := tmp.Check(); t > curvalue { 73 | curvalue = t 74 | c = tmp 75 | } 76 | } 77 | lg.Infof("Smart Decoding, selected: %s with likelihood==%d%%", c.Name(), int(curvalue*100)) 78 | return 79 | } 80 | 81 | // IsPrint checks if a decoded string is a valid utf string 82 | func IsPrint(decoded string) bool { 83 | if strings.Contains(decoded, string(invalid)) { 84 | return false 85 | } 86 | for _, r := range decoded { 87 | if !unicode.IsPrint(r) { 88 | return false 89 | } 90 | } 91 | return true 92 | } 93 | -------------------------------------------------------------------------------- /decode/decoder.go: -------------------------------------------------------------------------------- 1 | package decode 2 | 3 | import ( 4 | "bytes" 5 | "strings" 6 | "unicode/utf8" 7 | ) 8 | 9 | const eof = -1 10 | 11 | var invalid = '�' 12 | 13 | // Pos is an integer that define a position inside a string 14 | type Pos int 15 | type itemType int 16 | 17 | func genInvalid(n int) (inv string) { 18 | return strings.Repeat(string(invalid), n) 19 | } 20 | 21 | // stateFn represents the state of the scanner as a function that returns the next state. 22 | type stateFn func(*decoder) stateFn 23 | 24 | // decoder holds the state of the scanner. 25 | type decoder struct { 26 | input string // the string being scanned 27 | state stateFn // the next lexing function to enter 28 | pos Pos // current position in the input 29 | start Pos // start position of this item 30 | width Pos // width of last rune read from input 31 | out *bytes.Buffer 32 | } 33 | 34 | // Constructs a new decoder 35 | func newDecoder(input string, startState stateFn) *decoder { 36 | return &decoder{ 37 | input: input, 38 | state: startState, 39 | out: bytes.NewBuffer(nil), 40 | } 41 | } 42 | 43 | // next returns the next rune in the input. 44 | func (l *decoder) next() rune { 45 | if int(l.pos) >= len(l.input) { 46 | l.width = 0 47 | return eof 48 | } 49 | r, w := utf8.DecodeRuneInString(l.input[l.pos:]) 50 | l.width = Pos(w) 51 | l.pos += l.width 52 | return r 53 | } 54 | 55 | // peek returns but does not consume the next rune in the input. 56 | func (l *decoder) peek() rune { 57 | p := l.width 58 | r := l.next() 59 | l.backup() 60 | l.width = p 61 | return r 62 | } 63 | 64 | // backup steps back one rune. Can only be called once per call of next. 65 | func (l *decoder) backup() { 66 | l.pos -= l.width 67 | } 68 | 69 | // ignore skips over the pending input before this point. 70 | func (l *decoder) ignore() { 71 | l.start = l.pos 72 | } 73 | 74 | // acceptRun consumes a run of runes from the valid set. 75 | func (l *decoder) acceptRun(valid string) { 76 | for bytes.ContainsRune([]byte(valid), l.next()) { 77 | } 78 | l.backup() 79 | } 80 | 81 | // decode runs the decode until EOF 82 | func (l *decoder) decode() []byte { 83 | for l.state != nil { 84 | l.state = l.state(l) 85 | } 86 | return l.out.Bytes() 87 | } 88 | -------------------------------------------------------------------------------- /decode/gzip.go: -------------------------------------------------------------------------------- 1 | package decode 2 | 3 | import ( 4 | "bytes" 5 | "compress/gzip" 6 | "encoding/base64" 7 | "fmt" 8 | "io" 9 | "log" 10 | "strings" 11 | ) 12 | 13 | const gzipname = "gzip" 14 | 15 | // gzip takes an input string 16 | type gzipDec struct { 17 | input string 18 | } 19 | 20 | // NewGzipCodecC state machine to decompress a gzip compressed file 21 | func NewGzipCodecC(in string) CodecC { 22 | return &gzipDec{ 23 | input: in, 24 | } 25 | } 26 | 27 | // Name returns the name of the codec 28 | func (b *gzipDec) Name() string { 29 | return gzipname 30 | } 31 | 32 | // Decode a valid gzip compressed string 33 | func (b *gzipDec) Decode() (output string) { 34 | buf := new(bytes.Buffer) 35 | 36 | zr, err := gzip.NewReader(strings.NewReader(b.input)) 37 | 38 | if err == io.EOF { 39 | return "" 40 | } 41 | 42 | if err != nil { 43 | log.Fatal(err) 44 | } 45 | 46 | buf.WriteString(fmt.Sprintf("Name: %s\n", zr.Name)) 47 | 48 | if com := zr.Comment; com != "" { 49 | buf.WriteString(fmt.Sprintf("Comment: %s\n", com)) 50 | } 51 | 52 | if _, err := io.Copy(buf, zr); err != nil { 53 | return "Not a valid gzip compressed string" 54 | } 55 | 56 | if err := zr.Close(); err != nil { 57 | log.Fatal(err) 58 | } 59 | 60 | return buf.String() 61 | } 62 | 63 | // Encode compresses a string with gzip 64 | func (b *gzipDec) Encode() (output string) { 65 | var buf bytes.Buffer 66 | zw := gzip.NewWriter(&buf) 67 | 68 | zw.Name = "gzip" 69 | zw.Comment = "Compressed with Wapty" 70 | 71 | _, err := zw.Write([]byte(b.input)) 72 | if err != nil { 73 | log.Fatal(err) 74 | } 75 | 76 | err = zw.Close() 77 | if err != nil { 78 | log.Fatal(err) 79 | } 80 | 81 | return base64.StdEncoding.EncodeToString(buf.Bytes()) 82 | } 83 | 84 | // Check returns the probability a string is gzip compressed 85 | func (b *gzipDec) Check() (acceptability float64) { 86 | var c int 87 | tot := 8 88 | gzipID1 := 0x1f 89 | gzipID2 := 0x8b 90 | buf := []byte(b.input) 91 | 92 | if len(buf) < 10 { 93 | return 0. 94 | } 95 | 96 | // this is the first ID byte 97 | if buf[0] == byte(gzipID1) { 98 | c = c + 2 99 | } 100 | 101 | // this is the secondo ID byte 102 | if buf[1] == byte(gzipID2) { 103 | c = c + 2 104 | } 105 | 106 | // this is the compression method 107 | if buf[2] <= 8 { 108 | c = c + 2 109 | } 110 | 111 | // this is the flags 112 | if buf[3] < 32 { 113 | c++ 114 | } 115 | 116 | // this is the operating system 117 | if buf[9] <= 13 || buf[9] == 255 { 118 | c++ 119 | } 120 | return float64(c) / float64(tot) 121 | } 122 | -------------------------------------------------------------------------------- /decode/gzip_test.go: -------------------------------------------------------------------------------- 1 | package decode 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | var GzipDecodeTest = []struct { 8 | in string 9 | eOut string 10 | }{ 11 | { 12 | "H4sICAxCLFoAA3RvY29tcHJlc3MAc8vPd0os4gIA5QGj3QcAAAA=", 13 | "Name: tocompress\nFooBar\n", 14 | }, 15 | { 16 | "", 17 | "", 18 | }, 19 | } 20 | 21 | var GzipEncodeTest = []struct { 22 | in string 23 | eOut string 24 | }{ 25 | { 26 | "FooBar", 27 | "H4sIGAAAAAAA/2d6aXAAQ29tcHJlc3NlZCB3aXRoIFdhcHR5AHLLz3dKLAIEAAD//0NcF6EGAAAA", 28 | }, 29 | { 30 | // this does not return an emptu string since the title and the comment are set 31 | "", 32 | "H4sIGAAAAAAA/2d6aXAAQ29tcHJlc3NlZCB3aXRoIFdhcHR5AAEAAP//AAAAAAAAAAA=", 33 | }, 34 | } 35 | 36 | var GzipCheckTest = []struct { 37 | in string 38 | eOut float64 39 | }{ 40 | { 41 | // 1f8b08080c422c5a0003746f636f6d70726573730073cbcf774a2ce20200e501a3dd07000000139 42 | "H4sICAxCLFoAA3RvY29tcHJlc3MAc8vPd0os4gIA5QGj3QcAAAA=", 43 | 1, 44 | }, 45 | { 46 | "H4sI", 47 | 0, 48 | }, 49 | } 50 | 51 | func TestGzipDecode(t *testing.T) { 52 | for _, tt := range GzipDecodeTest { 53 | b64 := NewB64CodecC(tt.in) 54 | input := b64.Decode() 55 | d := NewGzipCodecC(input) 56 | out := d.Decode() 57 | if out != tt.eOut { 58 | t.Errorf("Expected decoded value: '%s' but got '%s'", tt.eOut, out) 59 | } 60 | } 61 | } 62 | 63 | func TestGzipEncode(t *testing.T) { 64 | for _, tt := range GzipEncodeTest { 65 | d := NewGzipCodecC(tt.in) 66 | out := d.Encode() 67 | if out != tt.eOut { 68 | t.Errorf("Expected decoded value: '%s' but got '%s'", tt.eOut, out) 69 | } 70 | } 71 | } 72 | 73 | func TestGzipCheck(t *testing.T) { 74 | for _, tt := range GzipCheckTest { 75 | b64 := NewB64CodecC(tt.in) 76 | input := b64.Decode() 77 | d := NewGzipCodecC(input) 78 | out := d.Check() 79 | if CompareFloat(out, tt.eOut, 0.1) != 0 { 80 | t.Errorf("Expected acceptability value: %f, but got %f", tt.eOut, out) 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /decode/init.go: -------------------------------------------------------------------------------- 1 | package decode 2 | 3 | import "github.com/empijei/wapty/cli" 4 | 5 | var cmdDecode = &cli.Cmd{ 6 | Name: "decode", 7 | Run: MainStandalone, 8 | UsageLine: "decode [flags]", 9 | Short: "decode something.", 10 | //FIXME write this, and add that this can read from pipe 11 | Long: `decode something in a really clever way: 12 | 13 | blah blah blah 14 | `, 15 | } 16 | 17 | var flagEncode bool // -encode 18 | var flagCodeclist string // -codec 19 | 20 | func init() { 21 | cmdDecode.Flag.BoolVar(&flagEncode, "encode", false, "Sets the decoder to an encoder instead") 22 | cmdDecode.Flag.StringVar(&flagCodeclist, "codec", "smart", 23 | `Sets the decoder/encoder codec. Multiple codecs can be specified and comma separated: 24 | they will be applied one on the output of the previous as in a pipeline. 25 | `) 26 | cli.AddCommand(cmdDecode) 27 | } 28 | -------------------------------------------------------------------------------- /decode/main.go: -------------------------------------------------------------------------------- 1 | package decode 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "strings" 8 | 9 | "github.com/empijei/cli/lg" 10 | ) 11 | 12 | // MainStandalone parses its own flag and it is the funcion to be run when using 13 | // `wapty decode`. This behaves as a main and expects the "decode" parameter to 14 | // be removed from os.Args. 15 | func MainStandalone(args ...string) { 16 | buf := takeInput(args) 17 | sequence := strings.Split(flagCodeclist, ",") 18 | for _, codec := range sequence { 19 | //This is to avoid printing twice the final result 20 | //if i < len(sequence)-2 { 21 | //fmt.Fprintln(os.Stderr, buf) 22 | //} 23 | if out, codecUsed, err := DecodeEncode(buf, flagEncode, codec); err == nil { 24 | lg.Debugf("Codec %s\n", codecUsed) 25 | lg.Infof("%s\n", out) 26 | buf = out 27 | } else { 28 | lg.Error(err.Error()) 29 | os.Exit(2) 30 | } 31 | } 32 | } 33 | 34 | // DecodeEncode takes an input string `buf` and decodes/encodes it (depending on the 35 | // `encode` parameter) with the given `codec`. It returns the encoded/decoded string 36 | // or an error if the process failed. 37 | func DecodeEncode(buf string, encode bool, codec string) (out string, codecUsed string, err error) { 38 | 39 | // Build list of available codecs 40 | var codecNames []string 41 | for _, cc := range codecs { 42 | codecNames = append(codecNames, cc.name) 43 | } 44 | codecNamesStr := strings.Join(codecNames, ", ") 45 | 46 | var c CodecC 47 | if codec == "smart" { 48 | if encode { 49 | err = fmt.Errorf("Cannot 'smart' encode, please specify a codec") 50 | return 51 | } 52 | c = SmartDecode(buf) 53 | } else { 54 | for _, cc := range codecs { 55 | if cc.name == codec { 56 | c = cc.codecCons(buf) 57 | } 58 | } 59 | if c == nil { 60 | err = fmt.Errorf("Codec not found: '%s'. Supported codecs are: %s\n", codec, codecNamesStr) 61 | return 62 | } 63 | } 64 | codecUsed = c.Name() 65 | if encode { 66 | out = c.Encode() 67 | } else { 68 | out = c.Decode() 69 | } 70 | return 71 | } 72 | 73 | func takeInput(args []string) string { 74 | stdininfo, err := os.Stdin.Stat() 75 | if err != nil { 76 | fmt.Fprintf(os.Stderr, "Error while connecting to stdin: %s\n", err.Error()) 77 | } 78 | if err == nil && stdininfo.Mode()&os.ModeCharDevice == 0 { 79 | //The input is a pipe, so I assume it is what I'm going to decode/encode 80 | fmt.Fprintln(os.Stderr, "Reading from stdin...") 81 | buf, err := ioutil.ReadAll(os.Stdin) 82 | if err != nil { 83 | fmt.Fprintf(os.Stderr, "Error while reading from stdin: %s\n", err.Error()) 84 | os.Exit(2) 85 | } 86 | return string(buf) 87 | } 88 | if len(args) == 0 { 89 | fmt.Fprintln(os.Stderr, "Didn't find anything to decode/encode, exiting...") 90 | os.Exit(2) 91 | } 92 | return args[0] 93 | } 94 | -------------------------------------------------------------------------------- /decode/main_test.go: -------------------------------------------------------------------------------- 1 | package decode 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | type expectation int 10 | 11 | const ( 12 | expectGood expectation = iota // expect a good result with no errors 13 | expectBad // expect a bad result with no errors 14 | expectErr // expect errors 15 | ) 16 | 17 | var testDecodeData = []struct { 18 | cFlag string 19 | in string 20 | eOut string 21 | expect expectation 22 | }{ 23 | { 24 | "b16", 25 | "666F6F626172", 26 | "foobar", 27 | expectGood, 28 | }, 29 | { 30 | "b16,b64", 31 | "5a6d3976", 32 | "foo", 33 | expectGood, 34 | }, 35 | { 36 | ",,", 37 | "5a6d3976", 38 | "", 39 | expectErr, 40 | }, 41 | { 42 | "", 43 | "5a6d3976", 44 | "", 45 | expectErr, 46 | }, 47 | { 48 | "smart,smart", 49 | "5a6d3976", 50 | "foo", 51 | expectGood, 52 | }, 53 | } 54 | 55 | func TestDecode(t *testing.T) { 56 | succFailStr := map[bool]string{ 57 | true: "good result", 58 | false: "bad result", 59 | } 60 | for _, test := range testDecodeData { 61 | wasErrRaised := false 62 | buf := test.in 63 | for _, codec := range strings.Split(test.cFlag, ",") { 64 | var err error 65 | buf, _, err = DecodeEncode(buf, false, codec) 66 | if err != nil { 67 | if test.expect != expectErr { 68 | t.Errorf("On call DecodeEncode('%s', false, '%s'):\n", buf, codec) 69 | t.Fatalf("Unexpected error while decoding: %s", err.Error()) 70 | } 71 | wasErrRaised = true 72 | } 73 | } 74 | if test.expect == expectErr { 75 | if !wasErrRaised { 76 | t.Errorf("Expected error on test decode(in='%s', codecs='%s') -> '%s', but got none", 77 | test.in, test.cFlag, test.eOut) 78 | } 79 | } else if (buf == test.eOut) != (test.expect == expectGood) { 80 | t.Errorf("Expected %s on test decode(in='%s', codecs='%s') -> '%s', but got %s", 81 | succFailStr[test.expect == expectGood], 82 | test.in, 83 | test.cFlag, 84 | test.eOut, 85 | succFailStr[buf == test.eOut], 86 | ) 87 | t.Errorf("\n\tout: '%s'\n\n\texpected: '%s'\n", buf, test.eOut) 88 | } 89 | } 90 | } 91 | 92 | var testGoodCodecData = []struct { 93 | in string 94 | codecsUsed []string 95 | }{ 96 | { 97 | "Zm9vYmFyIC1uCg==", 98 | []string{"b64"}, 99 | }, 100 | { 101 | "5a6d3976", 102 | []string{"b16", "b64"}, 103 | }, 104 | } 105 | 106 | func TestGoodCodec(t *testing.T) { 107 | for _, test := range testGoodCodecData { 108 | buf := test.in 109 | var codecUsed string 110 | var err error 111 | for _, expectedCodec := range test.codecsUsed { 112 | prevBuf := buf 113 | buf, codecUsed, err = DecodeEncode(buf, false, "smart") 114 | if err != nil { 115 | t.Errorf("On call DecodeEncode('%s', false, 'smart'):\n", buf) 116 | t.Fatalf("Unexpected error while decoding: %s", err.Error()) 117 | } 118 | if codecUsed != expectedCodec { 119 | t.Errorf("Decoding '%s': expected codec '%s' but got '%s'", 120 | prevBuf, expectedCodec, codecUsed) 121 | } 122 | } 123 | } 124 | } 125 | 126 | func TestDecodeStdin(t *testing.T) { 127 | // TODO 128 | } 129 | 130 | var testEncodeData = []struct { 131 | in string 132 | codecsUsed []string 133 | out string 134 | }{ 135 | { 136 | "foobar", 137 | []string{"b64"}, 138 | "Zm9vYmFy", 139 | }, 140 | { 141 | "asdasd", 142 | []string{"b16"}, 143 | "617364617364", 144 | }, 145 | { 146 | "wapty", 147 | []string{"b32"}, 148 | "O5QXA5DZ", 149 | }, 150 | { 151 | "asdasd", 152 | []string{"b16", "b32", "b64"}, 153 | "R1lZVE9NWldHUTNEQ05aVEdZMkE9PT09", 154 | }, 155 | { 156 | "asdasd", 157 | []string{"b16", "b16", "b16"}, 158 | "333633313337333333363334333633313337333333363334", 159 | }, 160 | { 161 | "asdasd", 162 | []string{"b32", "b32", "b32"}, 163 | "JJLEIRSVKYZEUTCGI5DESVCLKJEFKNSUGJIEUNKIKU6T2PJ5HU6Q====", 164 | }, 165 | { 166 | "asdasd", 167 | []string{"b64", "b64", "b64"}, 168 | "V1ZoT2ExbFlUbXM9", 169 | }, 170 | } 171 | 172 | func TestEncode(t *testing.T) { 173 | type step struct { 174 | codec string 175 | out string 176 | } 177 | for _, test := range testEncodeData { 178 | buf := test.in 179 | var err error 180 | var intermediate []step 181 | for _, codecUsed := range test.codecsUsed { 182 | buf, _, err = DecodeEncode(buf, true, codecUsed) 183 | intermediate = append(intermediate, step{codecUsed, buf}) 184 | if err != nil { 185 | t.Errorf("On call DecodeEncode('%s', true, '%s'):\n", buf, codecUsed) 186 | t.Fatalf("Unexpected error while encoding: %s", err.Error()) 187 | } 188 | } 189 | if buf != test.out { 190 | t.Errorf("Encoding '%s' with %s:\n\n\texpected\n\t\t'%s'\n\tbut got\n\t\t'%s'", 191 | test.in, strings.Join(test.codecsUsed, ","), test.out, buf) 192 | s := "Intermediate steps:\n" 193 | for _, stp := range intermediate { 194 | s += fmt.Sprintf("\tcodec: %s\n\tresult: %s\n", stp.codec, stp.out) 195 | } 196 | t.Errorf(s) 197 | } 198 | } 199 | } 200 | 201 | func TestEncodeStdin(t *testing.T) { 202 | // TODO 203 | } 204 | -------------------------------------------------------------------------------- /decode/tocompress: -------------------------------------------------------------------------------- 1 | FooBar 2 | -------------------------------------------------------------------------------- /decode/url.go: -------------------------------------------------------------------------------- 1 | package decode 2 | 3 | import ( 4 | "bytes" 5 | "net/url" 6 | ) 7 | 8 | const urlname = "url" 9 | 10 | // URL takes an input string 11 | type URL struct { 12 | input string 13 | } 14 | 15 | // NewURLCodecC state machine to smartly decode a string with invalid chars 16 | func NewURLCodecC(in string) CodecC { 17 | return &URL{ 18 | input: in, 19 | } 20 | } 21 | 22 | // Name returns the name of the codec 23 | func (b *URL) Name() string { 24 | return urlname 25 | } 26 | 27 | // Decode a valid url encoded string 28 | func (b *URL) Decode() (output string) { 29 | res, err := url.PathUnescape(b.input) 30 | if err != nil { 31 | return "Not a valid URL encoded string" 32 | } 33 | return res 34 | } 35 | 36 | // Encode a string to url encode 37 | func (b *URL) Encode() (output string) { 38 | return url.PathEscape(b.input) 39 | } 40 | 41 | // Check returns the percentage of valid url characters in the input string 42 | func (b *URL) Check() (acceptability float64) { 43 | var c int 44 | var tot int 45 | for pos, char := range b.input { 46 | tot++ 47 | if bytes.ContainsRune([]byte("%"), char) && pos < len(b.input)+2 { 48 | if bytes.ContainsAny([]byte(b16Alphabet), string(b.input[pos+1])) && 49 | bytes.ContainsAny([]byte(b16Alphabet), string(b.input[pos+2])) { 50 | c++ 51 | } 52 | } 53 | if bytes.ContainsRune([]byte(b64Alphabet), char) { 54 | c++ 55 | } 56 | } 57 | return float64(c) / float64(tot) 58 | } 59 | -------------------------------------------------------------------------------- /decode/url_test.go: -------------------------------------------------------------------------------- 1 | package decode 2 | 3 | import "testing" 4 | 5 | var URLDecodeTest = []struct { 6 | in string 7 | eOut string 8 | }{ 9 | { 10 | "foo%20bar", 11 | "foo bar", 12 | }, 13 | { 14 | "%C3%BCnter", 15 | "ünter", 16 | }, 17 | { 18 | "", 19 | "", 20 | }, 21 | } 22 | 23 | var URLEncodeTest = []struct { 24 | in string 25 | eOut string 26 | }{ 27 | { 28 | "foo bar", 29 | "foo%20bar", 30 | }, 31 | { 32 | "ünter", 33 | "%C3%BCnter", 34 | }, 35 | { 36 | "", 37 | "", 38 | }, 39 | } 40 | 41 | var URLCheckTest = []struct { 42 | in string 43 | eOut float64 44 | }{ 45 | { 46 | "foo%20ba", 47 | 1, 48 | }, 49 | { 50 | "foo%2Xba", 51 | 0.875, 52 | }, 53 | { 54 | "foo%2", 55 | 0.8, 56 | }, 57 | { 58 | "", 59 | 0.0, 60 | }, 61 | } 62 | 63 | func TestURLDecode(t *testing.T) { 64 | for _, tt := range URLDecodeTest { 65 | d := NewURLCodecC(tt.in) 66 | out := d.Decode() 67 | if out != tt.eOut { 68 | t.Errorf("Expected decoded value: '%s' but got '%s'", tt.eOut, out) 69 | } 70 | } 71 | } 72 | 73 | func TestURLEncode(t *testing.T) { 74 | for _, tt := range URLEncodeTest { 75 | d := NewURLCodecC(tt.in) 76 | out := d.Encode() 77 | if out != tt.eOut { 78 | t.Errorf("Expected encoded value: '%s' but got '%s'", tt.eOut, out) 79 | } 80 | } 81 | } 82 | 83 | func TestURLCheck(t *testing.T) { 84 | for _, tt := range Base16CheckTest { 85 | d := NewURLCodecC(tt.in) 86 | out := d.Check() 87 | if CompareFloat(out, tt.eOut, 0.1) != 0 { 88 | t.Errorf("Expected check value of '%f' but got '%f'", tt.eOut, out) 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /documentation/InterceptSequenceDiagram.sdedit: -------------------------------------------------------------------------------- 1 | c:ExternalClient 2 | irw:interceptRequestWrapper 3 | mp:mitm.Proxy 4 | rsq:RequestQueue 5 | dp:dispatchLoop 6 | mt:ModifiedTransport 7 | rpq:ResponseQueue 8 | s:ExternalServer 9 | 10 | c:mp.Original Request 11 | mp:Edited/Original Request=irw.Original Request 12 | irw: 13 | irw:Add ID header to Request 14 | irw:Save request 15 | [c:alt intercept is true] 16 | irw:Add Intercepted header to reques 17 | irw: 18 | irw:Edited Request / nil=rsq.OriginalRequest 19 | rsq:Edited Request / nil=dp.Original Request 20 | dp:User Edits Request and/or forwards 21 | irw:Save Edited Request if not nil 22 | [/c] 23 | mp:Edited/Original Response=mt.Request 24 | mt: 25 | mt:Remove Wapty headers 26 | mt:Original Response =s.Request 27 | [c:alt request was intercepted] 28 | mt:Edited Response/nil=rpq.Original Response 29 | rpq:Edited Response/nil=dp.Original Response 30 | dp: 31 | dp:User Edits Response and/or forwards 32 | mt: 33 | mt:Save Edited Response if not nil 34 | [/c] 35 | mp:c.Edited / Original Response 36 | 37 | 38 | 39 | 40 | ExternalClient:w->mitmProxy[label="1 Original Request", constraint=false]; 41 | mitmProxy -> interceptRequestWrapper[label="2 Original Request", constraint=false]; 42 | interceptRequestWrapper -> RequestQueue[label="3 if intercept\nOriginal Request", constraint=false]; 43 | RequestQueue -> dispatchLoop[label="4 Original Request", constraint=false]; 44 | dispatchLoop -> interceptRequestWrapper[label="5 Edited Request", constraint=false]; 45 | interceptRequestWrapper -> mitmProxy[label="6 Edited? Request"]; 46 | mitmProxy -> modifiedTransport[label="7 Request"]; 47 | modifiedTransport -> ExternalServer[label="8 Request"]; 48 | ExternalServer -> modifiedTransport[label="9 Original Response", constraint=false]; 49 | modifiedTransport -> ResponseQueue[label = "10 if req was intercepted\nsends Original Response", constraint=false]; 50 | ResponseQueue -> dispatchLoop[label = "11 Original Response", constraint=false]; 51 | dispatchLoop -> modifiedTransport[label = "12 Edited Response", constraint=false]; 52 | modifiedTransport -> mitmProxy[label = "13 Edited? Response", constraint=false]; 53 | mitmProxy ->ExternalClient:e[label = "14 Edited? Response", constraint=false]; 54 | 55 | -------------------------------------------------------------------------------- /documentation/README.md: -------------------------------------------------------------------------------- 1 | THIS IS OUT OF DATE, IGNORE PLS 2 | -------------------------------------------------------------------------------- /documentation/informationFlow.dot: -------------------------------------------------------------------------------- 1 | digraph{ 2 | rankdir=LR; 3 | // main -> interceptMainloop[label = "executes", constraint=false]; 4 | // interceptMainloop -> dispatchLoop[label = "spawns", constraint=false]; 5 | // interceptMainloop -> modifiedTransport[label = "creates", constraint=false]; 6 | // interceptMainloop -> mitmProxy[label = "creates", constraint=false]; 7 | // mitmProxy -> modifiedTransport[label = "uses", constraint=false]; 8 | ExternalClient:w->mitmProxy[label="1 Original Request", constraint=false]; 9 | mitmProxy -> interceptRequestWrapper[label="2 Original Request", constraint=false]; 10 | interceptRequestWrapper -> RequestQueue[label="3 if intercept\nOriginal Request", constraint=false]; 11 | RequestQueue -> dispatchLoop[label="4 Original Request", constraint=false]; 12 | dispatchLoop -> interceptRequestWrapper[label="5 Edited Request", constraint=false]; 13 | interceptRequestWrapper -> mitmProxy[label="6 Edited? Request"]; 14 | mitmProxy -> modifiedTransport[label="7 Request"]; 15 | modifiedTransport -> ExternalServer[label="8 Request"]; 16 | ExternalServer -> modifiedTransport[label="9 Original Response", constraint=false]; 17 | modifiedTransport -> ResponseQueue[label = "10 if req was intercepted\nsends Original Response", constraint=false]; 18 | ResponseQueue -> dispatchLoop[label = "11 Original Response", constraint=false]; 19 | dispatchLoop -> modifiedTransport[label = "12 Edited Response", constraint=false]; 20 | modifiedTransport -> mitmProxy[label = "13 Edited? Response", constraint=false]; 21 | mitmProxy ->ExternalClient:e[label = "14 Edited? Response", constraint=false]; 22 | 23 | {rank=same modifiedTransport } 24 | 25 | {rank=same mitmProxy dispatchLoop ResponseQueue RequestQueue ExternalClient} 26 | } 27 | -------------------------------------------------------------------------------- /documentation/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/empijei/wapty/22661afbca5b7baf0859469ebb4959206f3d14ba/documentation/screenshot.png -------------------------------------------------------------------------------- /fuzz/json.go: -------------------------------------------------------------------------------- 1 | package fuzz 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | ) 7 | 8 | func nestedJSON(level int, fieldname string) (json string) { 9 | //TODO escape " 10 | var open = fmt.Sprintf("{\"%s\":", fieldname) 11 | outbuf := bytes.NewBuffer(nil) 12 | for i := 0; i < level; i++ { 13 | outbuf.WriteString(open) 14 | } 15 | outbuf.WriteString("{}") 16 | for i := 0; i < level; i++ { 17 | outbuf.WriteString("}") 18 | } 19 | return string(outbuf.Bytes()) 20 | } 21 | -------------------------------------------------------------------------------- /fuzz/json_test.go: -------------------------------------------------------------------------------- 1 | package fuzz 2 | 3 | import "testing" 4 | 5 | var nestedTests = []struct { 6 | l int 7 | n string 8 | exp string 9 | }{ 10 | { 11 | 1, 12 | "a", 13 | "{\"a\":{}}", 14 | }, 15 | } 16 | 17 | func TestNestedJSON(t *testing.T) { 18 | for _, tt := range nestedTests { 19 | out := nestedJSON(tt.l, tt.n) 20 | if out != tt.exp { 21 | t.Errorf("nestedJSON(%d,%s) expected %s but got %s", tt.l, tt.n, tt.exp, out) 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /fuzz/main.go: -------------------------------------------------------------------------------- 1 | package fuzz 2 | 3 | // MainStandalone parses its own flag and it is the funcion to be run when using 4 | // `wapty decode`. This behaves as a main and expects the first parameter, the directive, 5 | // to be removed from os.Args. 6 | func MainStandalone() { 7 | //TODO 8 | } 9 | -------------------------------------------------------------------------------- /intercept/common_test.go: -------------------------------------------------------------------------------- 1 | package intercept 2 | 3 | import "github.com/empijei/wapty/ui/apis" 4 | 5 | type MockSubscription struct { 6 | ID int64 7 | Channel string 8 | DataCh chan apis.Command 9 | SentStuff []*apis.Command 10 | } 11 | 12 | func (s *MockSubscription) Receive() apis.Command { 13 | return <-s.DataCh 14 | } 15 | func (s *MockSubscription) RecChannel() <-chan apis.Command { 16 | return s.DataCh 17 | } 18 | 19 | //Sends the command and sets the channel with the value set in the subscription 20 | func (s *MockSubscription) Send(c *apis.Command) { 21 | s.SentStuff = append(s.SentStuff, c) 22 | } 23 | -------------------------------------------------------------------------------- /intercept/editorActions.go: -------------------------------------------------------------------------------- 1 | package intercept 2 | 3 | import ( 4 | "github.com/empijei/cli/lg" 5 | "github.com/empijei/wapty/ui" 6 | "github.com/empijei/wapty/ui/apis" 7 | ) 8 | 9 | var uiEditor ui.Subscription 10 | 11 | func init() { 12 | uiEditor = ui.Subscribe(apis.CHN_EDITOR) 13 | } 14 | 15 | //Invokes the edit action on the proxy ui. When a response is received returns 16 | //the payload and the action in its string form. It does not attempt to validate 17 | //the action, the caller must take care of it. 18 | func editBuffer(p string, b []byte, endpoint string) ([]byte, apis.Action) { 19 | if !intercept.value() { 20 | return nil, apis.EDT_FORWARD 21 | } 22 | lg.Debugf("Editing: %s", p) 23 | args := map[apis.ArgName]string{ 24 | apis.ARG_PAYLOADTYPE: p, 25 | apis.ARG_ENDPOINT: endpoint} 26 | uiEditor.Send(&apis.Command{Action: apis.EDT_EDIT, Args: args, Payload: b}) 27 | lg.Debug("Waiting for user interaction") 28 | result := uiEditor.Receive() 29 | lg.Debug("User interacted") 30 | return result.Payload, result.Action 31 | } 32 | -------------------------------------------------------------------------------- /intercept/editorActions_test.go: -------------------------------------------------------------------------------- 1 | package intercept 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/empijei/wapty/ui/apis" 8 | ) 9 | 10 | type paramsType struct { 11 | p string 12 | cmd apis.Command 13 | b []byte 14 | } 15 | 16 | type outputType struct { 17 | b []byte 18 | e apis.Action 19 | } 20 | 21 | var editBufferTests = []struct { 22 | in paramsType 23 | out outputType 24 | }{ 25 | //TODO 26 | } 27 | 28 | func TestEditBuffer(t *testing.T) { 29 | mockChan := make(chan apis.Command) 30 | uiEditor = &MockSubscription{DataCh: mockChan} 31 | //uiEditor = &ui.SubscriptionImpl{ 32 | //dataCh: mockChan, 33 | //} 34 | defer func() { 35 | uiEditor = nil 36 | close(mockChan) 37 | }() 38 | for i, tt := range editBufferTests { 39 | go func() { 40 | mockChan <- tt.in.cmd 41 | }() 42 | b, e := editBuffer(tt.in.p, tt.in.b, "https://thisisatest.com:443") 43 | if bytes.Compare(b, tt.out.b) != 0 || e != tt.out.e { 44 | t.Errorf("editBufferTests[%d]", i) 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /intercept/history.go: -------------------------------------------------------------------------------- 1 | package intercept 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | "net/http" 7 | "net/http/httputil" 8 | "sync" 9 | 10 | "github.com/empijei/cli/lg" 11 | "github.com/empijei/wapty/ui/apis" 12 | ) 13 | 14 | var status History 15 | 16 | func init() { 17 | } 18 | 19 | //History is used to represent all the req/resp that went through the proxy 20 | //FIXME!!! implement high-level methods!!! 21 | //FIXME make the fields private and create a dummy object to transmit this 22 | type History struct { 23 | sync.RWMutex `json:"-"` 24 | //Remove count, use it only for serialization 25 | Count int 26 | ReqResps []*ReqResp 27 | } 28 | 29 | //Finds the correct Request based on the ID and adds the modified request to it 30 | //This is thread safe 31 | func (h *History) addRawEditedRequest(ID int, rawEditedReq []byte) { 32 | h.RLock() 33 | h.ReqResps[ID].RawEditedReq = rawEditedReq 34 | h.RUnlock() 35 | } 36 | 37 | //func (h *History) addEditedRequest(ID int, req *http.Request) { 38 | //} 39 | 40 | //Finds the correct Request based on the ID and adds the original response to it 41 | //This is thread safe 42 | func (h *History) addRawResponse(ID int, rawRes []byte) { 43 | h.RLock() 44 | h.ReqResps[ID].RawRes = rawRes 45 | h.RUnlock() 46 | } 47 | 48 | func (h *History) addResponse(ID int, res *http.Response) { 49 | tmp, err := httputil.DumpResponse(res, true) 50 | if err != nil { 51 | //TODO 52 | lg.Error(err) 53 | } 54 | h.addRawResponse(ID, tmp) 55 | } 56 | 57 | // Finds the correct Request based on the ID and adds the modified response to it 58 | // This is thread safe 59 | func (h *History) addRawEditedResponse(ID int, rawEditedRes []byte) { 60 | h.RLock() 61 | h.ReqResps[ID].RawEditedRes = rawEditedRes 62 | h.RUnlock() 63 | } 64 | 65 | //func (h *History) addEditedResponse(ID int, res *http.Response) {} 66 | 67 | // StatusDump dumps the status in the log. This is only meant for debug purposes. 68 | func StatusDump(status *History) { 69 | status.RLock() 70 | foo, err := json.MarshalIndent(status, " ", " ") 71 | if err != nil { 72 | lg.Error(err) 73 | } 74 | lg.Info(foo) 75 | status.RUnlock() 76 | } 77 | 78 | func (h *History) getItem(ID int) *ReqResp { 79 | h.RLock() 80 | defer h.RUnlock() 81 | if ID < h.Count { 82 | return h.ReqResps[ID] 83 | } 84 | return nil 85 | } 86 | 87 | //This loop will wait for commands directed to the history control and will 88 | //execute them 89 | func historyLoop() { 90 | for { 91 | select { 92 | case cmd := <-uiHistory.RecChannel(): 93 | switch cmd.Action { 94 | case apis.HST_DUMP: 95 | status.RLock() 96 | dump, err := json.Marshal(status) 97 | status.RUnlock() 98 | if err != nil { 99 | StatusDump(&status) 100 | panic(err) 101 | } 102 | lg.Debugf("Dump: %s", dump) 103 | uiHistory.Send(&apis.Command{Action: "Dump", Payload: dump}) 104 | case apis.HST_FETCH: 105 | uiHistory.Send(handleFetch(cmd)) 106 | } 107 | case <-done: 108 | return 109 | } 110 | } 111 | } 112 | 113 | func handleFetch(cmd apis.Command) *apis.Command { 114 | var ID int 115 | err := cmd.UnpackArgs([]apis.ArgName{apis.ARG_ID}, &ID) 116 | if err != nil { 117 | lg.Error(err) 118 | return apis.Err(err) 119 | } 120 | lg.Debug("Requested history entry") 121 | rr := status.getItem(ID) 122 | buf, _ := json.Marshal(rr) 123 | return &apis.Command{Action: apis.HST_FETCH, Payload: buf} 124 | } 125 | 126 | //ReqResp represents an item of the proxy history 127 | //TODO methods to parse req-resp 128 | //TODO create a test that fails if this is different from apis.ReqResp 129 | type ReqResp struct { 130 | //Unique ID in the history 131 | ID int 132 | //Meta Data about both Req and Resp 133 | MetaData *apis.ReqRespMetaData 134 | //Original Request 135 | RawReq []byte 136 | //Original Response 137 | RawRes []byte 138 | //Edited Request 139 | RawEditedReq []byte 140 | //Edited Response 141 | RawEditedRes []byte 142 | } 143 | 144 | //Creates a new history item and safely adds it to the status, incrementing the 145 | //current id value 146 | //Returns the id of the newly created item 147 | func newRawReqResp(rawReq []byte) int { 148 | //lg.Info("Locking status for write") 149 | status.Lock() 150 | //lg.Info("Locked") 151 | curReq := status.Count 152 | tmp := &ReqResp{RawReq: rawReq, ID: curReq, MetaData: &apis.ReqRespMetaData{ID: curReq}} 153 | status.ReqResps = append(status.ReqResps, tmp) 154 | status.Count++ 155 | //lg.Info("UnLocking status") 156 | status.Unlock() 157 | return curReq 158 | } 159 | 160 | func newReqResp(req *http.Request) int { 161 | tmp, err := httputil.DumpRequest(req, true) 162 | if err != nil { 163 | //TODO 164 | lg.Error(err.Error()) 165 | } 166 | return newRawReqResp(tmp) 167 | } 168 | 169 | //represents an *http.Request if err == nil, represents the error otherwise. 170 | type mayBeRequest struct { 171 | req *http.Request 172 | res *http.Response 173 | err error 174 | } 175 | 176 | //a struct used to transmit to the dispatchLoop a requests that waits to be 177 | //edited or forwarded by the user 178 | type pendingRequest struct { 179 | id int 180 | intercepted bool 181 | originalRequest *http.Request 182 | modifiedRequest chan *mayBeRequest 183 | } 184 | 185 | //represents an *http.Response if err == nil, represents the error otherwise. 186 | type mayBeResponse struct { 187 | res *http.Response 188 | err error 189 | } 190 | 191 | //a struct used to transmit to the dispatchLoop a response that waits to be 192 | //edited or forwarded by the user 193 | type pendingResponse struct { 194 | id int 195 | originalResponse *http.Response 196 | originalRequest *http.Request 197 | modifiedResponse chan *mayBeResponse 198 | } 199 | 200 | // Save saves the status in a json formatted stream 201 | func (h *History) Save(out io.Writer) error { 202 | h.RLock() 203 | defer h.RUnlock() 204 | 205 | enc := json.NewEncoder(out) 206 | err := enc.Encode(h) 207 | return err 208 | } 209 | 210 | // Load loads the status from a json formatted stream 211 | func (h *History) Load(in io.Reader) error { 212 | var tmp History 213 | dec := json.NewDecoder(in) 214 | err := dec.Decode(&tmp) 215 | 216 | if err != nil { 217 | return err 218 | } 219 | 220 | h.Lock() 221 | defer h.Unlock() 222 | 223 | h.ReqResps = tmp.ReqResps 224 | h.Count = tmp.Count 225 | return nil 226 | } 227 | 228 | // String returns the name of the current package/project 229 | func (h *History) String() string { 230 | return "Intercept" 231 | } 232 | 233 | // GetStatus returns the current status 234 | func GetStatus() *History { 235 | return &status 236 | } 237 | -------------------------------------------------------------------------------- /intercept/historyActions.go: -------------------------------------------------------------------------------- 1 | package intercept 2 | 3 | import ( 4 | "github.com/empijei/wapty/ui" 5 | "github.com/empijei/wapty/ui/apis" 6 | ) 7 | 8 | var uiHistory ui.Subscription 9 | 10 | func init() { 11 | uiHistory = ui.Subscribe(apis.CHN_HISTORY) 12 | } 13 | -------------------------------------------------------------------------------- /intercept/historyMetaData.go: -------------------------------------------------------------------------------- 1 | package intercept 2 | 3 | import ( 4 | "encoding/json" 5 | "net" 6 | "net/http" 7 | "strings" 8 | "time" 9 | 10 | "github.com/empijei/cli/lg" 11 | "github.com/empijei/wapty/ui/apis" 12 | ) 13 | 14 | //DISCLAIMER use original req AFTER editing the new one 15 | //And use it from a thread that has a readlock on the status 16 | func (rr *ReqResp) parseRequest(req *http.Request) { 17 | this := rr.MetaData 18 | this.Host = req.Host 19 | this.Method = req.Method 20 | this.Path = req.URL.Path 21 | //TODO implement this in a way that does not consume the body 22 | //if len(req.Form) == 0 { 23 | // _ = req.ParseForm() 24 | //} 25 | //this.Params = len(req.Form) > 0 26 | //this supposes to alread have a RLock on the status. 27 | this.Edited = status.ReqResps[this.ID].RawEditedReq != nil 28 | tmp := strings.Split(this.Path, ".") 29 | if !strings.Contains(tmp[len(tmp)-1], "/") { 30 | this.Extension = tmp[len(tmp)-1] 31 | } 32 | ipport := strings.Split(this.Host, ":") 33 | ips, err := net.LookupHost(ipport[0]) 34 | if err == nil && len(ips) >= 1 { 35 | this.IP = ips[0] 36 | if len(ipport) >= 2 { 37 | this.Port = ipport[1] 38 | } else { 39 | switch req.URL.Scheme { 40 | case "https": 41 | this.Port = "443" 42 | case "http": 43 | this.Port = "80" 44 | default: 45 | lg.Infof("Port not specified: %s\n", this.Host) 46 | } 47 | } 48 | } else { 49 | lg.Error("Unable to resolve Host: %s\n", this.Host) 50 | } 51 | this.Time = time.Now().String() 52 | sendMetaData(this) 53 | } 54 | 55 | //DISCLAIMER use original res AFTER editing the new one 56 | //And use it from a thread that has a readlock on the status 57 | func (rr *ReqResp) parseResponse(res *http.Response) { 58 | this := rr.MetaData 59 | if !this.Edited { 60 | //this supposes to alread have a RLock on the status. 61 | this.Edited = status.ReqResps[this.ID].RawEditedRes != nil 62 | } 63 | this.Status = res.Status 64 | //FIXME 65 | this.Length = res.ContentLength 66 | this.ContentType = res.Header.Get("Content-Type") 67 | this.TLS = res.TLS != nil 68 | tmp := res.Cookies() 69 | for _, cookie := range tmp { 70 | this.Cookies += cookie.String() + "; " 71 | } 72 | sendMetaData(this) 73 | } 74 | 75 | func sendMetaData(metaData *apis.ReqRespMetaData) { 76 | metaJSON, err := json.Marshal(metaData) 77 | if err != nil { 78 | lg.Error(err) 79 | } 80 | uiHistory.Send(&apis.Command{Action: apis.HST_METADATA, Args: map[apis.ArgName]string{apis.HST_METADATA: string(metaJSON)}}) 81 | } 82 | -------------------------------------------------------------------------------- /intercept/historyMetaData_test.go: -------------------------------------------------------------------------------- 1 | package intercept 2 | -------------------------------------------------------------------------------- /intercept/history_test.go: -------------------------------------------------------------------------------- 1 | package intercept 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/empijei/wapty/ui/apis" 8 | ) 9 | 10 | /* 11 | functions 12 | -historyLoop() 13 | -newReqResp(rawReq []byte) : uint 14 | */ 15 | 16 | func genDummyStatus() *History { 17 | var st History 18 | metadata := &apis.ReqRespMetaData{ 19 | ID: 0, 20 | Host: "host", 21 | Method: "method", 22 | Path: "path", 23 | Params: true, 24 | Edited: true, 25 | Status: "status", 26 | Length: 42, 27 | ContentType: "content type", 28 | Extension: "extension", 29 | TLS: true, 30 | IP: "ip", 31 | Port: "port", 32 | Cookies: "cookie", 33 | Time: "time", 34 | } 35 | 36 | rr := []*ReqResp{{ 37 | ID: 0, 38 | MetaData: metadata, 39 | RawReq: []byte("raw request"), 40 | RawRes: []byte("ras response"), 41 | RawEditedReq: []byte("raw edited request"), 42 | RawEditedRes: []byte("ras edited response"), 43 | }} 44 | 45 | st.Count = 1 46 | st.ReqResps = rr 47 | 48 | return &st 49 | } 50 | 51 | var expected = []byte(`{"Count":1,"ReqResps":[{"ID":0,"MetaData":{"ID":0,"Host":"host","Method":"method","Path":"path","Params":true,"Edited":true,"Status":"status","Length":42,"ContentType":"content type","Extension":"extension","TLS":true,"IP":"ip","Port":"port","Cookies":"cookie","Time":"time"},"RawReq":"cmF3IHJlcXVlc3Q=","RawRes":"cmFzIHJlc3BvbnNl","RawEditedReq":"cmF3IGVkaXRlZCByZXF1ZXN0","RawEditedRes":"cmFzIGVkaXRlZCByZXNwb25zZQ=="}]} 52 | `) 53 | 54 | func TestSave(t *testing.T) { 55 | st := genDummyStatus() 56 | 57 | b := bytes.NewBuffer(nil) 58 | err := st.Save(b) 59 | if err != nil { 60 | t.Error(err) 61 | } 62 | 63 | if bytes.Compare(b.Bytes(), expected) != 0 { 64 | t.Errorf("History save failed: expected \n<%s>\nbut got \n<%s>", string(expected), string(b.Bytes())) 65 | } 66 | } 67 | 68 | func TestLoad(t *testing.T) { 69 | in := bytes.NewBuffer(expected) 70 | var st History 71 | 72 | err := st.Load(in) 73 | if err != nil { 74 | t.Error(err) 75 | } 76 | 77 | out := bytes.NewBuffer(nil) 78 | err = st.Save(out) 79 | if err != nil { 80 | t.Error(err) 81 | } 82 | 83 | if bytes.Compare(out.Bytes(), expected) != 0 { 84 | t.Errorf("History load failed: expected \n<%s>\nbut got \n<%s>", string(expected), string(out.Bytes())) 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /intercept/interceptHop.go: -------------------------------------------------------------------------------- 1 | package intercept 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/empijei/cli/lg" 7 | ) 8 | 9 | //Remove trailers? 10 | //https://github.com/squid-cache/squid/blob/master/src/http/RegisteredHeadersHash.cci 11 | 12 | // HopByHopHeaders is a list of all the HTTP headers that are stripped away by 13 | // the proxy 14 | var HopByHopHeaders = []string{ 15 | "Content-Encoding", 16 | "Connection", 17 | "TE", 18 | "HTTP2-Settings", 19 | "Keep-Alive", 20 | "Proxy-Authenticate", 21 | "Proxy-Connection", 22 | "Proxy-Authorization", 23 | "Trailer", 24 | "Upgrade", 25 | "Transfer-Encoding", 26 | "Alternate-Protocol", 27 | "X-Forwarded-For", 28 | "Proxy-Connection", 29 | } 30 | 31 | func stripHTHHeaders(h *http.Header) { 32 | for _, header := range HopByHopHeaders { 33 | h.Del(header) 34 | } 35 | } 36 | 37 | // Interceptor is a struct that respects the net.RoundTripper interface and just wraps 38 | // the original http.RoundTripper 39 | type Interceptor struct { 40 | wrappedRT http.RoundTripper 41 | } 42 | 43 | // RoundTrip is a mock RoundTrip used to intercept requests and responses 44 | // before they are forwarded by the proxy. 45 | func (ri *Interceptor) RoundTrip(req *http.Request) (res *http.Response, err error) { 46 | //This first part is dedicated to the REQUESTS 47 | intercepted := intercept.value() 48 | backUpURL := req.URL 49 | req, ID, err := preProcessRequest(req) 50 | if err != nil { 51 | //TODO handle possible autodrop 52 | //TODO other errors 53 | lg.Error(err) 54 | } 55 | if intercepted { 56 | var editedReq *http.Request 57 | editedReq, res, err = editRequest(req, ID) 58 | if err != nil { 59 | //TODO 60 | lg.Error(err) 61 | } 62 | if editedReq != nil { 63 | req = editedReq 64 | req.URL.Scheme = backUpURL.Scheme 65 | req.URL.Host = backUpURL.Host 66 | } 67 | } 68 | 69 | status.RLock() 70 | status.ReqResps[ID].parseRequest(req) 71 | status.RUnlock() 72 | if res != nil { 73 | //TODO Adding dropped responses could be avoided. 74 | status.addResponse(ID, res) 75 | return 76 | } 77 | 78 | //This second part works on the RESPONSES 79 | //Perform the request, but disable compressing. 80 | //The gzip encoding should be used by the http package transparently 81 | req.Header.Del("Accept-Encoding") 82 | res, err = ri.wrappedRT.RoundTrip(req) 83 | if err != nil { 84 | lg.Error("Something went wrong trying to contact the server") 85 | //TODO return a fake response containing the error message 86 | res = GenerateResponse("Error", "Error in performing the request: "+err.Error(), 500) 87 | return 88 | } 89 | res = preProcessResponse(req, res, ID) 90 | if intercepted { 91 | res, err = editResponse(req, res, ID) 92 | } 93 | status.RLock() 94 | status.ReqResps[ID].parseResponse(res) 95 | status.RUnlock() 96 | return 97 | } 98 | -------------------------------------------------------------------------------- /intercept/interceptHop_test.go: -------------------------------------------------------------------------------- 1 | package intercept 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | "net/http" 7 | "net/url" 8 | "testing" 9 | ) 10 | 11 | type mockRT struct { 12 | //This is filled with the interceptor req 13 | req *http.Request 14 | //Mock return values 15 | err error 16 | res *http.Response 17 | } 18 | 19 | func (mr *mockRT) RoundTrip(req *http.Request) (res *http.Response, err error) { 20 | defer func() { mr.res = nil }() 21 | mr.req = req 22 | res, err = mr.res, mr.err 23 | return 24 | } 25 | 26 | func mockhandleResponse(presp *pendingResponse) {} 27 | 28 | var rtTests = []struct { 29 | //Input data 30 | in *http.Request 31 | subj mockRT 32 | interceptStatus bool 33 | reqModifier func() 34 | resModifier func() 35 | //Expected values 36 | eEditedRequest *http.Request 37 | eOut *http.Response 38 | eError error 39 | }{ 40 | //TODO 41 | { 42 | in: &http.Request{URL: &url.URL{}, Header: http.Header{}}, 43 | subj: mockRT{res: &http.Response{ContentLength: 3, Body: ioutil.NopCloser(bytes.NewReader([]byte(`foo`)))}}, 44 | interceptStatus: false, 45 | eEditedRequest: &http.Request{URL: &url.URL{}}, 46 | eOut: &http.Response{ContentLength: 3, Body: ioutil.NopCloser(bytes.NewReader([]byte(`foo`)))}, 47 | }, 48 | { 49 | in: &http.Request{URL: &url.URL{}, Header: http.Header{}}, 50 | subj: mockRT{res: &http.Response{ContentLength: 3, Body: ioutil.NopCloser(bytes.NewReader([]byte(`foo`)))}}, 51 | interceptStatus: true, 52 | eEditedRequest: &http.Request{URL: &url.URL{}}, 53 | eOut: &http.Response{ContentLength: 3, Body: ioutil.NopCloser(bytes.NewReader([]byte(`foo`)))}, 54 | }, 55 | } 56 | 57 | func TestRoundTrip(t *testing.T) { 58 | for i, tt := range rtTests { 59 | ri := &Interceptor{wrappedRT: &tt.subj} 60 | intercept.setValue(tt.interceptStatus) 61 | if intercept.value() { 62 | if tt.reqModifier != nil { 63 | go tt.reqModifier() 64 | } else { 65 | go func() { 66 | p := <-RequestQueue 67 | p.modifiedRequest <- &mayBeRequest{req: p.originalRequest} 68 | }() 69 | } 70 | 71 | if tt.resModifier != nil { 72 | go tt.resModifier() 73 | } else { 74 | go func() { 75 | p := <-ResponseQueue 76 | p.modifiedResponse <- &mayBeResponse{res: p.originalResponse} 77 | }() 78 | } 79 | } 80 | out, err := ri.RoundTrip(tt.in) 81 | if err != tt.eError { 82 | t.Errorf("Test %d failed, errors differ: wanted %v got %v", i, tt.eError, err) 83 | } 84 | if pass := reqEqual(tt.subj.req, tt.eEditedRequest); !pass { 85 | t.Errorf("Test %d failed, requests differ: wanted \n%+v\n got \n%+v", i, tt.subj.req, tt.eEditedRequest) 86 | } 87 | if pass, jout, jeout := jsonEqual(out, tt.eOut); !pass { 88 | t.Errorf("Test %d failed, responses differ: wanted \n%s\n got \n%s", i, jeout, jout) 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /intercept/intercept_test.go: -------------------------------------------------------------------------------- 1 | package intercept 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | "os" 7 | "testing" 8 | ) 9 | 10 | func setup() { 11 | } 12 | 13 | func shutdown() {} 14 | 15 | func TestMain(m *testing.M) { 16 | setup() 17 | code := m.Run() 18 | shutdown() 19 | os.Exit(code) 20 | } 21 | 22 | func jsonEqual(a interface{}, b interface{}) (equal bool, as string, bs string) { 23 | buf, err := json.MarshalIndent(a, " ", " ") 24 | if err != nil { 25 | panic(err) 26 | } 27 | as = string(buf) 28 | buf, err = json.MarshalIndent(b, " ", " ") 29 | if err != nil { 30 | panic(err) 31 | } 32 | bs = string(buf) 33 | equal = as == bs 34 | return 35 | } 36 | 37 | func reqEqual(a *http.Request, b *http.Request) bool { 38 | //TODO IMPLEMENT THIS but only on exported fields 39 | //return reflect.DeepEqual(a, b) 40 | return true 41 | } 42 | -------------------------------------------------------------------------------- /intercept/plugin.go: -------------------------------------------------------------------------------- 1 | package intercept 2 | 3 | /* 4 | // Plugin is the instance of the resulting composition of all the plugins. This is likely 5 | // to be changed in future versions so please do not use/modify this. 6 | var Plugin PlugHandler 7 | 8 | // PlugHandler is the resulting composition of all the plugins. This is likely 9 | // to be changed in future versions so please do not use/modify this. 10 | type PlugHandler struct { 11 | sync.Mutex 12 | used bool 13 | //Called if request is intercepted and before the buffer is sento to the UI for editing 14 | preModifyRequest RequestModifier 15 | //called on every request before preModifyRequest 16 | alwaysModifyRequest RequestModifier 17 | //Called if request is intercepted and after the buffer has been modified 18 | postModifyRequest RequestModifier 19 | //Called if response is intercepted and before the buffer is sento to the UI for editing 20 | preModifyResponse ResponseModifier 21 | //called on every response before preModifyRequest 22 | alwaysModifyRespone ResponseModifier 23 | //Called if response is intercepted and after the buffer has been modified 24 | postModifyResponse ResponseModifier 25 | } 26 | 27 | 28 | type RequestModifier func(*http.Request) (*http.Request, error) 29 | 30 | func (p *PlugHandler) PreProcessRequest(rm RequestModifier, pre bool) { 31 | p.Lock() 32 | defer p.Unlock() 33 | addRequestModifier(&p.preModifyRequest, rm, pre) 34 | } 35 | 36 | func (p *PlugHandler) PostProcessRequest(rm RequestModifier, pre bool) { 37 | p.Lock() 38 | defer p.Unlock() 39 | addRequestModifier(&p.postModifyRequest, rm, pre) 40 | } 41 | 42 | func addRequestModifier(field *RequestModifier, rm RequestModifier, pre bool) { 43 | if *field == nil { 44 | *field = rm 45 | } else { 46 | if pre { 47 | *field = composeRequestModifier(*field, rm) 48 | } else { 49 | *field = composeRequestModifier(rm, *field) 50 | } 51 | } 52 | } 53 | 54 | func composeRequestModifier(a RequestModifier, b RequestModifier) RequestModifier { 55 | return func(r *http.Request) (*http.Request, error) { 56 | out, err := b(r) 57 | if err != nil { 58 | return nil, err 59 | } 60 | return a(out) 61 | } 62 | } 63 | 64 | type ResponseModifier func(*http.Request, *http.Response) (*http.Response, error) 65 | 66 | func (p *PlugHandler) PreProcessResponse(rm ResponseModifier, pre bool) { 67 | p.Lock() 68 | defer p.Unlock() 69 | addResponseModifier(&p.preModifyResponse, rm, pre) 70 | } 71 | 72 | func (p *PlugHandler) PostProcessResponse(rm ResponseModifier, pre bool) { 73 | p.Lock() 74 | defer p.Unlock() 75 | addResponseModifier(&p.postModifyResponse, rm, pre) 76 | } 77 | 78 | func addResponseModifier(field *ResponseModifier, rm ResponseModifier, pre bool) { 79 | if *field == nil { 80 | *field = rm 81 | } else { 82 | if pre { 83 | *field = composeResponseModifier(*field, rm) 84 | } else { 85 | *field = composeResponseModifier(rm, *field) 86 | } 87 | } 88 | } 89 | 90 | func composeResponseModifier(a ResponseModifier, b ResponseModifier) ResponseModifier { 91 | return func(req *http.Request, in *http.Response) (*http.Response, error) { 92 | out, err := b(req, in) 93 | if err != nil { 94 | return nil, err 95 | } 96 | return a(req, out) 97 | } 98 | } 99 | */ 100 | -------------------------------------------------------------------------------- /intercept/plugin_test.go: -------------------------------------------------------------------------------- 1 | package intercept 2 | 3 | /* 4 | func a(req *http.Request, in *http.Response) (*http.Response, error) { 5 | in.Header.Set("X-Wapty-Test", in.Header.Get("X-Wapty-Test")+"FuncA") 6 | return in, nil 7 | } 8 | func b(req *http.Request, in *http.Response) (*http.Response, error) { 9 | in.Header.Set("X-Wapty-Test", in.Header.Get("X-Wapty-Test")+"FuncB") 10 | return in, nil 11 | } 12 | func c(in *http.Request) (*http.Request, error) { 13 | in.Header.Set("X-Wapty-Test", in.Header.Get("X-Wapty-Test")+"FuncC") 14 | return in, nil 15 | } 16 | func d(in *http.Request) (*http.Request, error) { 17 | in.Header.Set("X-Wapty-Test", in.Header.Get("X-Wapty-Test")+"FuncD") 18 | return in, nil 19 | } 20 | 21 | func TestComposeResponseModifier(t *testing.T) { 22 | var ab = composeResponseModifier(a, b) 23 | tester := &http.Response{Header: http.Header{}} 24 | result, _ := ab(nil, tester) 25 | if actual := result.Header.Get("X-Wapty-Test"); "FuncBFuncA" != actual { 26 | t.Errorf("composeResponseModifier did not work, expected FuncBFuncA, got " + actual) 27 | } 28 | tester.Header.Del("X-Wapty-Test") 29 | var ba = composeResponseModifier(b, a) 30 | result, _ = ba(nil, tester) 31 | if actual := result.Header.Get("X-Wapty-Test"); "FuncAFuncB" != actual { 32 | t.Errorf("composeResponseModifier did not work, expected FuncAFuncB, got " + actual) 33 | } 34 | } 35 | 36 | func TestPlug(t *testing.T) { 37 | var tester PlugHandler 38 | tester.PreProcessRequest(c, true) 39 | tester.PreProcessRequest(d, true) 40 | result := &http.Request{Header: http.Header{}} 41 | result, _ = tester.preModifyRequest(result) 42 | if actual := result.Header.Get("X-Wapty-Test"); "FuncDFuncC" != actual { 43 | t.Errorf("PreProcessRequest did not work, expected FuncDFuncC, got " + actual) 44 | } 45 | } 46 | */ 47 | -------------------------------------------------------------------------------- /intercept/queues.go: -------------------------------------------------------------------------------- 1 | //Package intercept is meant to handle all the interception of requests and responses, 2 | //including stopping and waiting for edited payloads. 3 | //Every request going through the proxy is parsed and added to the Status by this 4 | //package. 5 | package intercept 6 | 7 | import ( 8 | "crypto/tls" 9 | "net" 10 | "net/http" 11 | "sync" 12 | "time" 13 | 14 | "github.com/empijei/cli/lg" 15 | "github.com/empijei/wapty/mitm" 16 | "github.com/empijei/wapty/ui" 17 | "github.com/empijei/wapty/ui/apis" 18 | ) 19 | 20 | //Not used yet 21 | var done chan struct{} 22 | 23 | //If value is set to true tells the proxy to start the intercept 24 | var intercept syncBool 25 | 26 | var uiSettings ui.Subscription 27 | 28 | func init() { 29 | done = make(chan struct{}) 30 | //intercept.value = true 31 | uiSettings = ui.Subscribe(apis.CHN_INTERCEPTSETTINGS) 32 | } 33 | 34 | // syncBool is just used as a thread safe bool. As of 19/06/2017 the sync/atomic 35 | // package does not provide boolean operations 36 | type syncBool struct { 37 | sync.RWMutex 38 | val bool 39 | } 40 | 41 | func (s *syncBool) value() bool { 42 | s.RLock() 43 | defer s.RUnlock() 44 | return s.val 45 | } 46 | 47 | func (s *syncBool) setValue(v bool) { 48 | s.Lock() 49 | s.val = v 50 | s.Unlock() 51 | } 52 | 53 | // MainLoop is the core of the interceptor. 54 | // In order for the normal lifecycle program to work this should always be started. 55 | // It starts the goroutine that waits for new requests and response that have 56 | // been intercepted and takes action based on current configuration. 57 | func MainLoop() { 58 | //Load Certificate authority 59 | ca, err := mitm.LoadCA() 60 | if err != nil { 61 | lg.Failure(err) 62 | } 63 | 64 | //Call dispatchloop on other goroutine 65 | go dispatchLoop() 66 | 67 | //Run History interactions 68 | go historyLoop() 69 | 70 | //Listen for settings changes 71 | go settingsLoop() 72 | 73 | //Create the modified transport to intercept responses 74 | //modifiedTransport := ResponseInterceptor{wrappedRT: http.DefaultTransport} //This uses HTTP2 75 | wrappedTransport := &http.Transport{ 76 | TLSNextProto: make(map[string]func(authority string, c *tls.Conn) http.RoundTripper), 77 | TLSHandshakeTimeout: 5 * time.Second, //TODO make this a variable 78 | TLSClientConfig: &tls.Config{ 79 | InsecureSkipVerify: true, 80 | }, 81 | Dial: (&net.Dialer{ 82 | Timeout: 5 * time.Second, //TODO make this a variable 83 | }).Dial, 84 | } 85 | //noHTTP2Transport.DisableCompression = false 86 | modifiedTransport := Interceptor{wrappedRT: wrappedTransport} 87 | 88 | //Creates the mitm.Proxy with the modified transport and the loaded CA 89 | p := &mitm.Proxy{ 90 | CA: &ca, 91 | TLSServerConfig: &tls.Config{ 92 | MinVersion: tls.VersionSSL30, 93 | }, 94 | //Wrap: interceptRequestWrapper, 95 | Transport: &modifiedTransport, 96 | } 97 | lg.Infof("Proxy is running on localhost: %d\n", 8080) 98 | //Starts the mitm.Proxy 99 | lg.Info(http.ListenAndServe(":8080", p)) //TODO parametrize this and allow for closure 100 | close(done) 101 | } 102 | 103 | //This loop will keep reading from the RequestQueue and ResponseQueue for new 104 | //intercepted payloads. 105 | func dispatchLoop() { 106 | for { 107 | select { 108 | case preq := <-RequestQueue: 109 | handleRequest(preq) 110 | case presp := <-ResponseQueue: 111 | handleResponse(presp) 112 | case <-done: 113 | return 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /intercept/requests.go: -------------------------------------------------------------------------------- 1 | package intercept 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "io/ioutil" 7 | "net/http" 8 | "net/http/httputil" 9 | 10 | "github.com/empijei/cli/lg" 11 | "github.com/empijei/wapty/ui/apis" 12 | ) 13 | 14 | // RequestQueue represents the queue of requests that have been intercepted 15 | var RequestQueue chan *pendingRequest 16 | 17 | func init() { 18 | RequestQueue = make(chan *pendingRequest) 19 | } 20 | 21 | //In order for the program to work this should always be started. 22 | //MainLoop is the core of the interceptor. It starts the goroutine that waits 23 | //for new requests and response that have been intercepted and takes action 24 | //based on current configuration. 25 | 26 | //Called by the dispatchLoop if a request is intercepted 27 | func handleRequest(preq *pendingRequest) { 28 | r := preq.originalRequest 29 | ContentLength := r.ContentLength 30 | r.ContentLength = -1 31 | r.Header.Del("Content-Length") 32 | req, err := httputil.DumpRequest(r, true) 33 | if err != nil { 34 | lg.Error("intercept: dumping request %s", err.Error()) 35 | preq.modifiedRequest <- &mayBeRequest{err: err} 36 | return 37 | } 38 | var editedRequest *http.Request 39 | var providedResp *http.Response 40 | editedRequestDump, action := editBuffer(apis.PLD_REQUEST, req, r.URL.Scheme+"://"+r.Host) 41 | switch action { 42 | case apis.EDT_FORWARD: 43 | r.ContentLength = ContentLength 44 | editedRequest = r 45 | case apis.EDT_EDIT: 46 | editedRequest, err = editCase(editedRequestDump) 47 | status.addRawEditedRequest(preq.id, editedRequestDump) 48 | case apis.EDT_DROP: 49 | providedResp = caseDrop() 50 | case apis.EDT_PROVIDERESP: 51 | providedResponseBuffer := bufio.NewReader(bytes.NewReader(editedRequestDump)) 52 | providedResp, err = http.ReadResponse(providedResponseBuffer, preq.originalRequest) 53 | if err != nil { 54 | //TODO check this error and hijack connection to send raw bytes 55 | lg.Error("Error during provided response parsing") 56 | } 57 | status.addRawEditedResponse(preq.id, editedRequestDump) 58 | default: 59 | //TODO implement this 60 | lg.Error("Not implemented yet") 61 | editedRequest = preq.originalRequest 62 | } 63 | 64 | preq.modifiedRequest <- &mayBeRequest{req: editedRequest, res: providedResp, err: err} 65 | } 66 | 67 | func preProcessRequest(req *http.Request) (autoEdited *http.Request, ID int, err error) { 68 | stripHTHHeaders(&(req.Header)) 69 | ID = newReqResp(req) 70 | //TODO Add autoedit here 71 | autoEdited = req 72 | //FIXME Call this in a "decode" function for requests, like the one used for responses 73 | //TODO add to status as edited response 74 | //TODO Return edited one 75 | //TODO Add auto-resolve hostnames here 76 | return 77 | } 78 | 79 | func editRequest(req *http.Request, ID int) (*http.Request, *http.Response, error) { 80 | //Send request to the dispatchLoop 81 | ModifiedRequest := make(chan *mayBeRequest) 82 | RequestQueue <- &pendingRequest{id: ID, originalRequest: req, modifiedRequest: ModifiedRequest} 83 | lg.Debug("Request intercepted") 84 | //Wait for edited request 85 | mayBeReq := <-ModifiedRequest 86 | if mayBeReq.res != nil { 87 | return nil, mayBeReq.res, mayBeReq.err 88 | } 89 | if mayBeReq.err != nil { 90 | //If edit goes wrong, try to keep going with the original request 91 | lg.Error(mayBeReq.err) 92 | //FIXME Document this weir behavior or use error properly 93 | return req, nil, nil 94 | } 95 | return mayBeReq.req, nil, nil 96 | } 97 | 98 | func editCase(editedRequestDump []byte) (editedRequest *http.Request, err error) { 99 | rc := bufio.NewReader(bytes.NewReader(editedRequestDump)) 100 | editedRequest, err = http.ReadRequest(rc) 101 | if err != nil { 102 | lg.Error("Error during edited request parsing, dunno what to do yet!!!") 103 | //TODO Default to bare sockets 104 | } 105 | //Parsing leftovers, if any, must be the request body 106 | body, err := ioutil.ReadAll(rc) 107 | if err != nil { 108 | lg.Error("Error during edited body reading") 109 | //TODO 110 | } 111 | if length := len(body); length != 0 { 112 | editedRequest.ContentLength = int64(length) 113 | editedRequest.Body = ioutil.NopCloser(bufio.NewReader(bytes.NewReader(body))) 114 | } 115 | return 116 | } 117 | -------------------------------------------------------------------------------- /intercept/requests_test.go: -------------------------------------------------------------------------------- 1 | package intercept 2 | -------------------------------------------------------------------------------- /intercept/responses.go: -------------------------------------------------------------------------------- 1 | package intercept 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "io/ioutil" 7 | "net/http" 8 | "net/http/httputil" 9 | "strconv" 10 | 11 | "github.com/empijei/cli/lg" 12 | "github.com/empijei/wapty/ui/apis" 13 | ) 14 | 15 | //ResponseQueue represents the queue of the response to requests that have been intercepted 16 | var ResponseQueue chan *pendingResponse 17 | 18 | func init() { 19 | ResponseQueue = make(chan *pendingResponse) 20 | } 21 | 22 | //Called by the dispatchLoop if a response is intercepted 23 | func handleResponse(presp *pendingResponse) { 24 | res := presp.originalResponse 25 | ContentLength := res.ContentLength 26 | res.ContentLength = -1 27 | res.Header.Del("Content-Length") 28 | rawRes, err := httputil.DumpResponse(res, true) 29 | if err != nil { 30 | lg.Errorf("intercept: dumping response %v", err) 31 | presp.modifiedResponse <- &mayBeResponse{err: err} 32 | return 33 | } 34 | var editedResponse *http.Response 35 | editedResponseDump, action := editBuffer(apis.PLD_RESPONSE, rawRes, presp.originalRequest.URL.Scheme+"://"+presp.originalRequest.Host) 36 | switch action { 37 | case apis.EDT_FORWARD: 38 | res.ContentLength = ContentLength 39 | res.Header.Set("Content-Length", strconv.Itoa(int(ContentLength))) 40 | editedResponse = res 41 | case apis.EDT_EDIT, apis.EDT_PROVIDERESP: 42 | editedResponseBuffer := bufio.NewReader(bytes.NewReader(editedResponseDump)) 43 | editedResponse, err = http.ReadResponse(editedResponseBuffer, presp.originalRequest) 44 | if err != nil { 45 | //TODO check this error and hijack connection to send raw bytes 46 | lg.Error("Error during edited response parsing, forwarding original response.") 47 | res.ContentLength = ContentLength 48 | editedResponse = res 49 | } 50 | status.addRawEditedResponse(presp.id, editedResponseDump) 51 | case apis.EDT_DROP: 52 | editedResponse = caseDrop() 53 | default: 54 | //TODO implement this 55 | lg.Error("Not implemented yet") 56 | } 57 | presp.modifiedResponse <- &mayBeResponse{res: editedResponse, err: err} 58 | } 59 | func preProcessResponse(req *http.Request, res *http.Response, ID int) *http.Response { 60 | res = decodeResponse(res) 61 | //Skip intercept if request was not intercepted, just add the response to the Status 62 | status.addResponse(ID, res) 63 | //TODO autoEdit here 64 | //TODO add to status as edited if autoedited 65 | return res 66 | } 67 | func editResponse(req *http.Request, res *http.Response, ID int) (*http.Response, error) { 68 | //Request was intercepted, go through the intercept/edit process 69 | //TODO use the autoedited one to edit 70 | ModifiedResponse := make(chan *mayBeResponse) 71 | ResponseQueue <- &pendingResponse{id: ID, modifiedResponse: ModifiedResponse, originalRequest: req, originalResponse: res} 72 | mayBeRes := <-ModifiedResponse 73 | return mayBeRes.res, mayBeRes.err 74 | } 75 | 76 | //Making use of the net.http package to remove all the encoding by exausting the 77 | //request body and replacing it with a io.ReadCloser with the complete response. 78 | //This takes care of Transfer-Encoding and Content-Encoding 79 | func decodeResponse(res *http.Response) *http.Response { 80 | defer func() { _ = res.Body.Close() }() 81 | buf, err := ioutil.ReadAll(res.Body) 82 | if err != nil { 83 | lg.Error(err) 84 | return res 85 | } 86 | res.Body = ioutil.NopCloser(bytes.NewBuffer(buf)) 87 | res.TransferEncoding = nil 88 | stripHTHHeaders(&(res.Header)) 89 | res.ContentLength = int64(len(buf)) 90 | return res 91 | } 92 | 93 | func caseDrop() (res *http.Response) { 94 | return GenerateResponse("Interceptor", "Response was dropped", 418) 95 | } 96 | -------------------------------------------------------------------------------- /intercept/responsesGenerator.go: -------------------------------------------------------------------------------- 1 | package intercept 2 | 3 | import ( 4 | "bytes" 5 | "html/template" 6 | "io/ioutil" 7 | "net/http" 8 | ) 9 | 10 | const templateResponse = ` 11 | 12 | 13 | {{.Title}} 14 | 15 | 16 | 17 |

{{.Content}}

18 | 19 | 20 | ` 21 | 22 | // GenerateResponse creates a response with a given title, content and status 23 | // code. This can be used for example to provide responses when a request is 24 | // dropped and do not leave the client hanging. 25 | func GenerateResponse(title, content string, status int) *http.Response { 26 | t, _ := template.New("Generated Response").Parse(templateResponse) 27 | data := struct { 28 | Title, Content string 29 | }{ 30 | title, 31 | content, 32 | } 33 | body := bytes.NewBuffer(nil) 34 | _ = t.Execute(body, data) 35 | res := &http.Response{} 36 | res.ContentLength = int64(body.Len()) 37 | res.Body = ioutil.NopCloser(body) 38 | res.StatusCode = status 39 | res.Header = http.Header{} 40 | res.Header.Set("X-WAPTY-Status", title) 41 | return res 42 | } 43 | -------------------------------------------------------------------------------- /intercept/responses_test.go: -------------------------------------------------------------------------------- 1 | package intercept 2 | -------------------------------------------------------------------------------- /intercept/settingsActions.go: -------------------------------------------------------------------------------- 1 | package intercept 2 | 3 | import ( 4 | "github.com/empijei/cli/lg" 5 | "github.com/empijei/wapty/ui/apis" 6 | ) 7 | 8 | func settingsLoop() { 9 | for { 10 | select { 11 | case cmd := <-uiSettings.RecChannel(): 12 | lg.Debug("Settings accessed") 13 | switch cmd.Action { 14 | case apis.STN_INTERCEPT: 15 | uiSettings.Send(handleIntercept(cmd)) 16 | default: 17 | //TODO send error? 18 | lg.Error("Unknown action: %v", cmd.Action) 19 | } 20 | case <-done: 21 | return 22 | } 23 | } 24 | } 25 | 26 | func handleIntercept(cmd apis.Command) *apis.Command { 27 | value := apis.ARG_FALSE 28 | if len(cmd.Args) >= 1 { 29 | lg.Debug("Requested change intercept status") 30 | intercept.setValue(cmd.Args[apis.ARG_ON] == apis.ARG_TRUE) 31 | if intercept.value() { 32 | value = apis.ARG_TRUE 33 | } 34 | } 35 | lg.Debug("Requested intercept status") 36 | if intercept.value() { 37 | value = apis.ARG_TRUE 38 | } 39 | return &apis.Command{ 40 | Action: apis.STN_INTERCEPT, 41 | Args: map[apis.ArgName]string{apis.ARG_ON: value}, 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /intercept/settingsActions_test.go: -------------------------------------------------------------------------------- 1 | package intercept 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/empijei/wapty/ui/apis" 8 | ) 9 | 10 | var loopTests = []struct { 11 | //TODO 12 | }{} 13 | 14 | var handleTests = []struct { 15 | in apis.Command 16 | out apis.Command 17 | }{ 18 | { 19 | apis.Command{ 20 | Action: apis.STN_INTERCEPT, 21 | Args: map[apis.ArgName]string{apis.ARG_ON: apis.ARG_FALSE}, 22 | }, 23 | apis.Command{ 24 | Action: apis.STN_INTERCEPT, 25 | Args: map[apis.ArgName]string{apis.ARG_ON: apis.ARG_FALSE}, 26 | }, 27 | }, 28 | { 29 | apis.Command{ 30 | Action: apis.STN_INTERCEPT, 31 | }, 32 | apis.Command{ 33 | Action: apis.STN_INTERCEPT, 34 | Args: map[apis.ArgName]string{apis.ARG_ON: apis.ARG_FALSE}, 35 | }, 36 | }, 37 | { 38 | apis.Command{ 39 | Action: apis.STN_INTERCEPT, 40 | Args: map[apis.ArgName]string{apis.ARG_ON: apis.ARG_TRUE}, 41 | }, 42 | apis.Command{ 43 | Action: apis.STN_INTERCEPT, 44 | Args: map[apis.ArgName]string{apis.ARG_ON: apis.ARG_TRUE}, 45 | }, 46 | }, 47 | { 48 | apis.Command{ 49 | Action: apis.STN_INTERCEPT}, 50 | apis.Command{ 51 | Action: apis.STN_INTERCEPT, 52 | Args: map[apis.ArgName]string{apis.ARG_ON: apis.ARG_TRUE}, 53 | }, 54 | }, 55 | } 56 | 57 | func TestHandleIntercept(t *testing.T) { 58 | for _, tt := range handleTests { 59 | out := handleIntercept(tt.in) 60 | if !reflect.DeepEqual(*out, tt.out) { 61 | t.Errorf("handleIntercept(%v) => %v, want %v", tt.in, out, tt.out) 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | # Name 2 | BINARY=wapty 3 | IMPORTPATH=`go list`/ 4 | 5 | # Variables 6 | VERSION=0.2.0 7 | BUILD=`git rev-parse --short HEAD` 8 | 9 | LDFLAGS=-ldflags "-X ${IMPORTPATH}cli.Version=${VERSION} -X ${IMPORTPATH}cli.Commit=${BUILD}" 10 | LDFLAGS_RELEASE=-ldflags "-X ${IMPORTPATH}cli.Version=${VERSION} -X ${IMPORTPATH}cli.Commit=${BUILD} -X ${IMPORTPATH}cli.Build=Release" 11 | 12 | .DEFAULT_GOAL: ${BINARY} 13 | 14 | # Just build the wapty 15 | # TODO call gopherjs 16 | ${BINARY}: buildjs rebind 17 | # Building the executable. 18 | go build ${LDFLAGS_RELEASE} -o ${BINARY} 19 | 20 | run: 21 | # This will make rice use data that is on disk, creates a lighter executable 22 | # and it is faster to build 23 | -rm ui/rice-box.go >& /dev/null 24 | # Generating JS 25 | cd ui/gopherjs/ && gopherjs build -o ../static/gopherjs.js 26 | # Done generating JS, launching wapty 27 | go run -race ${LDFLAGS} wapty.go 28 | 29 | fast: run 30 | 31 | test: buildjs rebind 32 | go test -race ${LDFLAGS} ./... 33 | 34 | testv: buildjs rebind 35 | go test -v -x -race ${LDFLAGS} ./... 36 | 37 | buildjs: 38 | # Regenerating minified js 39 | cd ui/gopherjs/ && gopherjs build -m -o ../static/gopherjs.js 40 | # Remove mappings 41 | rm ui/static/gopherjs.js.map 42 | 43 | rebind: 44 | # Cleaning and re-embedding assets 45 | cd ui && rm rice-box.go 1>/dev/null 2>/dev/null; rice embed-go 46 | 47 | install: buildjs rebind 48 | # Installing the executable 49 | go install ${LDFLAGS_RELEASE} 50 | 51 | installdeps: 52 | # Installing dependencies to embed assets 53 | go get github.com/GeertJohan/go.rice/... 54 | # Installing dependencies to build JS 55 | go get github.com/gopherjs/gopherjs 56 | go get github.com/gopherjs/websocket/... 57 | # Installing Diff dependencies 58 | go get github.com/fatih/color 59 | go get github.com/pmezard/go-difflib/difflib 60 | 61 | updatedeps: 62 | # Updating dependencies to embed assets 63 | go get -u github.com/GeertJohan/go.rice/... 64 | # Updating dependencies to build JS 65 | go get -u github.com/gopherjs/gopherjs 66 | go get -u github.com/gopherjs/websocket/... 67 | # Updating Diff dependencies 68 | go get -u github.com/fatih/color 69 | go get -u github.com/pmezard/go-difflib/difflib 70 | 71 | clean: 72 | # Cleaning all generated files 73 | -rm ui/rice-box.go 74 | -rm ui/static/gopherjs.js* 75 | go clean 76 | if [ -f ${BINARY} ] ; then rm ${BINARY} ; fi 77 | -------------------------------------------------------------------------------- /mitm/CA.go: -------------------------------------------------------------------------------- 1 | //Package mitm is the core of this project and is responsible for creating a proxy that 2 | //intercepts all the HTTP/HTTPS traffic going through it. 3 | //The SSL bumping is currently made by using a fake CA whose keys are stored in 4 | //path.Join(os.Getenv("HOME"), ".mitm") 5 | //This is a fork of github.com/kr/mitm 6 | package mitm 7 | 8 | import ( 9 | "crypto/tls" 10 | "crypto/x509" 11 | "io/ioutil" 12 | "os" 13 | "path" 14 | 15 | "github.com/empijei/wapty/config" 16 | ) 17 | 18 | var ( 19 | localhostname, _ = os.Hostname() 20 | keyFile = path.Join(config.ConfDir, "ca-key.pem") 21 | certFile = path.Join(config.ConfDir, "ca-cert.crt") 22 | ) 23 | 24 | // LoadCA loads the ca from "HOME/dir" 25 | func LoadCA() (cert tls.Certificate, err error) { 26 | // TODO(kr): check file permissions 27 | cert, err = tls.LoadX509KeyPair(certFile, keyFile) 28 | if os.IsNotExist(err) { 29 | cert, err = genCA() 30 | } 31 | if err == nil { 32 | cert.Leaf, err = x509.ParseCertificate(cert.Certificate[0]) 33 | } 34 | return 35 | } 36 | 37 | func genCA() (cert tls.Certificate, err error) { 38 | certPEM, keyPEM, err := GenerateCA(localhostname) 39 | if err != nil { 40 | return 41 | } 42 | cert, _ = tls.X509KeyPair(certPEM, keyPEM) 43 | err = ioutil.WriteFile(certFile, certPEM, 0400) 44 | if err == nil { 45 | err = ioutil.WriteFile(keyFile, keyPEM, 0400) 46 | } 47 | return cert, err 48 | } 49 | -------------------------------------------------------------------------------- /mitm/CA_test.go: -------------------------------------------------------------------------------- 1 | package mitm 2 | -------------------------------------------------------------------------------- /mitm/cert.go: -------------------------------------------------------------------------------- 1 | package mitm 2 | 3 | import ( 4 | "crypto/ecdsa" 5 | "crypto/elliptic" 6 | "crypto/rand" 7 | "crypto/tls" 8 | "crypto/x509" 9 | "crypto/x509/pkix" 10 | "encoding/pem" 11 | "errors" 12 | "fmt" 13 | "math/big" 14 | "net" 15 | "time" 16 | ) 17 | 18 | const ( 19 | caMaxAge = 5 * 365 * 24 * time.Hour 20 | leafMaxAge = 24 * time.Hour 21 | caUsage = x509.KeyUsageDigitalSignature | 22 | x509.KeyUsageContentCommitment | 23 | x509.KeyUsageKeyEncipherment | 24 | x509.KeyUsageDataEncipherment | 25 | x509.KeyUsageKeyAgreement | 26 | x509.KeyUsageCertSign | 27 | x509.KeyUsageCRLSign 28 | leafUsage = caUsage 29 | ) 30 | 31 | // GenerateCA generates a CA cert and key pair. 32 | func GenerateCA(name string) (certPEM, keyPEM []byte, err error) { 33 | now := time.Now().UTC() 34 | tmpl := &x509.Certificate{ 35 | SerialNumber: big.NewInt(1), 36 | Subject: pkix.Name{CommonName: name}, 37 | NotBefore: now, 38 | NotAfter: now.Add(caMaxAge), 39 | KeyUsage: caUsage, 40 | BasicConstraintsValid: true, 41 | IsCA: true, 42 | MaxPathLen: 2, 43 | SignatureAlgorithm: x509.ECDSAWithSHA512, 44 | } 45 | key, err := genKeyPair() 46 | if err != nil { 47 | return 48 | } 49 | certDER, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, key.Public(), key) 50 | if err != nil { 51 | return 52 | } 53 | keyDER, err := x509.MarshalECPrivateKey(key) 54 | if err != nil { 55 | return 56 | } 57 | certPEM = pem.EncodeToMemory(&pem.Block{ 58 | Type: "CERTIFICATE", 59 | Bytes: certDER, 60 | }) 61 | keyPEM = pem.EncodeToMemory(&pem.Block{ 62 | Type: "ECDSA PRIVATE KEY", 63 | Bytes: keyDER, 64 | }) 65 | return 66 | } 67 | 68 | // GenerateCert generates a leaf cert from ca. 69 | func GenerateCert(ca *tls.Certificate, hosts ...string) (*tls.Certificate, error) { 70 | now := time.Now().Add(-1 * time.Hour).UTC() 71 | if !ca.Leaf.IsCA { 72 | return nil, errors.New("CA cert is not a CA") 73 | } 74 | serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) 75 | serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) 76 | if err != nil { 77 | return nil, fmt.Errorf("failed to generate serial number: %s", err) 78 | } 79 | template := &x509.Certificate{ 80 | SerialNumber: serialNumber, 81 | Subject: pkix.Name{CommonName: hosts[0]}, 82 | NotBefore: now, 83 | NotAfter: now.Add(leafMaxAge), 84 | KeyUsage: leafUsage, 85 | BasicConstraintsValid: true, 86 | SignatureAlgorithm: x509.ECDSAWithSHA256, 87 | } 88 | 89 | for _, h := range hosts { 90 | if ip := net.ParseIP(h); ip != nil { 91 | template.IPAddresses = append(template.IPAddresses, ip) 92 | } else { 93 | template.DNSNames = append(template.DNSNames, h) 94 | } 95 | } 96 | 97 | key, err := genKeyPair() 98 | if err != nil { 99 | return nil, err 100 | } 101 | x, err := x509.CreateCertificate(rand.Reader, template, ca.Leaf, key.Public(), ca.PrivateKey) 102 | if err != nil { 103 | return nil, err 104 | } 105 | cert := new(tls.Certificate) 106 | cert.Certificate = append(cert.Certificate, x) 107 | cert.PrivateKey = key 108 | cert.Leaf, _ = x509.ParseCertificate(x) 109 | return cert, nil 110 | } 111 | 112 | func genKeyPair() (*ecdsa.PrivateKey, error) { 113 | return ecdsa.GenerateKey(elliptic.P384(), rand.Reader) 114 | } 115 | -------------------------------------------------------------------------------- /mocksy/burpimporter.go: -------------------------------------------------------------------------------- 1 | package mocksy 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/xml" 6 | "fmt" 7 | "io" 8 | 9 | "github.com/empijei/cli/lg" 10 | ) 11 | 12 | // Request is a struct used to deserialize burp XML. It contains text data and an attribute telling 13 | // if the content is base64 or not. 14 | type Request struct { 15 | Base64 string `xml:"base64,attr"` 16 | Value []byte `xml:",chardata"` 17 | } 18 | 19 | // Bytes returns the []byte read by the XML decoder 20 | func (r Request) Bytes() []byte { 21 | return b64Able(r).Bytes() 22 | } 23 | 24 | // Response is a struct used to deserialize burp XML. It contains text data and an attribute telling 25 | // if the content is base64 or not. 26 | type Response struct { 27 | Base64 string `xml:"base64,attr"` 28 | Value []byte `xml:",chardata"` 29 | } 30 | 31 | // Bytes returns the []byte read by the XML decoder 32 | func (r Response) Bytes() []byte { 33 | return b64Able(r).Bytes() 34 | } 35 | 36 | // this is just a sort of interface for fields used to not repeat code for the 37 | // Bytes function 38 | type b64Able struct { 39 | Base64 string 40 | Value []byte 41 | } 42 | 43 | // Bytes returns the []byte read by the XML decoder 44 | func (r b64Able) Bytes() []byte { 45 | if r.Base64 == "true" { 46 | value, err := base64.StdEncoding.DecodeString(string(r.Value)) 47 | if err != nil { 48 | //TODO handle more gently 49 | lg.Error(err) 50 | } 51 | return value 52 | } 53 | return r.Value 54 | } 55 | 56 | // Host is used to deserialize burp saved XML. It contains text data and an attribute "ip". 57 | type Host struct { 58 | Ip string `xml:"ip,attr"` 59 | Value string `xml:",chardata"` 60 | } 61 | 62 | // Item is used to deserialize burp saved XML. It is the element containing all the information 63 | // about a single request. 64 | type Item struct { 65 | Time string `xml:"time"` 66 | Url string `xml:"url"` 67 | Request Request `xml:"request"` 68 | Host Host `xml:"host"` 69 | Port string `xml:"port"` 70 | Protocol string `xml:"protocol"` 71 | Method string `xml:"method"` 72 | Path string `xml:"path"` 73 | Extension string `xml:"extension"` 74 | Status string `xml:"status"` 75 | ResponseLength string `xml:"responselength"` 76 | Response Response `xml:"response"` 77 | Mimetype string `xml:"mimetype"` 78 | Comment string `xml:"comment"` 79 | } 80 | 81 | // Items is used to deserialize burp saved XML. It is the root element, containing a list of Item's. 82 | type Items struct { 83 | Items []Item `xml:"item"` 84 | } 85 | 86 | // BurpImport reads a "saved requests" file from r both in base64 and cleartext form 87 | func BurpImport(r io.Reader) (*Items, error) { 88 | dec := xml.NewDecoder(r) 89 | var itm Items 90 | err := dec.Decode(&itm) 91 | if err != nil { 92 | // wrapping errors is good practice 93 | return nil, fmt.Errorf("mocksy: cannot import status: %s", err.Error()) 94 | } 95 | return &itm, nil 96 | } 97 | -------------------------------------------------------------------------------- /mocksy/init.go: -------------------------------------------------------------------------------- 1 | package mocksy 2 | 3 | import ( 4 | "io" 5 | "os" 6 | 7 | "github.com/empijei/wapty/cli" 8 | ) 9 | 10 | var outw io.Writer 11 | 12 | var cmdMocksy = &cli.Cmd{ 13 | Name: "mocksy", 14 | Run: Main, 15 | UsageLine: "mocksy", 16 | Short: "mock responses from a server", 17 | Long: "", 18 | } 19 | 20 | func init() { 21 | responseHistory = make([]Item, 0) 22 | outw = os.Stderr 23 | cli.AddCommand(cmdMocksy) 24 | } 25 | -------------------------------------------------------------------------------- /mocksy/main.go: -------------------------------------------------------------------------------- 1 | package mocksy 2 | 3 | import "github.com/empijei/cli/lg" 4 | 5 | // Main is the main function that starts Mocksy 6 | func Main(_ ...string) { 7 | const port = ":8082" 8 | const histDir = "." 9 | 10 | SetHistDir(histDir) 11 | 12 | lg.Infof("Starting mocksy server at %s", port) 13 | if err := StartServer(port); err != nil { 14 | panic(err) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /mocksy/matcher_test.go: -------------------------------------------------------------------------------- 1 | package mocksy 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "net/http" 7 | "net/url" 8 | "strings" 9 | "testing" 10 | ) 11 | 12 | const matcherTestData = ` 13 | 14 | 15 | 16 | 17 | localhost 18 | 443 19 | http 20 | 21 | 22 | null 23 | 24 | 200 25 | 4 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | localhost 34 | 443 35 | http 36 | 37 | 38 | null 39 | 40 | 200 41 | 4 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | localhost 50 | 8082 51 | http 52 | 53 | 54 | null 55 | 56 | 200 57 | 4 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | localhost 66 | 8083 67 | http 68 | 69 | 70 | null 71 | 72 | 200 73 | 5 74 | 75 | 76 | 77 | 78 | ` 79 | 80 | func init() { 81 | err := LoadResponsesFrom(strings.NewReader(matcherTestData)) 82 | if err != nil { 83 | panic(err) 84 | } 85 | } 86 | 87 | // httpBody is a trivial ReadCloser to pass as Body to http.Request 88 | type httpBody struct { 89 | io.Reader 90 | } 91 | 92 | func (h httpBody) Close() error { return nil } 93 | 94 | func TestMatcher(t *testing.T) { 95 | u, _ := url.Parse("http://localhost/") 96 | req := http.Request{ 97 | Method: "GET", 98 | URL: u, 99 | Proto: "HTTP/1.0", 100 | Body: httpBody{strings.NewReader(`PING`)}, 101 | ContentLength: 4, 102 | Host: "localhost", 103 | } 104 | resp := FindMatching(&req) 105 | 106 | if !bytes.Equal(resp.Value, []byte("PONG")) { 107 | t.Fatal("Expected response 'PONG' but got", string(resp.Value)) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /mocksy/server.go: -------------------------------------------------------------------------------- 1 | package mocksy 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | "net/http" 9 | "os" 10 | "strings" 11 | 12 | "github.com/empijei/cli/lg" 13 | ) 14 | 15 | // histDir is the directory to load XML from 16 | var histDir string = "." 17 | 18 | // LoadResponseHistory loads all XML files found in `histDir` into the matcher's history. 19 | // It does NOT clear the current history (use `ClearHistory()` for that). 20 | // It does NOT recurse on the directory. 21 | // In case of errors, it tries to load as much files as possible and reports the error afterwards. 22 | func LoadResponseHistory(dir string) error { 23 | files, err := ioutil.ReadDir(dir) 24 | if err != nil { 25 | return fmt.Errorf("mocksy: Error reading history directory: %s", err.Error()) 26 | } 27 | 28 | totLoaded := 0 29 | errorMsgs := make([]string, 0) 30 | for _, file := range files { 31 | if file.IsDir() || !strings.HasSuffix(file.Name(), ".xml") { 32 | continue 33 | } 34 | fp, err := os.Open(file.Name()) 35 | if err == nil { 36 | if err = LoadResponsesFrom(fp); err != nil { 37 | errorMsgs = append(errorMsgs, err.Error()) 38 | } else { 39 | fmt.Fprintf(outw, "Loaded history file %s\n", file.Name()) 40 | totLoaded++ 41 | } 42 | } else { 43 | errorMsgs = append(errorMsgs, err.Error()) 44 | } 45 | } 46 | 47 | fmt.Fprintf(outw, "Loaded %d files correctly (history size = %d).\n", totLoaded, HistoryLength()) 48 | if len(errorMsgs) > 0 { 49 | return fmt.Errorf("mocksy: Error importing %d files: %v", len(errorMsgs), errorMsgs) 50 | } 51 | return nil 52 | } 53 | 54 | // LoadResponsesFrom decodes an XML source and loads all req-resp pairs in the matcher's responseHistory. 55 | func LoadResponsesFrom(source io.ReadSeeker) error { 56 | // Go refuses to parse any XML whose version is != "1.0". Burp sometimes 57 | // declares XML 1.1, albeit it uses no 1.1-only features, so we trick 58 | // the XML parser into parsing our "invalid" XML by skipping the XML header. 59 | buf := make([]byte, len(``)) 60 | if n, err := source.Read(buf); err == nil && n == len(buf) { 61 | // Check we actually skipped the XML header and, if not, rewind. 62 | if !bytes.Equal(buf[:len(` 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | ]> 25 | 26 | 27 | 28 | 29 | localhost 30 | 443 31 | http 32 | 33 | 34 | null 35 | 36 | 200 37 | 4 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | localhost 46 | 443 47 | http 48 | 49 | 50 | null 51 | 52 | 200 53 | 4 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | localhost 62 | 8082 63 | http 64 | 65 | 66 | null 67 | 68 | 200 69 | 4 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | localhost 78 | 8083 79 | http 80 | 81 | 82 | null 83 | 84 | 200 85 | 5 86 | 87 | 88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /pics/history.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/empijei/wapty/22661afbca5b7baf0859469ebb4959206f3d14ba/pics/history.png -------------------------------------------------------------------------------- /pics/intercept.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/empijei/wapty/22661afbca5b7baf0859469ebb4959206f3d14ba/pics/intercept.png -------------------------------------------------------------------------------- /project.vim: -------------------------------------------------------------------------------- 1 | :NERDTreeToggle 2 | :normal P 3 | :normal O 4 | :tabedit ROADMAP.md 5 | :tabnext 6 | -------------------------------------------------------------------------------- /repeat/history.go: -------------------------------------------------------------------------------- 1 | package repeat 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | "sync" 7 | ) 8 | 9 | var status Repeaters 10 | 11 | // Repeaters is a list of repeater with its embedded RWMutex 12 | type Repeaters struct { 13 | sync.RWMutex 14 | Repeats []*Repeater 15 | } 16 | 17 | // Add appends in a thread-safe way a repeater to the current status and returns 18 | // its id 19 | func (h *Repeaters) Add(r *Repeater) int { 20 | h.Lock() 21 | defer h.Unlock() 22 | h.Repeats = append(h.Repeats, r) 23 | return len(h.Repeats) - 1 24 | } 25 | 26 | // Save writes the current status to the given writer in a thread-safe way 27 | func (h *Repeaters) Save(w io.Writer) error { 28 | h.RLock() 29 | defer h.RUnlock() 30 | e := json.NewEncoder(w) 31 | return e.Encode(h) 32 | } 33 | -------------------------------------------------------------------------------- /repeat/history_test.go: -------------------------------------------------------------------------------- 1 | package repeat 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | ) 7 | 8 | func TestSave(t *testing.T) { 9 | rr := NewRepeater() 10 | ri := Item{ 11 | Host: "host:port", 12 | Request: []byte("Request"), 13 | Response: []byte("Response"), 14 | } 15 | rr.History = append(rr.History, ri) 16 | status.Add(rr) 17 | b := bytes.NewBuffer(nil) 18 | err := status.Save(b) 19 | if err != nil { 20 | t.Log(err) 21 | } 22 | //FIXME 23 | t.Log(string(b.Bytes())) 24 | } 25 | -------------------------------------------------------------------------------- /repeat/queues.go: -------------------------------------------------------------------------------- 1 | package repeat 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "io" 7 | "io/ioutil" 8 | "strconv" 9 | 10 | "github.com/empijei/cli/lg" 11 | "github.com/empijei/wapty/ui/apis" 12 | ) 13 | 14 | var done = make(chan struct{}) 15 | 16 | // RepeaterLoop is the main loop for the repeater. It listens on apis.CHN_REPEAT 17 | // for calls and executes them 18 | func RepeaterLoop() { 19 | for { 20 | select { 21 | case cmd := <-uiRepeater.RecChannel(): 22 | switch cmd.Action { 23 | case apis.RPT_CREATE: 24 | uiRepeater.Send(handleCreate(&cmd)) 25 | case apis.RPT_GO: 26 | uiRepeater.Send(handleGo(&cmd)) 27 | case apis.RPT_GET: 28 | uiRepeater.Send(handleGet(&cmd)) 29 | default: 30 | lg.Error("Unknown repeater action: %s", cmd.Action) 31 | } 32 | case <-done: 33 | return 34 | } 35 | } 36 | } 37 | 38 | func handleCreate(cmd *apis.Command) *apis.Command { 39 | r := NewRepeater() 40 | id := status.Add(r) 41 | cmd.Args = map[apis.ArgName]string{apis.ARG_ID: strconv.Itoa(id)} 42 | 43 | //TODO reply with repeater ID 44 | return cmd 45 | } 46 | 47 | func handleGo(cmd *apis.Command) *apis.Command { 48 | var host string 49 | var tls bool 50 | var ri int 51 | err := cmd.UnpackArgs( 52 | []apis.ArgName{apis.ARG_ENDPOINT, apis.ARG_TLS, apis.ARG_ID}, 53 | &host, &tls, &ri, 54 | ) 55 | if err != nil { 56 | lg.Error(err) 57 | return apis.Err(err) 58 | } 59 | body := bytes.NewBuffer(cmd.Payload) 60 | status.RLock() 61 | defer status.RUnlock() 62 | if len(status.Repeats) <= ri || ri < 0 { 63 | err := "Repeater out of range" 64 | lg.Error(err) 65 | return apis.Err(err) 66 | } 67 | r := status.Repeats[ri] 68 | var res io.Reader 69 | var id int 70 | if res, id, err = r.repeat(body, host, tls); err != nil { 71 | lg.Error(err) 72 | return apis.Err(err) 73 | } 74 | resbuf, err := ioutil.ReadAll(res) 75 | if err != nil { 76 | return apis.Err(err) 77 | } 78 | cmd.Payload = resbuf 79 | cmd.Args[apis.ARG_SUBID] = strconv.Itoa(id) 80 | return cmd 81 | } 82 | 83 | func handleGet(cmd *apis.Command) *apis.Command { 84 | var ri, itemn int 85 | err := cmd.UnpackArgs( 86 | []apis.ArgName{apis.ARG_ID, apis.ARG_SUBID}, 87 | &ri, &itemn, 88 | ) 89 | status.RLock() 90 | defer status.RUnlock() 91 | if len(status.Repeats) <= ri { 92 | lg.Error("Repeater out of range") 93 | return apis.Err(err) 94 | } 95 | r := status.Repeats[ri] 96 | if len(r.History) <= itemn { 97 | err := "Repeater item out of range" 98 | lg.Error(err) 99 | return apis.Err(err) 100 | } 101 | repitem, err := json.Marshal(r.History[itemn]) 102 | if err != nil { 103 | err := "Error while marshaling repeat item" 104 | lg.Error(err) 105 | return apis.Err(err) 106 | } 107 | cmd.Payload = repitem 108 | return cmd 109 | } 110 | -------------------------------------------------------------------------------- /repeat/queues_test.go: -------------------------------------------------------------------------------- 1 | package repeat 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "net/http" 7 | "net/http/httptest" 8 | "net/url" 9 | "testing" 10 | 11 | "github.com/empijei/wapty/ui/apis" 12 | ) 13 | 14 | type MockSubscription struct { 15 | ID int64 16 | Channel string 17 | DataCh chan apis.Command 18 | SentStuff chan apis.Command 19 | } 20 | 21 | func (s *MockSubscription) Receive() apis.Command { 22 | return <-s.DataCh 23 | } 24 | func (s *MockSubscription) RecChannel() <-chan apis.Command { 25 | return s.DataCh 26 | } 27 | func (s *MockSubscription) Send(c *apis.Command) { 28 | s.SentStuff <- *c 29 | } 30 | 31 | func TestHandler(t *testing.T) { 32 | //backupstatus := status 33 | //backupui := uiRepeater 34 | //FIXME change this test to directly invoke proper handler and do not spawn 35 | // the repeater loop 36 | dataCh := make(chan apis.Command) 37 | mocksub := &MockSubscription{ 38 | DataCh: dataCh, 39 | SentStuff: make(chan apis.Command), 40 | } 41 | uiRepeater = mocksub 42 | status = Repeaters{} 43 | go RepeaterLoop() 44 | var req *http.Request 45 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 46 | req = r 47 | _, _ = w.Write([]byte("Test response")) 48 | })) 49 | defer ts.Close() 50 | URL, _ := url.Parse(ts.URL) 51 | assert := func(condition bool, message string, vars ...interface{}) { 52 | if !condition { 53 | t.Errorf(message, vars...) 54 | } 55 | } 56 | dataCh <- apis.Command{ 57 | Channel: apis.CHN_REPEAT, 58 | Action: apis.RPT_CREATE, 59 | } 60 | 61 | tmp := <-mocksub.SentStuff 62 | id := tmp.Args[apis.ARG_ID] 63 | assert(id == "0", "Expected repeater id 0 but got "+id) 64 | 65 | tmp = apis.Command{ 66 | Channel: apis.CHN_REPEAT, 67 | Action: apis.RPT_GO, 68 | Payload: []byte(`GET / HTTP/1.1 69 | Host: localhost:` + URL.Port() + ` 70 | X-Wapty-Test: TestHeader 71 | Connection: close 72 | 73 | 74 | `)} 75 | tmp.PackArgs( 76 | []apis.ArgName{apis.ARG_ENDPOINT, apis.ARG_TLS, apis.ARG_ID}, 77 | "localhost:"+URL.Port(), "false", id, 78 | ) 79 | dataCh <- tmp 80 | 81 | tmp = <-mocksub.SentStuff 82 | assert(bytes.Contains(tmp.Payload, []byte(`Test response`)), "Unexpected response: "+string(tmp.Payload)) 83 | assert(req.Header.Get("X-Wapty-Test") == "TestHeader", "Test header was not successfully set. Expected but got <%s>", req.Header.Get("X-Wapty-Test")) 84 | subid := tmp.Args[apis.ARG_SUBID] 85 | assert(subid == "0", "Expected repeat payload id 0 but got "+subid) 86 | 87 | tmp = apis.Command{ 88 | Channel: apis.CHN_REPEAT, 89 | Action: apis.RPT_GET, 90 | } 91 | tmp.PackArgs( 92 | []apis.ArgName{apis.ARG_ID, apis.ARG_SUBID}, 93 | id, subid, 94 | ) 95 | dataCh <- tmp 96 | 97 | tmp = <-mocksub.SentStuff 98 | var histitem Item 99 | err := json.Unmarshal(tmp.Payload, &histitem) 100 | if err != nil { 101 | t.Error("Unexpected error while fetching repeat entry: " + err.Error()) 102 | } 103 | assert(bytes.Contains(histitem.Response, []byte(`Test response`)), "Unexpected history response: "+string(tmp.Payload)) 104 | assert(bytes.Contains(histitem.Request, []byte(`TestHeader`)), "Unexpected history request: "+string(tmp.Payload)) 105 | subid = tmp.Args[apis.ARG_SUBID] 106 | assert(subid == "0", "Expected repeat payload id 0 but got "+subid) 107 | } 108 | -------------------------------------------------------------------------------- /repeat/repeat.go: -------------------------------------------------------------------------------- 1 | package repeat 2 | 3 | import ( 4 | "bytes" 5 | "crypto/tls" 6 | "io" 7 | "net" 8 | "sync" 9 | "time" 10 | 11 | "github.com/empijei/cli/lg" 12 | ) 13 | 14 | // DefaultTimeout is the default value for the timeout when creating a new Repeater 15 | var DefaultTimeout = 10 * time.Second 16 | 17 | // Item contains the information for a single "Go" of a Repeater 18 | type Item struct { 19 | Host string 20 | TLS bool 21 | Request []byte 22 | Response []byte 23 | } 24 | 25 | // Repeater represents a full history of requests and responses 26 | type Repeater struct { 27 | m sync.Mutex 28 | 29 | // The Repeater History 30 | History []Item 31 | 32 | // The timeout to wait before assuming the server will not respond. 33 | // Default is DefaultTimeout 34 | Timeout time.Duration 35 | } 36 | 37 | // NewRepeater creates a new Repeater with Timeout set to DefaultTimeout 38 | func NewRepeater() *Repeater { 39 | return &Repeater{ 40 | Timeout: DefaultTimeout, 41 | } 42 | } 43 | 44 | func (r *Repeater) repeat(buf io.Reader, host string, _tls bool) (res io.Reader, id int, err error) { 45 | id = -1 46 | r.m.Lock() 47 | defer r.m.Unlock() 48 | savedReq := bytes.NewBuffer(nil) 49 | teebuf := io.TeeReader(buf, savedReq) 50 | var conn net.Conn 51 | if _tls { 52 | //The repeater does not care about certs 53 | conn, err = tls.Dial("tcp", host, &tls.Config{InsecureSkipVerify: true}) 54 | } else { 55 | conn, err = net.Dial("tcp", host) 56 | } 57 | if err != nil { 58 | return 59 | } 60 | defer func() { _ = conn.Close() }() 61 | _ = conn.SetDeadline(time.Now().Add(r.Timeout)) 62 | resbuf := bytes.NewBuffer(nil) 63 | errWrite := make(chan error) 64 | 65 | go func() { 66 | lg.Debug("Transmitting the request") 67 | _, errw := io.Copy(conn, teebuf) 68 | errWrite <- errw 69 | lg.Debug("Request transmitted") 70 | }() 71 | 72 | lg.Debug("Reading the response") 73 | _, err = io.Copy(resbuf, conn) 74 | lg.Debug("Response read") 75 | if tmperr := <-errWrite; tmperr != nil { 76 | err = tmperr 77 | return 78 | } 79 | if err != nil { 80 | return 81 | } 82 | r.History = append(r.History, Item{Request: savedReq.Bytes(), Response: resbuf.Bytes()}) 83 | return resbuf, len(r.History) - 1, nil 84 | } 85 | -------------------------------------------------------------------------------- /repeat/repeatActions.go: -------------------------------------------------------------------------------- 1 | package repeat 2 | 3 | import ( 4 | "github.com/empijei/wapty/ui" 5 | "github.com/empijei/wapty/ui/apis" 6 | ) 7 | 8 | var uiRepeater ui.Subscription 9 | 10 | func init() { 11 | uiRepeater = ui.Subscribe(apis.CHN_REPEAT) 12 | } 13 | -------------------------------------------------------------------------------- /repeat/repeat_test.go: -------------------------------------------------------------------------------- 1 | package repeat 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | "net" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | type RepTest struct { 12 | in []byte 13 | out []byte 14 | } 15 | 16 | var RepeatTests = []RepTest{ 17 | { 18 | []byte(`GET /success.txt HTTP/1.1 19 | Host: detectportal.firefox.com 20 | User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:53.0) Gecko/20100101 Firefox/53.0 21 | Accept: */* 22 | Accept-Language: en-US,en;q=0.5 23 | Cache-Control: no-cache 24 | Pragma: no-cache 25 | Connection: close 26 | 27 | `), 28 | []byte(`HTTP/1.1 200 OK 29 | Content-Type: text/plain 30 | Content-Length: 8 31 | Last-Modified: Mon, 15 May 2017 18:04:40 GMT 32 | ETag: "ae780585f49b94ce1444eb7d28906123" 33 | Accept-Ranges: bytes 34 | Server: AmazonS3 35 | X-Amz-Cf-Id: MnfbeXeS3ep60gjgpK6jEZF5WYcQix8AeNXFZBLf8RpVEOC1kWBUUQ== 36 | Cache-Control: no-cache, no-store, must-revalidate 37 | Date: Tue, 23 May 2017 09:29:16 GMT 38 | Connection: close 39 | 40 | success 41 | `), 42 | }, 43 | } 44 | 45 | func listener(t *testing.T, testChan chan RepTest, input chan []byte, l net.Listener) { 46 | defer func() { _ = l.Close() }() 47 | for c, err := l.Accept(); err == nil; c, err = l.Accept() { 48 | t.Log("Got incoming connection") 49 | tt := <-testChan 50 | buf := make([]byte, len(tt.in)) 51 | n, err := c.Read(buf) 52 | if err != nil { 53 | t.Error(err) 54 | } 55 | t.Logf("Read from connection %d bytes", n) 56 | for tmp := 0; n < len(tt.in) && err == nil; { 57 | tmp, err = c.Read(buf[n+tmp:]) 58 | if tmp != 0 { 59 | t.Logf("Read from connection %d more bytes", tmp) 60 | } 61 | n += tmp 62 | } 63 | if err != nil { 64 | t.Error(err) 65 | } 66 | if n != len(tt.in) { 67 | t.Errorf("Expected read of %d but actually read %d bytes.", len(tt.in), n) 68 | } 69 | input <- buf 70 | n, err = c.Write(tt.out) 71 | if err != nil { 72 | t.Error(err) 73 | } 74 | if n != len(tt.out) { 75 | t.Errorf("Should have written %d bytes but wrote %d", len(tt.out), n) 76 | } 77 | _ = c.Close() 78 | } 79 | } 80 | 81 | func TestRepeatPlain(t *testing.T) { 82 | testChan := make(chan RepTest, 2) 83 | input := make(chan []byte, 2) 84 | t.Log("Listening on port 12321") 85 | l, err := net.Listen("tcp", ":12321") 86 | if err != nil { 87 | t.Fatal(err) 88 | } 89 | go listener(t, testChan, input, l) 90 | for _, tt := range RepeatTests { 91 | testChan <- tt 92 | DefaultTimeout = 1 * time.Second 93 | r := NewRepeater() 94 | buf := bytes.NewBuffer(tt.in) 95 | res, _, err := r.repeat(buf, "localhost:12321", false) 96 | if err != nil { 97 | t.Error(err) 98 | return 99 | } 100 | resBuf, err := ioutil.ReadAll(res) 101 | if err != nil { 102 | t.Error(err) 103 | } 104 | if bytes.Compare(resBuf, tt.out) != 0 { 105 | t.Errorf("Expected <%s> but got <%s>", string(tt.out), string(resBuf)) 106 | } 107 | in := <-input 108 | if bytes.Compare(in, tt.in) != 0 { 109 | t.Errorf("Expected <%s> but got <%s>", string(tt.in), string(in)) 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /sequence/pool.go: -------------------------------------------------------------------------------- 1 | package sequence 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "crypto/tls" 7 | "fmt" 8 | "io" 9 | "net" 10 | "net/http" 11 | "sync" 12 | "time" 13 | 14 | "github.com/empijei/cli/lg" 15 | ) 16 | 17 | var timeout time.Duration = 3 * time.Second 18 | 19 | type pool struct { 20 | wg sync.WaitGroup 21 | cookieName string 22 | throttledone chan struct{} 23 | out chan string 24 | errors chan error 25 | isThrottle bool 26 | req []byte 27 | _tls bool 28 | host string 29 | 30 | //fields not to be accessed by workers 31 | ticker *time.Ticker 32 | done bool 33 | 34 | //error ratio 35 | //cookies 36 | } 37 | 38 | //TODO rethrottle 39 | //TODO expose info on status and errors 40 | //TODO add counter, close when done 41 | 42 | func newPool(nw int, req []byte, _tls bool, host string, reqps int, cookieName string) *pool { 43 | var wg sync.WaitGroup 44 | wg.Add(nw) 45 | 46 | p := &pool{ 47 | wg: wg, 48 | cookieName: cookieName, 49 | throttledone: make(chan struct{}, 7*nw), 50 | out: make(chan string, 7*nw), 51 | errors: make(chan error, 7*nw), 52 | req: req, 53 | _tls: _tls, 54 | host: host, 55 | } 56 | if reqps > 0 { 57 | p.isThrottle = true 58 | p.ticker = time.NewTicker(time.Duration(1000000/reqps) * time.Microsecond) 59 | go func() { 60 | // TODO fix this, this is not good practice 61 | defer func() { 62 | _ = recover() 63 | }() 64 | for range p.ticker.C { 65 | p.throttledone <- struct{}{} 66 | } 67 | }() 68 | } 69 | 70 | for i := 0; i < nw; i++ { 71 | go doWork(p) 72 | } 73 | 74 | go func() { 75 | wg.Wait() 76 | close(p.out) 77 | p.done = true 78 | }() 79 | 80 | return p 81 | } 82 | 83 | func (p *pool) stop() { 84 | close(p.throttledone) 85 | } 86 | 87 | func doWork(p *pool) { 88 | defer p.wg.Done() 89 | 90 | for { 91 | //throttledone is used only as a "done" channel if worker is unthrottled 92 | //and is used to acquire execution tokens if the worker is throttled 93 | if p.isThrottle { 94 | select { 95 | case _, ok := <-p.throttledone: 96 | if !ok { 97 | return 98 | } 99 | } 100 | } else { 101 | select { 102 | case _, ok := <-p.throttledone: 103 | if !ok { 104 | return 105 | } 106 | default: 107 | } 108 | } 109 | 110 | //perform req 111 | resp, err := doReq(p.req, p._tls, p.host) 112 | 113 | //TODO regenerate request 114 | if err != nil { 115 | p.errors <- err 116 | continue 117 | } 118 | 119 | if len(resp.Cookies()) == 0 { 120 | p.errors <- fmt.Errorf("sequence: no cookie received") 121 | continue 122 | } 123 | 124 | //get the right cookie 125 | found := false 126 | for _, cookie := range resp.Cookies() { 127 | if cookie.Name == p.cookieName { 128 | //send cookie over channel 129 | p.out <- cookie.Value 130 | found = true 131 | break 132 | } 133 | } 134 | if !found { 135 | p.errors <- fmt.Errorf("sequence: no session cookie received") 136 | } 137 | } 138 | } 139 | 140 | func doReq(buf []byte, _tls bool, host string) (resp *http.Response, err error) { 141 | var conn net.Conn 142 | if _tls { 143 | //The repeater does not care about certs 144 | conn, err = tls.Dial("tcp", host, &tls.Config{InsecureSkipVerify: true}) 145 | } else { 146 | conn, err = net.Dial("tcp", host) 147 | } 148 | if err != nil { 149 | return 150 | } 151 | defer func() { _ = conn.Close() }() 152 | _ = conn.SetDeadline(time.Now().Add(timeout)) 153 | resbuf := bytes.NewBuffer(nil) 154 | errWrite := make(chan error) 155 | 156 | teebuf := bytes.NewBuffer(buf) 157 | 158 | go func() { 159 | _, errw := io.Copy(conn, teebuf) 160 | errWrite <- errw 161 | }() 162 | 163 | _, err = io.Copy(resbuf, conn) 164 | if tmperr := <-errWrite; tmperr != nil { 165 | err = tmperr 166 | return 167 | } 168 | if err != nil { 169 | return 170 | } 171 | 172 | lg.Debug(string(resbuf.Bytes())) 173 | return http.ReadResponse(bufio.NewReader(resbuf), nil) 174 | } 175 | -------------------------------------------------------------------------------- /sequence/pool_test.go: -------------------------------------------------------------------------------- 1 | // +build !race 2 | 3 | package sequence 4 | 5 | import ( 6 | "fmt" 7 | "net/http" 8 | "net/http/httptest" 9 | "net/url" 10 | "testing" 11 | "time" 12 | ) 13 | 14 | func TestPool(t *testing.T) { 15 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 16 | w.Header().Set("Set-Cookie", "test=foo;") 17 | })) 18 | defer ts.Close() 19 | 20 | URL, _ := url.Parse(ts.URL) 21 | testpayload := `GET / HTTP/1.1 22 | Host: localhost:` + URL.Port() + 23 | ` 24 | User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:54.0) Gecko/20100101 Firefox/54.0 25 | Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 26 | Accept-Language: en-US,en;q=0.5 27 | Connection: close 28 | Upgrade-Insecure-Requests: 1 29 | 30 | 31 | ` 32 | p := newPool(1, []byte(testpayload), false, URL.Host, 1, "test") 33 | testchan := make(chan string) 34 | go func() { 35 | t := time.NewTicker(3 * time.Second) 36 | select { 37 | case <-t.C: 38 | testchan <- "timeout" 39 | case val := <-p.out: 40 | testchan <- val 41 | } 42 | for range p.out { 43 | } 44 | }() 45 | 46 | time.Sleep(2 * time.Second) 47 | fmt.Println("Stopping pool") 48 | p.stop() 49 | cookie := <-testchan 50 | if cookie == "timeout" { 51 | t.Error("Test: no cookie received") 52 | } 53 | if cookie != "foo" { 54 | t.Errorf("Got wrong cookie, expected foo but got <%s>", cookie) 55 | } 56 | var err error 57 | select { 58 | case err = <-p.errors: 59 | default: 60 | } 61 | if err != nil { 62 | t.Error(err) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /sitemap/.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | -------------------------------------------------------------------------------- /sitemap/README.md: -------------------------------------------------------------------------------- 1 | This is going to be used to plot the target map, but is just a stub 2 | -------------------------------------------------------------------------------- /sitemap/mocksy.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "os" 7 | "strconv" 8 | "strings" 9 | 10 | "github.com/empijei/cli/lg" 11 | ) 12 | 13 | // Tree is a struct with a value and its children used to make the sitemap 14 | type Tree struct { 15 | value string 16 | children []*Tree 17 | } 18 | 19 | // HasChildren checks if there are childern and if so, returns them 20 | func (t *Tree) HasChildren(name string) *Tree { 21 | for _, c := range t.children { 22 | if c.value == name { 23 | return c 24 | } 25 | } 26 | return nil 27 | } 28 | 29 | func (t *Tree) dotName(lvl int) string { 30 | buf := bytes.NewBuffer(nil) 31 | buf.WriteString("node") 32 | for _, r := range t.value { 33 | if strings.ContainsRune("QWERTYUIOPASDFGHJKLZXCVBNMqwertyuiopasdfghjklzxcvbnm0123456789", r) { 34 | buf.WriteRune(r) 35 | } 36 | } 37 | return string(buf.Bytes()) + "_" + strconv.Itoa(lvl) 38 | } 39 | 40 | func (t *Tree) String() string { 41 | return "digraph{\n" + t.string(0) + "}" 42 | } 43 | 44 | func (t *Tree) string(lvl int) string { 45 | buf := bytes.NewBuffer(nil) 46 | _, _ = buf.WriteString(t.dotName(lvl)) 47 | _, _ = buf.WriteString(" [label=\"") 48 | _, _ = buf.WriteString(t.value) 49 | _, _ = buf.WriteString("\"];\n") 50 | for _, c := range t.children { 51 | _, _ = buf.WriteString(t.dotName(lvl)) 52 | _, _ = buf.WriteString("->") 53 | _, _ = buf.WriteString(c.dotName(lvl + 1)) 54 | _, _ = buf.WriteString(";\n") 55 | } 56 | for _, c := range t.children { 57 | _, _ = buf.WriteString(c.string(lvl + 1)) 58 | } 59 | return string(buf.Bytes()) 60 | } 61 | 62 | // Path is a struct that decompose an URL into a method and the directories 63 | type Path struct { 64 | method string 65 | directories []string 66 | } 67 | 68 | func main() { 69 | file, err := os.Open("infile.log") 70 | if err != nil { 71 | panic(err) 72 | } 73 | c := make(chan *Path) 74 | done := make(chan struct{}) 75 | start := &Tree{value: "root"} 76 | go func() { 77 | for p := range c { 78 | cur := start 79 | for _, d := range p.directories { 80 | if tmp := cur.HasChildren(d); tmp != nil { 81 | cur = tmp 82 | continue 83 | } 84 | tmp := &Tree{ 85 | value: d, 86 | } 87 | cur.children = append(cur.children, tmp) 88 | cur = tmp 89 | } 90 | } 91 | done <- struct{}{} 92 | }() 93 | s := bufio.NewScanner(file) 94 | for s.Scan() { 95 | nodes := strings.Split(s.Text(), "/") 96 | c <- &Path{ 97 | method: nodes[0], 98 | directories: nodes[1:], 99 | } 100 | } 101 | close(c) 102 | <-done 103 | lg.Info(start) 104 | lg.Info() 105 | } 106 | -------------------------------------------------------------------------------- /status.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/empijei/wapty/config" 5 | "github.com/empijei/wapty/intercept" 6 | ) 7 | 8 | var project = config.NewProject(intercept.GetStatus()) 9 | -------------------------------------------------------------------------------- /ui/apis/README.md: -------------------------------------------------------------------------------- 1 | # WHY 2 | This package is meant to be a lightweight package that both the gopherjs-based UI and the backend can import to share communication constants. 3 | 4 | It is a separate, stand-alone, lightweight package to avoid compiling big parts of the frontend in the backend. 5 | 6 | One more reason to keep these constants all together is to make it easier to build alternative UIs for wapty and to understand which functionalities are exposed by the websocket api. 7 | -------------------------------------------------------------------------------- /ui/apis/command.go: -------------------------------------------------------------------------------- 1 | package apis 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "reflect" 7 | "strconv" 8 | 9 | "github.com/empijei/cli/lg" 10 | ) 11 | 12 | // Command represents a packet of information sent or received by or from the server. 13 | type Command struct { 14 | Channel UIChannel 15 | Action Action 16 | Args map[ArgName]string 17 | Payload []byte 18 | } 19 | 20 | // UnpackArgs is used to extract the value of the arguments from a command. 21 | // cmd is the command to extract the values from, names is a list of ArgName 22 | // that is used to access cmd.Args. 23 | // vars can be pointers to either int, bool or string. This function will attempt 24 | // to deserialize the arguments with the proper type and operations in order 25 | // to fit the given vars types. 26 | // 27 | // WARNING: this function PANICS if len(names) != len(vars) since that surely 28 | // means there is a bug in the code. 29 | func (cmd *Command) UnpackArgs(names []ArgName, vars ...interface{}) (err error) { 30 | if nargs, nvars := len(cmd.Args), len(vars); nargs != nvars { 31 | err := fmt.Sprintf("wrong number of parameters, expected %d but got %d. Args: <%#v>", nvars, nargs, cmd.Args) 32 | lg.Error(err) 33 | return errors.New(err) 34 | } 35 | if nnames, nvars := len(names), len(vars); nnames != nvars { 36 | lg.Failuref("wrong call to ArgsUnpack: given %d names but got %d variables to store them", nnames, nvars) 37 | } 38 | for i := 0; i < len(vars); i++ { 39 | arg := cmd.Args[names[i]] 40 | switch _var := vars[i].(type) { 41 | case *int: 42 | *_var, err = strconv.Atoi(arg) 43 | if err != nil { 44 | lg.Infof("cannot read <%s> as int: %s", arg, err.Error()) 45 | return err 46 | } 47 | case *bool: 48 | *_var = arg == ARG_TRUE 49 | case *string: 50 | *_var = arg 51 | default: 52 | err := fmt.Sprintf("unsupported type passed to ArgsUnpack: %s, only supports pointers to int, string, bool", reflect.TypeOf(_var)) 53 | lg.Error(err) 54 | return errors.New(err) 55 | } 56 | } 57 | return nil 58 | } 59 | 60 | // PackArgs is used to set the value of the arguments of a command. 61 | // 62 | // WARNING: this function PANICS if len(names) != len(vars) since that surely 63 | // means there is a bug in the code. 64 | func (cmd *Command) PackArgs(names []ArgName, vars ...string) { 65 | if nnames, nvars := len(names), len(vars); nnames != nvars { 66 | lg.Failuref("wrong call to ArgsUnpack: given %d names but got %d variables to store them", nnames, nvars) 67 | } 68 | cmd.Args = make(map[ArgName]string) 69 | for i, name := range names { 70 | cmd.Args[name] = vars[i] 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /ui/apis/constants.go: -------------------------------------------------------------------------------- 1 | package apis 2 | 3 | // Action is a string representing the action to perform 4 | type Action string 5 | 6 | const ( 7 | // Editor possible user actions 8 | 9 | // EDT_FORWARD tells the backend to forward the currently intercepted request/response 10 | EDT_FORWARD Action = "forward" 11 | // EDT_EDIT tells the backend to forward the provided payload instead of the original one 12 | EDT_EDIT = "edit" 13 | // EDT_DROP tells the backend to create a dummy response and send it to the client. 14 | // If a request is dropped it won't be forwarded to the server. 15 | EDT_DROP = "drop" 16 | // EDT_PROVIDERESP only has meaning if a request was intercepted. It allows to 17 | // provide a response to the current request without forwarding it to the server. 18 | EDT_PROVIDERESP = "provideResp" 19 | 20 | //History actions 21 | 22 | // HST_DUMP dumps the entire status, this is for debug purposes only 23 | HST_DUMP = "dump" 24 | // HST_FILTER allows to search and filter through history 25 | HST_FILTER = "filter" 26 | // HST_FETCH return the ReqResp given an ID 27 | HST_FETCH = "fetch" 28 | // HST_METADATA returns the metadata for the given ID 29 | HST_METADATA = "metaData" 30 | 31 | //Repeater actions 32 | 33 | // RPT_CREATE Creates a new repeater entry 34 | RPT_CREATE = "create" 35 | // RPT_GO Performs the request 36 | RPT_GO = "go" 37 | // RPT_GET Retrieves an history item 38 | RPT_GET = "get" 39 | 40 | //Settings actions 41 | 42 | // STN_INTERCEPT Gets (no params) or sets (param "ON") the intercept status 43 | STN_INTERCEPT = "intercept" 44 | ) 45 | 46 | const ( 47 | //Possible payloads types 48 | 49 | // PLD_REQUEST should be used to tell the UI if the payload is a request 50 | PLD_REQUEST = "request" 51 | // PLD_RESPONSE should be used to tell the UI if the payload is a response 52 | PLD_RESPONSE = "response" 53 | ) 54 | 55 | // ArgName is the type of the set of Keys to use in the Args map of a command 56 | type ArgName string 57 | 58 | const ( 59 | // ARG_ID is used to identify which item of the collection should be fetched 60 | ARG_ID ArgName = "id" 61 | // ARG_SUBID is used to identify which item of the collection should be fetched. 62 | // This is applied only if the resource identified by ID contains a collection. 63 | ARG_SUBID = "subId" 64 | // ARG_PAYLOADTYPE is used by editor to distinguish between requests and responses. 65 | ARG_PAYLOADTYPE = "payloadType" 66 | // ARG_ENDPOINT is used to refer to the host:port or schema://host:port 67 | ARG_ENDPOINT = "endpoint" 68 | // ARG_ERR is a value used to communicate an error occourred 69 | ARG_ERR = "error" 70 | // ARG_TLS is used as a bool to tell if ARG_TLS must be used for the specified operation 71 | ARG_TLS = "tls" 72 | // ARG_TRUE is used to deserialize a bool from a string. 73 | ARG_TRUE = "true" 74 | // ARG_FALSE is used to deserialize a bool from a string. 75 | ARG_FALSE = "" 76 | // ARG_ON is used as a key value for togglable settings 77 | ARG_ON = "on" 78 | ) 79 | 80 | // UIChannel is a string used to multiplex on the websocket and route commands 81 | // to the proper packages 82 | type UIChannel string 83 | 84 | const ( 85 | // CHN_EDITOR channel used by intercept package, editor actions 86 | CHN_EDITOR UIChannel = "proxy/intercept/editor" 87 | // CHN_HISTORY channel used by intercept package, history actions 88 | CHN_HISTORY = "proxy/httpHistory" 89 | // CHN_REPEAT channel used by repeat package 90 | CHN_REPEAT = "repeat" 91 | // CHN_INTERCEPTSETTINGS channel used by intercept package, history actions 92 | CHN_INTERCEPTSETTINGS = "proxy/intercept/options" 93 | ) 94 | -------------------------------------------------------------------------------- /ui/apis/history.go: -------------------------------------------------------------------------------- 1 | package apis 2 | 3 | // ReqRespMetaData is a wrapper type to hold all metadata on a status ReqResp 4 | type ReqRespMetaData struct { 5 | ID int 6 | Host string 7 | Method string 8 | Path string 9 | Params bool 10 | Edited bool 11 | Status string 12 | Length int64 13 | ContentType string 14 | Extension string 15 | TLS bool 16 | IP string 17 | Port string 18 | Cookies string 19 | Time string 20 | /* 21 | Port! 22 | Title (maybe not?) 23 | Comment (user-defined) 24 | */ 25 | } 26 | 27 | // ReqResp Represents an item of the proxy history 28 | //TODO create a test that fails if this is different from intercept.ReqResp 29 | type ReqResp struct { 30 | //Unique ID in the history 31 | ID int 32 | //Meta Data about both Req and Resp 33 | MetaData *ReqRespMetaData 34 | //Original Request 35 | RawReq []byte 36 | //Original Response 37 | RawRes []byte 38 | //Edited Request 39 | RawEditedReq []byte 40 | //Edited Response 41 | RawEditedRes []byte 42 | } 43 | -------------------------------------------------------------------------------- /ui/apis/utils.go: -------------------------------------------------------------------------------- 1 | package apis 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // Err returns a command that contains the error message as argument and ERR as action 8 | func Err(m interface{}) *Command { 9 | message := fmt.Sprint(m) 10 | return &Command{ 11 | Action: ARG_ERR, 12 | Args: map[ArgName]string{ARG_ERR: message}, 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /ui/gopherjs/domelement.go: -------------------------------------------------------------------------------- 1 | // +build js 2 | 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | "strings" 8 | 9 | "github.com/empijei/cli/lg" 10 | "github.com/gopherjs/gopherjs/js" 11 | ) 12 | 13 | var document *js.Object 14 | 15 | type DomElement struct { 16 | *js.Object 17 | } 18 | 19 | func toString(o *js.Object) string { 20 | if o == nil || o == js.Undefined { 21 | return "" 22 | } 23 | return o.String() 24 | } 25 | 26 | func GetElementByID(id string) *DomElement { 27 | return &DomElement{js.Global.Get(id)} 28 | } 29 | 30 | func (de *DomElement) SetTextContent(content string) { 31 | de.Set("textContent", content) 32 | } 33 | func (de *DomElement) SetTextContentf(format string, args ...interface{}) { 34 | de.Set("textContent", fmt.Sprintf(format, args...)) 35 | } 36 | 37 | func (de *DomElement) GetTextContent() string { 38 | return toString(de.Get("textContent")) 39 | } 40 | 41 | func (de *DomElement) ToggleClass(old, new string) { 42 | oldclasses := strings.Split(toString(de.Get("classList")), " ") 43 | lg.Debugf("Oldclasses: %v", oldclasses) 44 | newclasses := make([]string, 0, len(oldclasses)+1) 45 | var replaced bool 46 | for _, class := range oldclasses { 47 | if class == old { 48 | replaced = true 49 | if new != "" { 50 | newclasses = append(newclasses, new) 51 | } 52 | } else { 53 | newclasses = append(newclasses, class) 54 | } 55 | } 56 | if !replaced { 57 | newclasses = append(newclasses, new) 58 | } 59 | 60 | lg.Debugf("New classes: %v", newclasses) 61 | de.Set("classList", strings.Join(newclasses, " ")) 62 | } 63 | 64 | func (de *DomElement) SetAttribute(name string, value string) { 65 | de.Call("setAttribute", name, value) 66 | } 67 | 68 | func (de *DomElement) SetAttributes(keyvalues map[string]string) { 69 | for attribute, value := range keyvalues { 70 | de.SetAttribute(attribute, value) 71 | } 72 | } 73 | 74 | func (de *DomElement) AppendChild(child *DomElement) { 75 | de.Call("appendChild", child.Object) 76 | } 77 | 78 | func createElement(name string) *DomElement { 79 | return &DomElement{document.Call("createElement", name)} 80 | } 81 | -------------------------------------------------------------------------------- /ui/gopherjs/history.go: -------------------------------------------------------------------------------- 1 | // +build js 2 | 3 | package main 4 | 5 | import ( 6 | "encoding/json" 7 | "fmt" 8 | "reflect" 9 | 10 | . "github.com/empijei/wapty/ui/apis" 11 | "github.com/gopherjs/gopherjs/js" 12 | ) 13 | 14 | var ( 15 | tmpHistory = make(map[int]map[string]*js.Object) 16 | ) 17 | 18 | func handleHistory(msg Command) { 19 | switch msg.Action { 20 | case HST_METADATA: 21 | var md ReqRespMetaData 22 | err := json.Unmarshal([]byte(msg.Args[HST_METADATA]), &md) 23 | if err != nil { 24 | panic(err) 25 | } 26 | 27 | if rowMap, ok := tmpHistory[md.ID]; !ok { 28 | row := historyTbody.Call("insertRow", -1) 29 | tmp := make(map[string]*js.Object) 30 | 31 | val := reflect.Indirect(reflect.ValueOf(md)) 32 | for i := 0; i < val.Type().NumField(); i++ { 33 | typeField := val.Type().Field(i) 34 | cell := row.Call("insertCell", -1) 35 | valueField := val.Field(i).Interface() 36 | cell.Set("innerText", fmt.Sprintf("%v", valueField)) 37 | tmp[typeField.Name] = cell 38 | } 39 | tmpHistory[md.ID] = tmp 40 | } else { 41 | val := reflect.Indirect(reflect.ValueOf(md)) 42 | for i := 0; i < val.Type().NumField(); i++ { 43 | typeField := val.Type().Field(i) 44 | cell := rowMap[typeField.Name] 45 | valueField := val.Field(i).Interface() 46 | cell.Set("innerText", fmt.Sprintf("%v", valueField)) 47 | } 48 | //FIXME this is commented because it looks like the page 49 | //receives the same metadata multiple times. 50 | //delete(tmpHistory, md.Id) 51 | } 52 | case HST_FETCH: 53 | var rr ReqResp 54 | err := json.Unmarshal(msg.Payload, &rr) 55 | if err != nil { 56 | panic(err) 57 | } 58 | //TODO check if is printable, otherwise show hex 59 | historyReqBuffer.SetTextContent(string(rr.RawReq)) 60 | historyResBuffer.SetTextContent(string(rr.RawRes)) 61 | } 62 | } 63 | 64 | //DOM Stuff 65 | var ( 66 | historyTbody *js.Object 67 | historyReqBuffer *DomElement 68 | historyResBuffer *DomElement 69 | ) 70 | 71 | func init() { 72 | historyTbody = js.Global.Get("historyTbody") 73 | historyReqBuffer = GetElementByID("historyReqBuffer") 74 | historyResBuffer = GetElementByID("historyResBuffer") 75 | 76 | hth := js.Global.Get("historyHeader") 77 | 78 | //This is used to make the ui adapt to backend changes in metadata 79 | val := reflect.Indirect(reflect.ValueOf(ReqRespMetaData{})) 80 | for i := 0; i < val.Type().NumField(); i++ { 81 | hth.Call("insertCell", -1).Set("innerText", val.Type().Field(i).Name) 82 | } 83 | 84 | js.Global.Set("hist", map[string]interface{}{ 85 | "onHistoryCellClick": onHistoryCellClick, 86 | }) 87 | 88 | } 89 | 90 | func onHistoryCellClick() { 91 | proxyAction(Command{ 92 | Action: HST_FETCH, 93 | Channel: CHN_HISTORY, 94 | Args: map[ArgName]string{ARG_ID: js.Global.Get("event").Get("target").Get("parentNode").Get("childNodes").Index(0).Get("textContent").String()}, 95 | }, true) 96 | } 97 | -------------------------------------------------------------------------------- /ui/gopherjs/main.go: -------------------------------------------------------------------------------- 1 | // +build js 2 | 3 | package main 4 | 5 | //FIXME ui does not respond while receiving big amounts of metadata 6 | 7 | import ( 8 | "encoding/json" 9 | 10 | "github.com/empijei/cli/lg" 11 | . "github.com/empijei/wapty/ui/apis" 12 | js "github.com/gopherjs/gopherjs/js" 13 | "github.com/gopherjs/websocket" 14 | ) 15 | 16 | var ( 17 | dec *json.Decoder 18 | enc *json.Encoder 19 | interceptOn bool 20 | ) 21 | 22 | func send(cmd Command) error { 23 | return enc.Encode(cmd) 24 | } 25 | 26 | func logger(cmd *Command) { 27 | lg.Infof("Received actions %s on channel %s", cmd.Action, cmd.Channel) 28 | } 29 | 30 | func main() { 31 | document = js.Global.Get("document") 32 | 33 | waptyServer, err := websocket.Dial("ws://localhost:8081/ws") 34 | if err != nil { 35 | //FIXME handle error 36 | panic(err) 37 | } 38 | 39 | lg.Debug("WebSocket connetcted") 40 | dec = json.NewDecoder(waptyServer) 41 | enc = json.NewEncoder(waptyServer) 42 | 43 | var msg Command 44 | 45 | msg.Action = STN_INTERCEPT 46 | msg.Channel = CHN_INTERCEPTSETTINGS 47 | 48 | err = send(msg) 49 | if err != nil { 50 | panic(err) 51 | } 52 | 53 | for { 54 | var msg Command 55 | err = dec.Decode(&msg) 56 | logger(&msg) 57 | if err != nil { 58 | panic(err) 59 | } 60 | switch msg.Channel { 61 | case CHN_EDITOR: 62 | handleEdit(msg) 63 | 64 | case CHN_INTERCEPTSETTINGS: 65 | handleIntercept(msg) 66 | 67 | case CHN_HISTORY: 68 | handleHistory(msg) 69 | 70 | case CHN_REPEAT: 71 | handleRepeat(msg) 72 | 73 | default: 74 | lg.Error("Unrecognized message") 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /ui/gopherjs/proxy.go: -------------------------------------------------------------------------------- 1 | // +build js 2 | 3 | package main 4 | 5 | import ( 6 | "github.com/empijei/cli/lg" 7 | . "github.com/empijei/wapty/ui/apis" 8 | "github.com/gopherjs/gopherjs/js" 9 | ) 10 | 11 | func handleEdit(msg Command) { 12 | proxyBuffer.SetTextContent(string(msg.Payload)) 13 | var text string 14 | if msg.Args[ARG_PAYLOADTYPE] == PLD_REQUEST { 15 | text = "Request for: " 16 | } else { 17 | text = "Response from: " 18 | } 19 | endpointIndicator.SetTextContent(text + msg.Args[ARG_ENDPOINT]) 20 | controls = true 21 | } 22 | 23 | func handleIntercept(msg Command) { 24 | switch msg.Action { 25 | case STN_INTERCEPT: 26 | if msg.Args[ARG_ON] == ARG_TRUE { 27 | btn.ToggleClass("btn-danger", "btn-success") 28 | btn.SetTextContent("Intercept is on") 29 | interceptOn = true 30 | } else { 31 | btn.ToggleClass("btn-success", "btn-danger") 32 | btn.SetTextContent("Intercept is off") 33 | interceptOn = false 34 | } 35 | } 36 | } 37 | 38 | //DOM Stuff 39 | var ( 40 | proxyBuffer *DomElement 41 | endpointIndicator *DomElement 42 | btn *DomElement 43 | controls bool 44 | ) 45 | 46 | func init() { 47 | proxyBuffer = GetElementByID("proxybuffer") 48 | endpointIndicator = GetElementByID("endpointIndicator") 49 | btn = GetElementByID("interceptToggle") 50 | js.Global.Set("proxy", map[string]interface{}{ 51 | "onDropClick": onDropClick, 52 | "onForwardModifiedClick": onForwardModifiedClick, 53 | "onForwardOriginalClick": onForwardOriginalClick, 54 | "onProvideResponseClick": onProvideResponseClick, 55 | "onToggleInterceptClick": onToggleInterceptClick, 56 | "onHistoryCellClick": onHistoryCellClick, 57 | }) 58 | } 59 | 60 | func proxyAction(msg Command, ignoreControls bool) { 61 | lg.Debugf("Requested action %s", msg.Action) 62 | if !ignoreControls { 63 | if !controls { 64 | return 65 | } 66 | controls = false 67 | } 68 | lg.Debugf("Performing action %s", msg.Action) 69 | err := enc.Encode(msg) 70 | if err != nil { 71 | panic(err) 72 | } 73 | proxyBuffer.SetTextContent("") 74 | endpointIndicator.SetTextContent("") 75 | lg.Debugf("Action %s invoked", msg.Action) 76 | } 77 | 78 | func onForwardOriginalClick() { 79 | proxyAction(Command{ 80 | Action: EDT_FORWARD, 81 | Channel: CHN_EDITOR, 82 | }, false) 83 | } 84 | 85 | func onForwardModifiedClick() { 86 | proxyAction(Command{ 87 | Action: EDT_EDIT, 88 | Channel: CHN_EDITOR, 89 | Payload: []byte(proxyBuffer.GetTextContent()), 90 | }, false) 91 | } 92 | 93 | func onDropClick() { 94 | proxyAction(Command{ 95 | Action: EDT_DROP, 96 | Channel: CHN_EDITOR, 97 | }, false) 98 | } 99 | 100 | func onProvideResponseClick() { 101 | proxyAction(Command{ 102 | Action: EDT_PROVIDERESP, 103 | Channel: CHN_EDITOR, 104 | Payload: []byte(proxyBuffer.GetTextContent()), 105 | }, false) 106 | } 107 | 108 | func onToggleInterceptClick() { 109 | var msg string 110 | if interceptOn { 111 | msg = ARG_FALSE 112 | } else { 113 | msg = ARG_TRUE 114 | } 115 | 116 | var buf string 117 | if controls && interceptOn { 118 | buf = proxyBuffer.GetTextContent() 119 | lg.Debugf("there is a buffer that will be forwarded, value: %s", buf) 120 | } 121 | 122 | proxyAction(Command{ 123 | Action: STN_INTERCEPT, 124 | Channel: CHN_INTERCEPTSETTINGS, 125 | Args: map[ArgName]string{ARG_ON: msg}, 126 | }, true) 127 | 128 | // If the proxy had a payload when intercept was turned off we assume it was 129 | // modified 130 | if buf != "" { 131 | lg.Debugf("forwarding buffer") 132 | proxyAction(Command{ 133 | Action: EDT_EDIT, 134 | Channel: CHN_EDITOR, 135 | Payload: []byte(buf), 136 | }, false) 137 | } 138 | interceptOn = !interceptOn 139 | } 140 | -------------------------------------------------------------------------------- /ui/gopherjs/repeat.go: -------------------------------------------------------------------------------- 1 | // +build js 2 | 3 | package main 4 | 5 | import . "github.com/empijei/wapty/ui/apis" 6 | 7 | func handleRepeat(mdg Command) { 8 | 9 | } 10 | 11 | //DOM Stuff 12 | func addRepeaterTab(title, request, response string) {} 13 | -------------------------------------------------------------------------------- /ui/gopherjs/tabgroup.go: -------------------------------------------------------------------------------- 1 | // +build js 2 | 3 | package main 4 | 5 | import "strconv" 6 | 7 | const tabtitletmpl = ` 8 |
  • 9 | 10 | Tab {{.Title}} 11 | 14 | 15 |
  • 16 | ` 17 | 18 | const tabcontenttmpl = ` 19 |
    20 | {{.Content}} 21 |
    22 | ` 23 | 24 | const tabHeadersContainertmpl = ` 26 | ` 27 | 28 | const tabBodiesContainertmpl = `
    29 |
    Tab 1 content
    30 | ` 31 | 32 | type TabGroup struct { 33 | Name string 34 | parentNode DomElement 35 | tabHeaders DomElement 36 | tabBodies DomElement 37 | currentID int 38 | Tabs []Tab 39 | } 40 | 41 | func newTabGroup(name string, parentNode DomElement) *TabGroup { 42 | //BOOKMARK 43 | return nil 44 | } 45 | 46 | type Tab struct { 47 | GroupName string 48 | ID int 49 | Title string 50 | Content string 51 | } 52 | 53 | func (tg *TabGroup) addTab(title string, content string) (tabID int) { 54 | tg.currentID++ 55 | t := Tab{ 56 | GroupName: tg.Name, 57 | ID: tg.currentID, 58 | Title: title, 59 | Content: content, 60 | } 61 | tg.Tabs = append(tg.Tabs, t) 62 | 63 | refid := "tab" + tg.Name + strconv.Itoa(tg.currentID) 64 | //Creating the tab header 65 | li := createElement("li") 66 | //Creating the link to the tab content 67 | a := createElement("a") 68 | a.SetAttributes(map[string]string{ 69 | "href": "#" + refid, 70 | "role": "tab", 71 | "data-toggle": "tab"}) 72 | a.SetTextContentf("Tab header for tab %d of tabgroup %s, %s", tg.currentID, tg.Name, title) 73 | //Creating the ✗ button for the tab 74 | button := createElement("button") 75 | button.SetAttributes(map[string]string{ 76 | "class": "close", 77 | "type": "button", 78 | "title": "Remove this tab"}) 79 | button.SetTextContent("✗") 80 | //Putting all together 81 | a.AppendChild(button) 82 | li.AppendChild(a) 83 | tg.tabHeaders.AppendChild(li) 84 | 85 | //Creating tab body 86 | div := createElement("div") 87 | div.SetAttributes(map[string]string{ 88 | "class": "tab-pane fade", 89 | "id": refid, 90 | }) 91 | div.Set("innerHTML", content) 92 | //Adding tab body to DOM 93 | tg.tabBodies.AppendChild(div) 94 | 95 | return tg.currentID 96 | } 97 | -------------------------------------------------------------------------------- /ui/server.go: -------------------------------------------------------------------------------- 1 | //Package ui is a general high level representation of all the uis connected to the current 2 | //instance of Wapty. Use this from other packages to read user input and write 3 | //output 4 | package ui 5 | 6 | import ( 7 | "encoding/json" 8 | "io" 9 | "net/http" 10 | "sync" 11 | 12 | rice "github.com/GeertJohan/go.rice" 13 | "github.com/empijei/wapty/ui/apis" 14 | 15 | "github.com/empijei/cli/lg" 16 | "golang.org/x/net/websocket" 17 | ) 18 | 19 | const BUFSIZE = 1024 20 | 21 | var inc = make(chan apis.Command, BUFSIZE) 22 | var outg = make(chan apis.Command, BUFSIZE) 23 | 24 | var connMut sync.Mutex 25 | var uiconn io.ReadWriteCloser 26 | 27 | func serve(pattern string) { 28 | // websocket handler 29 | onConnected := func(ws *websocket.Conn) { 30 | lg.Info("A client has connected") 31 | defer func() { 32 | _ = ws.Close() 33 | }() 34 | 35 | connMut.Lock() 36 | if uiconn != nil { 37 | //TODO tell the new UI to GTFO 38 | lg.Info("A UI is already connected") 39 | return 40 | } 41 | uiconn = ws 42 | connMut.Unlock() 43 | 44 | //This is a blocking call 45 | handleClient(uiconn) 46 | 47 | connMut.Lock() 48 | uiconn = nil 49 | connMut.Unlock() 50 | 51 | } 52 | 53 | http.Handle(pattern, websocket.Handler(onConnected)) 54 | for cmd := range inc { 55 | subsMutex.RLock() 56 | for _, out := range subscriptions[cmd.Channel] { 57 | out.dataCh <- cmd 58 | } 59 | subsMutex.RUnlock() 60 | } 61 | } 62 | 63 | func handleClient(uiconn io.ReadWriteCloser) { 64 | dedicatedchan := make(chan apis.Command) 65 | 66 | //This copyes the commands from the backend to a channel read by the sender goroutine. 67 | //when the channel is closed it gracefully handles the panic and prevent the last message from being lost. 68 | go func() { 69 | var cmd apis.Command 70 | defer func() { 71 | if r := recover(); r != nil { 72 | //cmd was not sent successfully, let's save it 73 | outg <- cmd 74 | } 75 | lg.Debug("Copyer terminated") 76 | }() 77 | for cmd = range outg { 78 | dedicatedchan <- cmd 79 | } 80 | }() 81 | 82 | //Sender goroutine, transmits data from the dedicadedchan to the ui 83 | go func() { 84 | enc := json.NewEncoder(uiconn) 85 | for cmd := range dedicatedchan { 86 | err := enc.Encode(cmd) 87 | if err != nil { 88 | lg.Error(err) 89 | break 90 | } 91 | } 92 | err := uiconn.Close() 93 | lg.Error(err) 94 | lg.Debug("Sender terminated") 95 | }() 96 | 97 | //Takes commands from the ui and sends them to the backend. 98 | //When the connection is closed this exits and signals the sender to stop. 99 | dec := json.NewDecoder(uiconn) 100 | var cmd apis.Command 101 | for { 102 | err := dec.Decode(&cmd) 103 | if err != nil { 104 | err2 := uiconn.Close() 105 | lg.Error(err2) 106 | lg.Error(err) 107 | break 108 | } 109 | inc <- cmd 110 | } 111 | close(dedicatedchan) 112 | lg.Info("A client has disconnected") 113 | } 114 | 115 | func send(cmd *apis.Command) { 116 | outg <- *cmd 117 | } 118 | 119 | // MainLoop is the UI's mainloop. It should be run on wapty's start and it will 120 | // not return until an error occours. 121 | func MainLoop() { 122 | // websocket server 123 | go serve("/ws") 124 | 125 | // static files 126 | http.Handle("/static/", 127 | http.StripPrefix("/static/", 128 | http.FileServer(rice.MustFindBox("static").HTTPBox()))) 129 | // TODO setup templates 130 | 131 | loadTemplates() 132 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 133 | _, _ = w.Write(appPage) 134 | }) 135 | lg.Infof("UI is running on: http://localhost:%d/", 8081) 136 | lg.Failure(http.ListenAndServe(":8081", nil)) 137 | 138 | } 139 | -------------------------------------------------------------------------------- /ui/static/colresizable.min.js: -------------------------------------------------------------------------------- 1 | // colResizable 1.6 - a jQuery plugin by Alvaro Prieto Lauroba http://www.bacubacu.com/colresizable/ 2 | 3 | !function(t){var e,i=t(document),r=t("head"),o=null,s={},d=0,n="id",a="px",l="JColResizer",c="JCLRFlex",f=parseInt,h=Math,p=navigator.userAgent.indexOf("Trident/4.0")>0;try{e=sessionStorage}catch(g){}r.append("");var u=function(e,i){var o=t(e);if(o.opt=i,o.mode=i.resizeMode,o.dc=o.opt.disabledColumns,o.opt.disable)return w(o);var a=o.id=o.attr(n)||l+d++;o.p=o.opt.postbackSafe,!o.is("table")||s[a]&&!o.opt.partialRefresh||("col-resize"!==o.opt.hoverCursor&&r.append(""),o.addClass(l).attr(n,a).before('
    '),o.g=[],o.c=[],o.w=o.width(),o.gc=o.prev(),o.f=o.opt.fixed,i.marginLeft&&o.gc.css("marginLeft",i.marginLeft),i.marginRight&&o.gc.css("marginRight",i.marginRight),o.cs=f(p?e.cellSpacing||e.currentStyle.borderSpacing:o.css("border-spacing"))||2,o.b=f(p?e.border||e.currentStyle.borderLeftWidth:o.css("border-left-width"))||1,s[a]=o,v(o))},w=function(t){var e=t.attr(n),t=s[e];t&&t.is("table")&&(t.removeClass(l+" "+c).gc.remove(),delete s[e])},v=function(i){var r=i.find(">thead>tr:first>th,>thead>tr:first>td");r.length||(r=i.find(">tbody>tr:first>th,>tr:first>th,>tbody>tr:first>td, >tr:first>td")),r=r.filter(":visible"),i.cg=i.find("col"),i.ln=r.length,i.p&&e&&e[i.id]&&m(i,r),r.each(function(e){var r=t(this),o=-1!=i.dc.indexOf(e),s=t(i.gc.append('
    ')[0].lastChild);s.append(o?"":i.opt.gripInnerHtml).append('
    '),e==i.ln-1&&(s.addClass("JCLRLastGrip"),i.f&&s.html("")),s.bind("touchstart mousedown",J),o?s.addClass("JCLRdisabledGrip"):s.removeClass("JCLRdisabledGrip").bind("touchstart mousedown",J),s.t=i,s.i=e,s.c=r,r.w=r.width(),i.g.push(s),i.c.push(r),r.width(r.w).removeAttr("width"),s.data(l,{i:e,t:i.attr(n),last:e==i.ln-1})}),i.cg.removeAttr("width"),i.find("td, th").not(r).not("table th, table td").each(function(){t(this).removeAttr("width")}),i.f||i.removeAttr("width").addClass(c),C(i)},m=function(t,i){var r,o,s=0,d=0,n=[];if(i){if(t.cg.removeAttr("width"),t.opt.flush)return void(e[t.id]="");for(r=e[t.id].split(";"),o=r[t.ln+1],!t.f&&o&&(t.width(o*=1),t.opt.overflow&&(t.css("min-width",o+a),t.w=o));d*{cursor:"+n.opt.dragCursor+"!important}"),a.addClass(n.opt.draggingClass),o=a,n.c[d.i].l)for(var f,h=0;h 4 | 5 | 6 | 7 | 8 |
      9 | {{range pagine write title}} 10 |
    11 |
    12 | {{range pagine write content}} 13 |
    14 | 15 | ``` 16 | 17 | # Pagina 18 | .Title 19 | .Content 20 | 21 | # Tree 22 | * Frame 23 | * Tabcontent 24 | * Tabcontent 25 | * Tabcontent 26 | * Tabcontent 27 | * Tabcontent 28 | 29 | -------------------------------------------------------------------------------- /ui/subs.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/empijei/wapty/ui/apis" 7 | ) 8 | 9 | const SUBBUFFERSIZE = 50 10 | 11 | type subsChannel map[int]subscriptionImpl 12 | 13 | var subscriptions = make(map[apis.UIChannel]subsChannel) 14 | var subsMutex sync.RWMutex 15 | var subsCounter int 16 | 17 | // Subscription is the high-level representation of a connection between a wapty 18 | // component and wapty UI. It will multiplex on apis.UIChannel transparently. 19 | type Subscription interface { 20 | Receive() apis.Command 21 | RecChannel() <-chan apis.Command 22 | Send(*apis.Command) 23 | } 24 | 25 | type subscriptionImpl struct { 26 | id int 27 | channel apis.UIChannel 28 | dataCh chan apis.Command 29 | } 30 | 31 | // Subscribe allows a package to start receiving and sending commands over a apis.UIChannel 32 | func Subscribe(channel apis.UIChannel) Subscription { 33 | subsMutex.Lock() 34 | subsCounter++ 35 | //Unless you are sure the out channel will be constantly read, it is strongly 36 | //suggested to create a buffered channel 37 | pipe := make(chan apis.Command, SUBBUFFERSIZE) 38 | out := subscriptionImpl{id: subsCounter, dataCh: pipe, channel: channel} 39 | if subscriptions[channel] == nil { 40 | subscriptions[channel] = make(map[int]subscriptionImpl) 41 | } 42 | subscriptions[channel][subsCounter] = out 43 | out.dataCh = pipe 44 | subsMutex.Unlock() 45 | return &out 46 | } 47 | 48 | // Receive blocks until a command is received 49 | func (s *subscriptionImpl) Receive() apis.Command { 50 | return <-s.dataCh 51 | } 52 | 53 | // RecChannel returns a read-only channel to receive commands. Use this only for 54 | // select statements. If you just need to receiv a command in a blocking way 55 | // please use Receive instead 56 | func (s *subscriptionImpl) RecChannel() <-chan apis.Command { 57 | return s.dataCh 58 | } 59 | 60 | // Send sends the command and sets the channel with the value set in the subscription 61 | func (s *subscriptionImpl) Send(c *apis.Command) { 62 | c.Channel = s.channel 63 | send(c) 64 | } 65 | -------------------------------------------------------------------------------- /ui/templates/history.html: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 | 5 |
    6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
    15 | 16 |
    17 |
    18 |
    19 | 20 |
    21 |
    22 |
    23 | 24 | 25 | 26 |
    27 |
    28 | 29 | 30 | 31 |
    32 |
    33 |
    34 |
    35 | -------------------------------------------------------------------------------- /ui/templates/index.tmpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | Wapty 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
    18 |
    19 | 29 | 30 |
    31 | {{range .Tabs}} 32 | 33 |
    34 | {{.Content}} 35 |
    36 | 37 | {{end}} 38 | 39 |
    40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /ui/templates/proxy.html: -------------------------------------------------------------------------------- 1 |
    2 | 3 | 4 | 5 | 6 | 7 | 14 |
    15 |
    16 | 17 | 18 |
    19 | 20 | -------------------------------------------------------------------------------- /ui/templates/repeat.html: -------------------------------------------------------------------------------- 1 | 6 | 7 |

    8 | 9 |

    10 | 13 | 14 |
    15 |
    Tab 1 content
    16 |
    17 | 18 | 56 | 57 | -------------------------------------------------------------------------------- /ui/webui.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io/ioutil" 7 | "strings" 8 | "text/template" 9 | 10 | rice "github.com/GeertJohan/go.rice" 11 | ) 12 | 13 | var tabs = []struct { 14 | Name string 15 | Title string 16 | }{ 17 | { 18 | Name: "proxy", 19 | }, 20 | { 21 | Name: "history", 22 | Title: "HTTP History", 23 | }, 24 | { 25 | Name: "repeat", 26 | }, 27 | } 28 | 29 | type Tab struct { 30 | Title string 31 | Active bool 32 | ID string 33 | Content string 34 | } 35 | 36 | type Index struct { 37 | Tabs []*Tab 38 | } 39 | 40 | var names = make(map[string]struct{}) 41 | 42 | var appPage []byte 43 | 44 | var templatesFolder *rice.Box 45 | 46 | func loadTemplates() { 47 | templatesFolder = rice.MustFindBox("templates") 48 | indexraw := mustreadall("index.tmpl") 49 | indextmpl := template.Must(template.New("index").Parse(indexraw)) 50 | home := new(Index) 51 | for i, tab := range tabs { 52 | home.Tabs = append(home.Tabs, loadTab(tab.Name, tab.Title, i == 0)) 53 | } 54 | buf := bytes.NewBuffer(nil) 55 | err := indextmpl.Execute(buf, home) 56 | if err != nil { 57 | panic(err) 58 | } 59 | appPage = buf.Bytes() 60 | } 61 | 62 | func loadTab(name string, title string, active bool) *Tab { 63 | //names must be unique 64 | if _, ok := names[name]; ok { 65 | panic(fmt.Errorf("name %s already in use!", name)) 66 | } 67 | names[name] = struct{}{} 68 | if title == "" { 69 | title = strings.Title(name) 70 | } 71 | return &Tab{ 72 | Title: title, 73 | Active: active, 74 | ID: name, 75 | Content: mustreadall(name + ".html"), 76 | } 77 | } 78 | 79 | func mustreadall(path string) string { 80 | file, err := templatesFolder.Open(path) 81 | if err != nil { 82 | panic(err) 83 | } 84 | buf, err := ioutil.ReadAll(file) 85 | if err != nil { 86 | panic(err) 87 | } 88 | return string(buf) 89 | } 90 | -------------------------------------------------------------------------------- /ui/webui_test.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import "testing" 4 | 5 | func TestLoad(t *testing.T) { 6 | loadTemplates() 7 | t.Log(string(appPage)) 8 | } 9 | -------------------------------------------------------------------------------- /wapty.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | 5 | //Packages imported for initialization purposes 6 | _ "github.com/empijei/wapty/decode" 7 | _ "github.com/empijei/wapty/mocksy" 8 | 9 | "github.com/empijei/wapty/cli" 10 | "github.com/empijei/wapty/intercept" 11 | "github.com/empijei/wapty/ui" 12 | ) 13 | 14 | var cmdProxy = &cli.Cmd{ 15 | Name: "proxy", 16 | Run: func(...string) { 17 | go ui.MainLoop() 18 | intercept.MainLoop() 19 | }, 20 | UsageLine: "proxy", 21 | Short: "work as a proxy", 22 | Long: "", 23 | } 24 | 25 | func init() { 26 | cli.AddCommand(cmdProxy) 27 | } 28 | 29 | func main() { 30 | cli.DefaultCommand = cmdProxy 31 | cli.Init() 32 | } 33 | --------------------------------------------------------------------------------