├── .gitignore ├── cmd ├── godship │ ├── README.md │ └── main.go └── godet │ └── main.go ├── .github └── workflows │ └── go.yml ├── LICENSE ├── examples ├── mobile.go ├── parallel.go └── example.go ├── README.md └── godet.go /.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 | 26 | .idea 27 | -------------------------------------------------------------------------------- /cmd/godship/README.md: -------------------------------------------------------------------------------- 1 | # godship 2 | A shell to interact with Chrome debugger 3 | 4 | ## Available commands (use 'help '): 5 | 6 | echo eval events exit expr foreach format function 7 | go help if json jsonpath load navigate output 8 | protocol query repeat set sleep stop tabs time 9 | var verbose version 10 | 11 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | on: [push] 3 | 4 | env: 5 | GO111MODULE: auto 6 | 7 | jobs: 8 | 9 | build: 10 | name: Build 11 | runs-on: ubuntu-latest 12 | steps: 13 | 14 | - name: Set up Go 1.18 15 | uses: actions/setup-go@v4 16 | with: 17 | go-version: 1.18 18 | id: go 19 | 20 | - name: Check out code into the Go module directory 21 | uses: actions/checkout@v3 22 | 23 | - name: Get dependencies 24 | run: | 25 | go get -v -t -d ./... 26 | if [ -f Gopkg.toml ]; then 27 | curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh 28 | dep ensure 29 | fi 30 | 31 | - name: Build 32 | run: go build -v . 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Raffaele Sena 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/mobile.go: -------------------------------------------------------------------------------- 1 | //go:build ignore 2 | // +build ignore 3 | 4 | package main 5 | 6 | import "fmt" 7 | import "time" 8 | 9 | import "github.com/raff/godet" 10 | 11 | func main() { 12 | // connect to Chrome instance 13 | remote, _ := godet.Connect("localhost:9222", false) 14 | 15 | // disconnect when done 16 | defer remote.Close() 17 | 18 | // get browser and protocol version 19 | version, _ := remote.Version() 20 | fmt.Println(version) 21 | 22 | // install some callbacks 23 | remote.CallbackEvent(godet.EventClosed, func(params godet.Params) { 24 | fmt.Println("RemoteDebugger connection terminated.") 25 | }) 26 | 27 | remote.CallbackEvent("Network.requestWillBeSent", func(params godet.Params) { 28 | fmt.Println("requestWillBeSent", 29 | params["type"], 30 | params["documentURL"], 31 | params["request"].(map[string]interface{})["url"]) 32 | }) 33 | 34 | remote.CallbackEvent("Network.responseReceived", func(params godet.Params) { 35 | fmt.Println("responseReceived", 36 | params["type"], 37 | params["response"].(map[string]interface{})["url"]) 38 | }) 39 | 40 | remote.CallbackEvent("Log.entryAdded", func(params godet.Params) { 41 | entry := params["entry"].(map[string]interface{}) 42 | fmt.Println("LOG", entry["type"], entry["level"], entry["text"]) 43 | }) 44 | 45 | // enable event processing 46 | remote.RuntimeEvents(true) 47 | remote.NetworkEvents(true) 48 | remote.PageEvents(true) 49 | remote.DOMEvents(true) 50 | remote.LogEvents(true) 51 | 52 | // Navigate to mobile site 53 | remote.Navigate("https://search.google.com/test/mobile-friendly") 54 | 55 | remote.SetVisibleSize(375, 667) // iPhone 7 56 | remote.SetDeviceMetricsOverride(375, 667, 3, true, false) // iPhone 7 57 | 58 | time.Sleep(time.Second * 3) 59 | 60 | // take a screenshot 61 | remote.SaveScreenshot("mobile.png", 0644, 0, true) 62 | } 63 | -------------------------------------------------------------------------------- /examples/parallel.go: -------------------------------------------------------------------------------- 1 | //go:build ignore 2 | // +build ignore 3 | 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | "github.com/raff/godet" 9 | "sync" 10 | "time" 11 | ) 12 | 13 | var ( 14 | urlList = []string{ 15 | "https://github.com", 16 | "https://github.com/gobs/httpclient", 17 | "https://github.com/gobs/simplejson", 18 | "https://github.com/gobs/jsonpath", 19 | "https://github.com/gobs/cmd", 20 | "https://github.com/raff/godet", 21 | "https://github.com/raff/glin", 22 | "https://github.com/raff/goble", 23 | "https://github.com/raff/elseql", 24 | "https://github.com/raff/statemachine", 25 | "https://github.com/raff/zipsaver", 26 | "https://github.com/raff/walkngo", 27 | } 28 | ) 29 | 30 | func processPage(id int, url string) { 31 | var remote *godet.RemoteDebugger 32 | var err error 33 | 34 | // 35 | // the Connect may temporary fail so retry a few times 36 | // 37 | for i := 0; i < 10; i++ { 38 | if i > 0 { 39 | time.Sleep(500 * time.Millisecond) 40 | } 41 | 42 | remote, err = godet.Connect("localhost:9222", false) 43 | if err == nil { 44 | break 45 | } 46 | 47 | fmt.Println(id, "connect", err) 48 | } 49 | 50 | if err != nil { 51 | fmt.Println(id, "cannot connect to browser") 52 | return 53 | } 54 | 55 | fmt.Println(id, "connected") 56 | defer remote.Close() 57 | 58 | done := make(chan bool) 59 | 60 | // 61 | // this should wait until the page request has loaded (if the page has multiple frames there 62 | // may be more "frameStoppedLoading" events and the check should be more complicated) 63 | // 64 | remote.CallbackEvent("Page.frameStoppedLoading", func(params godet.Params) { 65 | fmt.Println(id, "page loaded", params) 66 | done <- true 67 | }) 68 | 69 | tab, err := remote.NewTab(url) 70 | if err != nil { 71 | fmt.Println(id, "cannot create tab:", err) 72 | return 73 | } 74 | 75 | defer func() { 76 | remote.CloseTab(tab) 77 | fmt.Println(id, "done") 78 | }() 79 | 80 | // events needs to be associated to current tab (enable AFTER NewTab) 81 | remote.PageEvents(true) 82 | 83 | _ = <-done 84 | 85 | // here the page should be ready 86 | // add code to process content or take screenshot 87 | 88 | filename := fmt.Sprintf("%d.png", id) 89 | remote.SaveScreenshot(filename, 0644, 0, true) 90 | } 91 | 92 | func main() { 93 | var wg sync.WaitGroup 94 | 95 | for x := 0; x < 10; x++ { 96 | 97 | // now open new list 98 | for p := range urlList { 99 | wg.Add(1) 100 | go func(page int) { 101 | id := x*100 + page 102 | processPage(id, urlList[page]) 103 | wg.Done() 104 | }(p) 105 | } 106 | 107 | wg.Wait() 108 | fmt.Println(x, "------------------------------------------") 109 | } 110 | 111 | fmt.Println("DONE.") 112 | } 113 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | [![Go Documentation](http://godoc.org/github.com/raff/godet?status.svg)](http://godoc.org/github.com/raff/godet) 3 | [![Go Report Card](https://goreportcard.com/badge/github.com/raff/godet)](https://goreportcard.com/report/github.com/raff/godet) 4 | [![Actions Status](https://github.com/raff/godet/workflows/Go/badge.svg)](https://github.com/raff/godet/actions) 5 | 6 | 7 | # godet 8 | Remote client for Chrome DevTools 9 | 10 | ## Installation 11 | 12 | $ go get github.com/raff/godet 13 | 14 | ## Documentation 15 | http://godoc.org/github.com/raff/godet 16 | 17 | ## Example 18 | A pretty complete example is available at [`cmd/godet/main.go`](https://github.com/raff/godet/blob/master/cmd/godet/main.go). 19 | This example is available at [`examples/example.go`](https://github.com/raff/godet/blob/master/examples/example.go). 20 | 21 | ```go 22 | import "github.com/raff/godet" 23 | 24 | // connect to Chrome instance 25 | remote, err := godet.Connect("localhost:9222", true) 26 | if err != nil { 27 | fmt.Println("cannot connect to Chrome instance:", err) 28 | return 29 | } 30 | 31 | // disconnect when done 32 | defer remote.Close() 33 | 34 | // get browser and protocol version 35 | version, _ := remote.Version() 36 | fmt.Println(version) 37 | 38 | // get list of open tabs 39 | tabs, _ := remote.TabList("") 40 | fmt.Println(tabs) 41 | 42 | // install some callbacks 43 | remote.CallbackEvent(godet.EventClosed, func(params godet.Params) { 44 | fmt.Println("RemoteDebugger connection terminated.") 45 | }) 46 | 47 | remote.CallbackEvent("Network.requestWillBeSent", func(params godet.Params) { 48 | fmt.Println("requestWillBeSent", 49 | params["type"], 50 | params["documentURL"], 51 | params["request"].(map[string]interface{})["url"]) 52 | }) 53 | 54 | remote.CallbackEvent("Network.responseReceived", func(params godet.Params) { 55 | fmt.Println("responseReceived", 56 | params["type"], 57 | params["response"].(map[string]interface{})["url"]) 58 | }) 59 | 60 | remote.CallbackEvent("Log.entryAdded", func(params godet.Params) { 61 | entry := params["entry"].(map[string]interface{}) 62 | fmt.Println("LOG", entry["type"], entry["level"], entry["text"]) 63 | }) 64 | 65 | // block loading of most images 66 | _ = remote.SetBlockedURLs("*.jpg", "*.png", "*.gif") 67 | 68 | // create new tab 69 | tab, _ := remote.NewTab("https://www.google.com") 70 | fmt.Println(tab) 71 | 72 | // enable event processing 73 | remote.RuntimeEvents(true) 74 | remote.NetworkEvents(true) 75 | remote.PageEvents(true) 76 | remote.DOMEvents(true) 77 | remote.LogEvents(true) 78 | 79 | // navigate in existing tab 80 | _ = remote.ActivateTab(tabs[0]) 81 | 82 | // re-enable events when changing active tab 83 | remote.AllEvents(true) // enable all events 84 | 85 | _, _ = remote.Navigate("https://www.google.com") 86 | 87 | // evaluate Javascript expression in existing context 88 | res, _ := remote.EvaluateWrap(` 89 | console.log("hello from godet!") 90 | return 42; 91 | `) 92 | fmt.Println(res) 93 | 94 | // take a screenshot 95 | _ = remote.SaveScreenshot("screenshot.png", 0644, 0, true) 96 | 97 | // or save page as PDF 98 | _ = remote.SavePDF("page.pdf", 0644) 99 | 100 | ``` 101 | -------------------------------------------------------------------------------- /examples/example.go: -------------------------------------------------------------------------------- 1 | //go:build ignore 2 | // +build ignore 3 | 4 | package main 5 | 6 | import "fmt" 7 | import "time" 8 | 9 | import "github.com/raff/godet" 10 | 11 | func main() { 12 | // connect to Chrome instance 13 | remote, err := godet.Connect("localhost:9222", false) 14 | if err != nil { 15 | fmt.Println("cannot connect to Chrome instance:", err) 16 | return 17 | } 18 | 19 | // disconnect when done 20 | defer remote.Close() 21 | 22 | // get browser and protocol version 23 | version, _ := remote.Version() 24 | fmt.Println(version) 25 | 26 | // get list of open tabs 27 | tabs, _ := remote.TabList("") 28 | fmt.Println(tabs) 29 | 30 | // install some callbacks 31 | remote.CallbackEvent(godet.EventClosed, func(params godet.Params) { 32 | fmt.Println("RemoteDebugger connection terminated.") 33 | }) 34 | 35 | remote.CallbackEvent("Network.requestWillBeSent", func(params godet.Params) { 36 | fmt.Println("requestWillBeSent", 37 | params["type"], 38 | params["documentURL"], 39 | params["request"].(map[string]interface{})["url"]) 40 | }) 41 | 42 | remote.CallbackEvent("Network.responseReceived", func(params godet.Params) { 43 | fmt.Println("responseReceived", 44 | params["type"], 45 | params["response"].(map[string]interface{})["url"]) 46 | }) 47 | 48 | remote.CallbackEvent("Log.entryAdded", func(params godet.Params) { 49 | entry := params["entry"].(map[string]interface{}) 50 | fmt.Println("LOG", entry["type"], entry["level"], entry["text"]) 51 | }) 52 | 53 | // block loading of most images 54 | _ = remote.SetBlockedURLs("*.jpg", "*.png", "*.gif") 55 | 56 | // create new tab 57 | tab, _ := remote.NewTab("https://www.google.com") 58 | fmt.Println(tab) 59 | 60 | // enable event processing 61 | remote.RuntimeEvents(true) 62 | remote.NetworkEvents(true) 63 | remote.PageEvents(true) 64 | remote.DOMEvents(true) 65 | remote.LogEvents(true) 66 | 67 | // navigate in existing tab 68 | _ = remote.ActivateTab(tabs[0]) 69 | 70 | //remote.StartPreciseCoverage(true, true) 71 | 72 | // re-enable events when changing active tab 73 | remote.AllEvents(true) // enable all events 74 | 75 | _, _ = remote.Navigate("https://www.google.com") 76 | 77 | // evaluate Javascript expression in existing context 78 | res, _ := remote.EvaluateWrap(` 79 | console.log("hello from godet!") 80 | return 42; 81 | `) 82 | fmt.Println(res) 83 | 84 | // take a screenshot 85 | _ = remote.SaveScreenshot("screenshot.png", 0644, 0, true) 86 | 87 | time.Sleep(time.Second) 88 | 89 | // or save page as PDF 90 | _ = remote.SavePDF("page.pdf", 0644, godet.PortraitMode(), godet.Scale(0.5), godet.Dimensions(6.0, 2.0)) 91 | 92 | // if err := remote.SetInputFiles(0, []string{"hello.txt"}); err != nil { 93 | // fmt.Println("setInputFiles", err) 94 | // } 95 | 96 | time.Sleep(5 * time.Second) 97 | 98 | //remote.StopPreciseCoverage() 99 | 100 | r, err := remote.GetPreciseCoverage(true) 101 | if err != nil { 102 | fmt.Println("error profiling", err) 103 | } else { 104 | fmt.Println(r) 105 | } 106 | 107 | // Allow downloads 108 | _ = remote.SetDownloadBehavior(godet.AllowDownload, "/tmp/") 109 | _, _ = remote.Navigate("http://httpbin.org/response-headers?Content-Type=text/plain;%20charset=UTF-8&Content-Disposition=attachment;%20filename%3d%22test.jnlp%22") 110 | 111 | time.Sleep(time.Second) 112 | 113 | // Block downloads 114 | _ = remote.SetDownloadBehavior(godet.DenyDownload, "") 115 | _, _ = remote.Navigate("http://httpbin.org/response-headers?Content-Type=text/plain;%20charset=UTF-8&Content-Disposition=attachment;%20filename%3d%22test.jnlp%22") 116 | } 117 | -------------------------------------------------------------------------------- /cmd/godship/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/gobs/args" 5 | "github.com/gobs/cmd" 6 | "github.com/gobs/cmd/plugins/controlflow" 7 | "github.com/gobs/cmd/plugins/json" 8 | "github.com/gobs/pretty" 9 | "github.com/gobs/simplejson" 10 | "github.com/raff/godet" 11 | 12 | "flag" 13 | "fmt" 14 | "os" 15 | "os/exec" 16 | "runtime" 17 | "strconv" 18 | "strings" 19 | "time" 20 | ) 21 | 22 | type mmap = map[string]interface{} 23 | 24 | func timestamp() string { 25 | return time.Now().Format(time.RFC3339) 26 | } 27 | 28 | func unquote(s string) string { 29 | if res, err := strconv.Unquote(strings.TrimSpace(s)); err == nil { 30 | return res 31 | } 32 | 33 | return s 34 | } 35 | 36 | func printJson(v interface{}) { 37 | fmt.Println(simplejson.MustDumpString(v, simplejson.Indent(" "))) 38 | } 39 | 40 | func parseValue(v string) (interface{}, error) { 41 | switch { 42 | case strings.HasPrefix(v, "{") || strings.HasPrefix(v, "["): 43 | j, err := simplejson.LoadString(v) 44 | if err != nil { 45 | return nil, fmt.Errorf("error parsing %q", v) 46 | } else { 47 | return j.Data(), nil 48 | } 49 | 50 | case strings.HasPrefix(v, `"`): 51 | return strings.Trim(v, `"`), nil 52 | 53 | case strings.HasPrefix(v, `'`): 54 | return strings.Trim(v, `'`), nil 55 | 56 | case v == "": 57 | return v, nil 58 | 59 | case v == "true": 60 | return true, nil 61 | 62 | case v == "false": 63 | return false, nil 64 | 65 | case v == "null": 66 | return nil, nil 67 | 68 | default: 69 | if i, err := strconv.ParseInt(v, 10, 64); err == nil { 70 | return i, nil 71 | } 72 | if f, err := strconv.ParseFloat(v, 64); err == nil { 73 | return f, nil 74 | } 75 | 76 | return v, nil 77 | } 78 | } 79 | 80 | func runCommand(commandString string) error { 81 | parts := args.GetArgs(commandString) 82 | command := exec.Command(parts[0], parts[1:]...) 83 | return command.Start() 84 | } 85 | 86 | func limit(s string, l int) string { 87 | if len(s) > l { 88 | return s[:l] + "..." 89 | } 90 | return s 91 | } 92 | 93 | func documentNode(remote *godet.RemoteDebugger, verbose bool) int { 94 | res, err := remote.GetDocument() 95 | if err != nil { 96 | fmt.Println("error getting document: ", err) 97 | return -1 98 | } 99 | 100 | if verbose { 101 | pretty.PrettyPrint(res) 102 | } 103 | 104 | doc := simplejson.AsJson(res) 105 | return doc.GetPath("root", "nodeId").MustInt(-1) 106 | } 107 | 108 | func chromeApp() (chromeapp string) { 109 | switch runtime.GOOS { 110 | case "darwin": 111 | for _, c := range []string{ 112 | "/Applications/Google Chrome Canary.app", 113 | "/Applications/Google Chrome.app", 114 | } { 115 | // MacOS apps are actually folders 116 | if info, err := os.Stat(c); err == nil && info.IsDir() { 117 | chromeapp = fmt.Sprintf("open %q --args", c) 118 | break 119 | } 120 | } 121 | 122 | case "linux": 123 | for _, c := range []string{ 124 | "headless_shell", 125 | "chromium", 126 | "google-chrome-beta", 127 | "google-chrome-unstable", 128 | "google-chrome-stable"} { 129 | if _, err := exec.LookPath(c); err == nil { 130 | chromeapp = c 131 | break 132 | } 133 | } 134 | 135 | case "windows": 136 | } 137 | 138 | if chromeapp != "" { 139 | if chromeapp == "headless_shell" { 140 | chromeapp += " --no-sandbox" 141 | } else { 142 | chromeapp += " --headless" 143 | } 144 | 145 | chromeapp += " --remote-debugging-port=9222 --hide-scrollbars --wbsi --disable-extensions --disable-gpu about:blank" 146 | } 147 | 148 | return 149 | } 150 | 151 | func main() { 152 | chromeapp := chromeApp() 153 | 154 | cmdApp := flag.String("cmd", chromeapp, "command to execute to start the browser") 155 | headless := flag.Bool("headless", true, "headless mode") 156 | port := flag.String("port", "localhost:9222", "Chrome remote debugger port") 157 | verbose := flag.Bool("verbose", false, "verbose logging") 158 | 159 | /* 160 | seltab := flag.Int("tab", 0, "select specified tab if available") 161 | newtab := flag.Bool("new", false, "always open a new tab") 162 | history := flag.Bool("history", false, "display page history") 163 | filter := flag.String("filter", "page", "filter tab list") 164 | domains := flag.Bool("domains", false, "show list of available domains") 165 | requests := flag.Bool("requests", false, "show request notifications") 166 | responses := flag.Bool("responses", false, "show response notifications") 167 | allEvents := flag.Bool("all-events", false, "enable all events") 168 | logev := flag.Bool("log", false, "show log/console messages") 169 | query := flag.String("query", "", "query against current document") 170 | eval := flag.String("eval", "", "evaluate expression") 171 | screenshot := flag.Bool("screenshot", false, "take a screenshot") 172 | pdf := flag.Bool("pdf", false, "save current page as PDF") 173 | control := flag.String("control", "", "control navigation (proceed,cancel,cancelIgnore)") 174 | block := flag.String("block", "", "block specified URLs or pattenrs. Use '|' as separator") 175 | intercept := flag.String("intercept", "", "enable request interception and respond according to request type - use type:response,type:response,...\n\t type:[Document,Stylesheet,Image,Media,Font,Script,TextTrack,XHR,Fetch,EventSource,WebSocket,Manifest,Other]\n\t response:[Failed,Aborted,TimedOut,AccessDenied,ConnectionClosed,ConnectionReset,ConnectionRefused,ConnectionAborted,ConnectionFailed,NameNotResolved,InternetDisconnected,AddressUnreachable]") 176 | html := flag.Bool("html", false, "get outer HTML for current page") 177 | setHTML := flag.String("set-html", "", "set outer HTML for current page") 178 | wait := flag.Bool("wait", false, "wait for more events") 179 | box := flag.Bool("box", false, "get box model for document") 180 | styles := flag.Bool("styles", false, "get computed style for document") 181 | pause := flag.Duration("pause", 5*time.Second, "wait this amount of time before proceeding") 182 | */ 183 | 184 | flag.Parse() 185 | 186 | if *cmdApp != "" { 187 | if !*headless { 188 | *cmdApp = strings.Replace(*cmdApp, " --headless ", " ", -1) 189 | } 190 | 191 | if err := runCommand(*cmdApp); err != nil { 192 | fmt.Println("cannot start browser", err) 193 | } 194 | } 195 | 196 | var remote *godet.RemoteDebugger 197 | var err error 198 | 199 | for i := 0; i < 10; i++ { 200 | if i > 0 { 201 | time.Sleep(500 * time.Millisecond) 202 | } 203 | 204 | remote, err = godet.Connect(*port, *verbose) 205 | if err == nil { 206 | break 207 | } 208 | 209 | fmt.Println("connect", err) 210 | } 211 | 212 | if err != nil { 213 | fmt.Println("cannot connect to browser") 214 | return 215 | } 216 | 217 | defer remote.Close() 218 | 219 | v, err := remote.Version() 220 | if err != nil { 221 | fmt.Println("cannot get version: ", err) 222 | return 223 | } 224 | 225 | fmt.Println("connected to", v.Browser, "protocol version", v.ProtocolVersion) 226 | 227 | remote.CallbackEvent("Network.requestWillBeSent", func(params godet.Params) { 228 | req := params.Map("request") 229 | 230 | fmt.Println(timestamp(), "requestWillBeSent", 231 | params["type"], 232 | params["documentURL"], 233 | "\n\t", req["method"], req["url"]) 234 | 235 | for k, v := range req["headers"].(mmap) { 236 | fmt.Printf("\t%v: %v", k, v) 237 | } 238 | }) 239 | 240 | remote.CallbackEvent("Network.responseReceived", func(params godet.Params) { 241 | resp := params.Map("response") 242 | url := resp["url"].(string) 243 | 244 | fmt.Println(timestamp(), "responseReceived", 245 | params["type"], 246 | limit(url, 80), 247 | "\n\t\t\t", 248 | int(resp["status"].(float64)), 249 | resp["mimeType"].(string)) 250 | }) 251 | 252 | var interrupted bool 253 | 254 | commander := &cmd.Cmd{ 255 | HistoryFile: ".godship_history", 256 | EnableShell: true, 257 | Interrupt: func(sig os.Signal) bool { interrupted = true; return false }, 258 | } 259 | 260 | commander.Init(controlflow.Plugin, json.Plugin) 261 | commander.SetVar("print", true) 262 | 263 | setResult := func(v interface{}) { 264 | commander.SetVar("result", json.StringJson(v, true)) 265 | json.PrintJson(v) 266 | } 267 | 268 | commander.Add(cmd.Command{ 269 | "version", 270 | `version`, 271 | func(line string) (stop bool) { 272 | v, err := remote.Version() 273 | if err != nil { 274 | fmt.Println("cannot get version: ", err) 275 | } else { 276 | setResult(v) 277 | } 278 | 279 | return 280 | }, 281 | nil}) 282 | 283 | commander.Add(cmd.Command{ 284 | "protocol", 285 | `protocol`, 286 | func(line string) (stop bool) { 287 | p, err := remote.Protocol() 288 | if err != nil { 289 | fmt.Println("cannot get protocol: ", err) 290 | } else { 291 | setResult(p) 292 | } 293 | 294 | return 295 | }, 296 | nil}) 297 | 298 | commander.Add(cmd.Command{ 299 | "tabs", 300 | `tabs [filter]`, 301 | func(line string) (stop bool) { 302 | tabs, err := remote.TabList(line) 303 | if err != nil { 304 | fmt.Println("cannot get list of tabs: ", err) 305 | } else { 306 | setResult(tabs) 307 | } 308 | 309 | return 310 | }, 311 | nil}) 312 | 313 | commander.Add(cmd.Command{ 314 | "verbose", 315 | `verbose [true|false]`, 316 | func(line string) (stop bool) { 317 | if line != "" { 318 | enable, _ := strconv.ParseBool(line) 319 | remote.Verbose(enable) 320 | } 321 | 322 | return 323 | }, 324 | nil}) 325 | 326 | commander.Add(cmd.Command{ 327 | "events", 328 | `events [true|false]`, 329 | func(line string) (stop bool) { 330 | if line != "" { 331 | enable, _ := strconv.ParseBool(line) 332 | remote.AllEvents(enable) 333 | } 334 | 335 | return 336 | }, 337 | nil}) 338 | 339 | commander.Add(cmd.Command{ 340 | "navigate", 341 | `navigate url`, 342 | func(line string) (stop bool) { 343 | if line == "" { 344 | return 345 | } 346 | 347 | ret, err := remote.Navigate(line) 348 | if err != nil { 349 | setResult(err) 350 | } else { 351 | setResult(ret) 352 | } 353 | 354 | return 355 | }, 356 | nil}) 357 | 358 | commander.Add(cmd.Command{ 359 | "query", 360 | `query selector`, 361 | func(line string) (stop bool) { 362 | id := documentNode(remote, *verbose) 363 | 364 | res, err := remote.QuerySelector(id, line) 365 | if err != nil { 366 | setResult(err) 367 | return 368 | } 369 | 370 | if res == nil { 371 | setResult(nil) 372 | return 373 | } 374 | 375 | id = int(res["nodeId"].(float64)) 376 | res, err = remote.ResolveNode(id) 377 | if err != nil { 378 | setResult(err) 379 | return 380 | } 381 | 382 | setResult(res) 383 | return 384 | }, 385 | nil}) 386 | 387 | commander.Add(cmd.Command{ 388 | "eval", 389 | `eval javascript`, 390 | func(line string) (stop bool) { 391 | res, err := remote.Evaluate(line) 392 | if err != nil { 393 | setResult(err) 394 | return 395 | } 396 | 397 | setResult(res) 398 | return 399 | }, 400 | nil}) 401 | 402 | commander.Add(cmd.Command{ 403 | "html", 404 | "html [html body]", 405 | func(line string) (stop bool) { 406 | id := documentNode(remote, *verbose) 407 | 408 | if line == "" { 409 | res, err := remote.GetOuterHTML(id) 410 | if err != nil { 411 | setResult(err) 412 | return 413 | } 414 | 415 | setResult(res) 416 | } else { 417 | err = remote.SetOuterHTML(id, line) 418 | if err != nil { 419 | setResult(err) 420 | return 421 | } 422 | } 423 | 424 | return 425 | }, 426 | nil}) 427 | 428 | commander.Commands["set"] = commander.Commands["var"] 429 | commander.Commands["go"] = commander.Commands["navigate"] 430 | 431 | switch flag.NArg() { 432 | case 0: // program name only 433 | break 434 | 435 | case 1: // one arg - expect URL or @filename 436 | cmd := flag.Arg(0) 437 | if !strings.HasPrefix(cmd, "@") { 438 | cmd = "base " + cmd 439 | } 440 | 441 | commander.OneCmd(cmd) 442 | 443 | default: 444 | fmt.Println("usage:", os.Args[0], "[base-url]") 445 | return 446 | } 447 | 448 | commander.CmdLoop() 449 | } 450 | -------------------------------------------------------------------------------- /cmd/godet/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "os" 8 | "os/exec" 9 | "regexp" 10 | "runtime" 11 | "strings" 12 | "time" 13 | 14 | "github.com/gobs/args" 15 | "github.com/gobs/pretty" 16 | "github.com/gobs/simplejson" 17 | "github.com/raff/godet" 18 | ) 19 | 20 | func runCommand(commandString string) error { 21 | parts := args.GetArgs(commandString) 22 | exe := strings.Replace(parts[0], "\u00A0", " ", -1) 23 | 24 | cmd := exec.Command(exe, parts[1:]...) 25 | return cmd.Start() 26 | } 27 | 28 | func limit(s string, l int) string { 29 | if len(s) > l { 30 | return s[:l] + "..." 31 | } 32 | return s 33 | } 34 | 35 | func documentNode(remote *godet.RemoteDebugger, verbose bool) int { 36 | res, err := remote.GetDocument() 37 | if err != nil { 38 | log.Fatal("error getting document: ", err) 39 | } 40 | 41 | if verbose { 42 | pretty.PrettyPrint(res) 43 | } 44 | 45 | doc := simplejson.AsJson(res) 46 | return doc.GetPath("root", "nodeId").MustInt(-1) 47 | } 48 | 49 | func main() { 50 | chromeapp := os.Getenv("GODET_CHROMEAPP") 51 | 52 | if chromeapp == "" { 53 | switch runtime.GOOS { 54 | case "darwin": 55 | for _, c := range []string{ 56 | "/Applications/Google Chrome Canary.app", 57 | "/Applications/Google Chrome.app", 58 | } { 59 | // MacOS apps are actually folders 60 | if info, err := os.Stat(c); err == nil && info.IsDir() { 61 | chromeapp = fmt.Sprintf("open %q --args", c) 62 | break 63 | } 64 | } 65 | 66 | case "linux": 67 | for _, c := range []string{ 68 | "headless_shell", 69 | "chromium", 70 | "google-chrome-beta", 71 | "google-chrome-unstable", 72 | "google-chrome-stable"} { 73 | if _, err := exec.LookPath(c); err == nil { 74 | chromeapp = c 75 | break 76 | } 77 | } 78 | 79 | case "windows": 80 | for _, c := range []string{ 81 | "C:/Program Files (x86)/Google/Chrome/Application/chrome.exe", 82 | "C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe", 83 | } { 84 | if _, err := exec.LookPath(c); err == nil { 85 | if strings.Contains(c, " ") { 86 | chromeapp = `"` + strings.Replace(c, " ", "\u00A0", -1) + `"` 87 | } else { 88 | chromeapp = c 89 | } 90 | break 91 | } 92 | } 93 | } 94 | } 95 | 96 | if chromeapp != "" { 97 | if chromeapp == "headless_shell" { 98 | chromeapp += " --no-sandbox" 99 | } else { 100 | chromeapp += " --headless" 101 | } 102 | 103 | chromeapp += " --remote-debugging-port=9222 --hide-scrollbars --bwsi --disable-extensions --disable-gpu about:blank" 104 | } 105 | 106 | cmd := flag.String("cmd", chromeapp, "command to execute to start the browser") 107 | headless := flag.String("headless", "", "headless mode (true/false, old or new)") 108 | port := flag.String("port", "localhost:9222", "Chrome remote debugger port") 109 | verbose := flag.Bool("verbose", false, "verbose logging") 110 | version := flag.Bool("version", false, "display remote devtools version") 111 | protocol := flag.Bool("protocol", false, "display the DevTools protocol") 112 | listtabs := flag.Bool("tabs", false, "show list of open tabs") 113 | seltab := flag.Int("tab", -1, "select specified tab if available") 114 | newtab := flag.Bool("new", false, "always open a new tab") 115 | listtargets := flag.Bool("targets", false, "show list of targets") 116 | history := flag.Bool("history", false, "display page history") 117 | filter := flag.String("filter", "page", "filter tab list") 118 | domains := flag.Bool("domains", false, "show list of available domains") 119 | requests := flag.Bool("requests", false, "show request notifications") 120 | responses := flag.Bool("responses", false, "show response notifications") 121 | fetch := flag.Bool("fetch", false, "enable processing of requestPaused events (in the Fetch domain)") 122 | allEvents := flag.Bool("all-events", false, "enable all events") 123 | logev := flag.Bool("log", false, "show log/console messages") 124 | query := flag.String("query", "", "query against current document") 125 | eval := flag.String("eval", "", "evaluate expression") 126 | screenshot := flag.Bool("screenshot", false, "take a screenshot") 127 | pdf := flag.Bool("pdf", false, "save current page as PDF") 128 | control := flag.String("control", "", "control navigation (proceed,cancel,cancelIgnore)") 129 | block := flag.String("block", "", "block specified URLs or pattenrs. Use '|' as separator") 130 | intercept := flag.String("intercept", "", "enable request interception and respond according to request type - use type:response,type:response,...\n\t type:[Document,Stylesheet,Image,Media,Font,Script,TextTrack,XHR,Fetch,EventSource,WebSocket,Manifest,Other]\n\t response:[Failed,Aborted,TimedOut,AccessDenied,ConnectionClosed,ConnectionReset,ConnectionRefused,ConnectionAborted,ConnectionFailed,NameNotResolved,InternetDisconnected,AddressUnreachable]") 131 | html := flag.Bool("html", false, "get outer HTML for current page") 132 | setHTML := flag.String("set-html", "", "set outer HTML for current page") 133 | wait := flag.Bool("wait", false, "wait for more events") 134 | box := flag.Bool("box", false, "get box model for document") 135 | styles := flag.Bool("styles", false, "get computed style for document") 136 | pause := flag.Duration("pause", 5*time.Second, "wait this amount of time before proceeding") 137 | close := flag.Bool("close", false, "gracefully close browser") 138 | getCookies := flag.Bool("cookies", false, "get cookies for current page") 139 | getAllCookies := flag.Bool("all-cookies", false, "get all cookies for current page") 140 | body := flag.Bool("body", false, "show response body") 141 | bypass := flag.Bool("bypass", false, "bypass service workers") 142 | download := flag.String("download", "", "download behavour (default,allow,deny)") 143 | flag.Parse() 144 | 145 | if *cmd != "" { 146 | if *headless != "" { 147 | hparam := fmt.Sprintf(" --headless=%v ", *headless) 148 | if *headless == "false" { 149 | hparam = " " 150 | } 151 | 152 | *cmd = strings.Replace(*cmd, " --headless ", hparam, -1) 153 | } 154 | 155 | if err := runCommand(*cmd); err != nil { 156 | log.Println("cannot start browser", err) 157 | } 158 | } 159 | 160 | var remote *godet.RemoteDebugger 161 | var err error 162 | 163 | for i := 0; i < 20; i++ { 164 | if i > 0 { 165 | time.Sleep(time.Second) 166 | } 167 | 168 | remote, err = godet.Connect(*port, *verbose) 169 | if err == nil { 170 | break 171 | } 172 | 173 | log.Println("connect", err) 174 | } 175 | 176 | if err != nil { 177 | log.Fatal("cannot connect to browser") 178 | } 179 | 180 | defer remote.Close() 181 | 182 | done := make(chan bool) 183 | shouldWait := *wait 184 | 185 | var pwait chan bool 186 | 187 | v, err := remote.Version() 188 | if err != nil { 189 | log.Fatal("cannot get version: ", err) 190 | } 191 | 192 | if *version { 193 | pretty.PrettyPrint(v) 194 | } else { 195 | log.Println("connected to", v.Browser, "protocol version", v.ProtocolVersion) 196 | } 197 | 198 | if *protocol { 199 | p, err := remote.Protocol() 200 | if err != nil { 201 | log.Fatal("cannot get protocol: ", err) 202 | } 203 | 204 | pretty.PrettyPrint(p) 205 | shouldWait = false 206 | } 207 | 208 | if *listtabs { 209 | tabs, err := remote.TabList(*filter) 210 | if err != nil { 211 | log.Fatal("cannot get list of tabs: ", err) 212 | } 213 | 214 | pretty.PrettyPrint(tabs) 215 | shouldWait = false 216 | } 217 | 218 | if *listtargets { 219 | targets, err := remote.GetTargets() 220 | if err != nil { 221 | log.Fatal("cannot get list of targets: ", err) 222 | } 223 | 224 | pretty.PrettyPrint(targets) 225 | shouldWait = false 226 | } 227 | 228 | if *domains { 229 | d, err := remote.GetDomains() 230 | if err != nil { 231 | log.Fatal("cannot get domains: ", err) 232 | } 233 | 234 | pretty.PrettyPrint(d) 235 | shouldWait = false 236 | } 237 | 238 | if *history { 239 | curr, entries, err := remote.GetNavigationHistory() 240 | if err != nil { 241 | log.Fatal("cannot get history: ", err) 242 | } 243 | 244 | fmt.Println("current entry:", curr) 245 | pretty.PrettyPrint(entries) 246 | shouldWait = false 247 | } 248 | 249 | remote.CallbackEvent(godet.EventClosed, func(params godet.Params) { 250 | log.Println("RemoteDebugger connection terminated.") 251 | done <- true 252 | }) 253 | 254 | remote.CallbackEvent("Emulation.virtualTimeBudgetExpired", func(params godet.Params) { 255 | pwait <- true 256 | }) 257 | 258 | if *requests { 259 | remote.CallbackEvent("Network.requestWillBeSent", func(params godet.Params) { 260 | log.Println("requestWillBeSent", 261 | params["type"], 262 | params["documentURL"], 263 | params.Map("request")["url"]) 264 | }) 265 | } 266 | 267 | if *responses { 268 | remote.CallbackEvent("Network.responseReceived", func(params godet.Params) { 269 | resp := params.Map("response") 270 | url := resp["url"].(string) 271 | 272 | log.Println("responseReceived", 273 | params["type"], 274 | limit(url, 80), 275 | "\n\t\t\t", 276 | int(resp["status"].(float64)), 277 | resp["mimeType"].(string)) 278 | 279 | if *body { 280 | go func() { 281 | req := params.String("requestId") 282 | res, err := remote.GetResponseBody(req) 283 | if err != nil { 284 | log.Println("Error getting responseBody", err) 285 | } else { 286 | log.Printf("body (%v)\n", len(res)) 287 | log.Println(string(res)) 288 | } 289 | }() 290 | } 291 | }) 292 | } 293 | 294 | if *logev { 295 | remote.CallbackEvent("Log.entryAdded", func(params godet.Params) { 296 | entry := params.Map("entry") 297 | log.Println("LOG", entry["type"], entry["level"], entry["text"]) 298 | }) 299 | 300 | remote.CallbackEvent("Runtime.consoleAPICalled", func(params godet.Params) { 301 | l := []interface{}{"CONSOLE", params["type"].(string)} 302 | 303 | for _, a := range params["args"].([]interface{}) { 304 | arg := a.(map[string]interface{}) 305 | 306 | if arg["value"] != nil { 307 | l = append(l, arg["value"]) 308 | } else if arg["preview"] != nil { 309 | arg := arg["preview"].(map[string]interface{}) 310 | 311 | v := arg["description"].(string) + "{" 312 | 313 | for i, p := range arg["properties"].([]interface{}) { 314 | if i > 0 { 315 | v += ", " 316 | } 317 | 318 | prop := p.(map[string]interface{}) 319 | if prop["name"] != nil { 320 | v += fmt.Sprintf("%q: ", prop["name"]) 321 | } 322 | 323 | v += fmt.Sprintf("%v", prop["value"]) 324 | } 325 | 326 | v += "}" 327 | l = append(l, v) 328 | } else { 329 | l = append(l, arg["type"].(string)) 330 | } 331 | 332 | } 333 | 334 | log.Println(l...) 335 | }) 336 | } 337 | 338 | if *block != "" { 339 | blocks := strings.Split(*block, "|") 340 | remote.SetBlockedURLs(blocks...) 341 | } 342 | 343 | if *bypass { 344 | remote.SetBypassServiceWorker(true) 345 | } 346 | 347 | var site string 348 | 349 | tabs, err := remote.TabList("page") 350 | if err != nil { 351 | log.Fatal("cannot get tabs: ", err) 352 | } 353 | if *seltab >= 0 && *seltab < len(tabs) { 354 | if err = remote.ActivateTab(tabs[*seltab]); err != nil { 355 | log.Println("cannot select tab", *seltab) 356 | } 357 | } 358 | 359 | if flag.NArg() > 0 { 360 | site = flag.Arg(0) 361 | 362 | if len(tabs) == 0 || *newtab { 363 | _, err = remote.NewTab(site) 364 | site = "" 365 | 366 | if err != nil { 367 | log.Fatal("error loading page: ", err) 368 | } 369 | } 370 | } 371 | 372 | // 373 | // enable events AFTER creating/selecting a tab but BEFORE navigating to a page 374 | // 375 | if *allEvents { 376 | remote.AllEvents(true) 377 | } else { 378 | remote.RuntimeEvents(true) 379 | remote.NetworkEvents(true) 380 | remote.PageEvents(true) 381 | remote.DOMEvents(true) 382 | remote.LogEvents(true) 383 | remote.EmulationEvents(true) 384 | remote.ServiceWorkerEvents(true) 385 | //remote.TargetEvents(true) 386 | } 387 | 388 | if *download != "" { 389 | var path string 390 | 391 | parts := strings.SplitN(*download, ",", 2) 392 | behavior := godet.DownloadBehavior(parts[0]) 393 | if len(parts) > 1 { 394 | path = parts[1] 395 | } 396 | remote.SetDownloadBehavior(behavior, path) 397 | } 398 | 399 | if *fetch { 400 | remote.EnableRequestPaused(true) 401 | 402 | remote.CallbackEvent("Fetch.requestPaused", func(params godet.Params) { 403 | rid := params.String("requestId") 404 | nid := params.String("networkId") 405 | rtype := params.String("resourceType") 406 | 407 | log.Println("request paused for", rid, nid, rtype, params.Map("request")["url"]) 408 | if v, ok := params["responseErrorReason"]; ok { 409 | log.Println(" error reason:", v) 410 | } 411 | if v, ok := params["responseStatusCode"]; ok { 412 | log.Println(" status code:", v) 413 | } 414 | 415 | remote.ContinueRequest(rid, "", "", "", nil) 416 | }) 417 | } 418 | 419 | if *control != "" { 420 | remote.SetControlNavigations(true) 421 | navigationResponse := godet.NavigationProceed 422 | 423 | switch *control { 424 | case "proceed": 425 | navigationResponse = godet.NavigationProceed 426 | case "cancel": 427 | navigationResponse = godet.NavigationCancel 428 | case "cancelIgnore": 429 | navigationResponse = godet.NavigationCancelAndIgnore 430 | } 431 | 432 | remote.CallbackEvent("Page.navigationRequested", func(params godet.Params) { 433 | log.Println("navigation requested for", params.String("url"), navigationResponse) 434 | 435 | remote.ProcessNavigation(params.Int("navigationId"), navigationResponse) 436 | }) 437 | } 438 | 439 | if *intercept != "" { 440 | remote.EnableRequestInterception(true) 441 | responses := map[string]string{} 442 | 443 | if strings.Contains(*intercept, ":") { // type:response 444 | matches := regexp.MustCompile(`(\w+):(\w+),?`).FindAllStringSubmatch(*intercept, -1) 445 | 446 | for _, m := range matches { 447 | responses[m[1]] = m[2] 448 | } 449 | } // else, we just log the intercept requests 450 | 451 | remote.CallbackEvent("Network.requestIntercepted", func(params godet.Params) { 452 | iid := params.String("interceptionId") 453 | rtype := params.String("resourceType") 454 | reason := responses[rtype] 455 | 456 | log.Println("request intercepted for", iid, rtype, params.Map("request")["url"]) 457 | if reason != "" { 458 | log.Println(" abort with reason", reason) 459 | } 460 | if params.Bool("isNavigationRequest") { 461 | log.Println(" navigationRequest") 462 | } 463 | if params.Bool("isDownload") { 464 | log.Println(" download") 465 | } 466 | 467 | remote.ContinueInterceptedRequest(iid, godet.ErrorReason(reason), "", "", "", "", nil) 468 | }) 469 | } 470 | 471 | if *pause > 0 && shouldWait { 472 | pwait = make(chan bool) 473 | 474 | remote.SetVirtualTimePolicy(godet.VirtualTimePolicyPauseIfNetworkFetchesPending, 475 | int(*pause/time.Millisecond)) 476 | } 477 | 478 | if len(site) > 0 { 479 | _, err = remote.Navigate(site) 480 | if err != nil { 481 | log.Fatal("error loading page: ", err) 482 | } 483 | } 484 | 485 | if pwait != nil { 486 | fmt.Println("Pause", *pause) 487 | <-pwait 488 | } 489 | 490 | if *query != "" { 491 | id := documentNode(remote, *verbose) 492 | 493 | res, err := remote.QuerySelector(id, *query) 494 | if err != nil { 495 | log.Fatal("error in querySelector: ", err) 496 | } 497 | 498 | if res == nil { 499 | log.Println("no result for", *query) 500 | } else { 501 | id = int(res["nodeId"].(float64)) 502 | res, err = remote.ResolveNode(id) 503 | if err != nil { 504 | log.Fatal("error in resolveNode: ", err) 505 | } 506 | 507 | pretty.PrettyPrint(res) 508 | } 509 | 510 | shouldWait = false 511 | } 512 | 513 | if *eval != "" { 514 | res, err := remote.EvaluateWrap(*eval) 515 | if err != nil { 516 | log.Fatal("error in evaluate: ", err) 517 | } 518 | 519 | pretty.PrettyPrint(res) 520 | shouldWait = false 521 | } 522 | 523 | if *setHTML != "" { 524 | id := documentNode(remote, *verbose) 525 | 526 | res, err := remote.QuerySelector(id, "html") 527 | if err != nil { 528 | log.Fatal("error in querySelector: ", err) 529 | } 530 | 531 | id = int(res["nodeId"].(float64)) 532 | 533 | err = remote.SetOuterHTML(id, *setHTML) 534 | if err != nil { 535 | log.Fatal("error in setOuterHTML: ", err) 536 | } 537 | 538 | shouldWait = false 539 | } 540 | 541 | if *html { 542 | id := documentNode(remote, *verbose) 543 | 544 | res, err := remote.GetOuterHTML(id) 545 | if err != nil { 546 | log.Fatal("error in getOuterHTML: ", err) 547 | } 548 | 549 | log.Println(res) 550 | shouldWait = false 551 | } 552 | 553 | if *box { 554 | id := documentNode(remote, *verbose) 555 | 556 | res, err := remote.QuerySelector(id, "html") 557 | if err != nil { 558 | log.Fatal("error in querySelector: ", err) 559 | } 560 | 561 | id = int(res["nodeId"].(float64)) 562 | 563 | res, err = remote.GetBoxModel(id) 564 | if err != nil { 565 | log.Fatal("error in getBoxModel: ", err) 566 | } 567 | 568 | pretty.PrettyPrint(res) 569 | shouldWait = false 570 | } 571 | 572 | if *styles { 573 | id := documentNode(remote, *verbose) 574 | 575 | res, err := remote.QuerySelector(id, "html") 576 | if err != nil { 577 | log.Fatal("error in querySelector: ", err) 578 | } 579 | 580 | id = int(res["nodeId"].(float64)) 581 | 582 | res, err = remote.GetComputedStyleForNode(id) 583 | if err != nil { 584 | log.Fatal("error in getComputedStyleForNode: ", err) 585 | } 586 | 587 | pretty.PrettyPrint(res) 588 | shouldWait = false 589 | } 590 | 591 | if *screenshot { 592 | id := documentNode(remote, *verbose) 593 | 594 | res, err := remote.QuerySelector(id, "html") 595 | if err != nil { 596 | log.Fatal("error in querySelector: ", err) 597 | } 598 | 599 | id = int(res["nodeId"].(float64)) 600 | 601 | res, err = remote.GetBoxModel(id) 602 | if err != nil { 603 | log.Fatal("error in getBoxModel: ", err) 604 | } 605 | 606 | if res == nil { 607 | log.Println("BoxModel not available") 608 | } else { 609 | res = res["model"].(map[string]interface{}) 610 | width := int(res["width"].(float64)) 611 | height := int(res["height"].(float64)) 612 | 613 | err = remote.SetVisibleSize(width, height) 614 | if err != nil { 615 | log.Fatal("error in setVisibleSize: ", err) 616 | } 617 | } 618 | 619 | remote.SaveScreenshot("screenshot.png", 0644, 0, true) 620 | shouldWait = false 621 | } 622 | 623 | if *getCookies { 624 | cookies, err := remote.GetCookies(nil) 625 | if err != nil { 626 | log.Println("error getting cookies:", err) 627 | } else { 628 | pretty.PrettyPrint(cookies) 629 | } 630 | shouldWait = false 631 | } 632 | 633 | if *getAllCookies { 634 | cookies, err := remote.GetAllCookies() 635 | if err != nil { 636 | log.Println("error getting cookies:", err) 637 | } else { 638 | pretty.PrettyPrint(cookies) 639 | } 640 | shouldWait = false 641 | } 642 | 643 | if *pdf { 644 | remote.SavePDF("page.pdf", 0644) 645 | shouldWait = false 646 | } 647 | 648 | if *close { 649 | remote.CloseBrowser() 650 | } 651 | 652 | if *wait || shouldWait { 653 | log.Println("Wait for events...") 654 | <-done 655 | } 656 | 657 | log.Println("Closing") 658 | } 659 | -------------------------------------------------------------------------------- /godet.go: -------------------------------------------------------------------------------- 1 | // Package godet implements a client to interact with an instance of Chrome via the Remote Debugging Protocol. 2 | // 3 | // See https://developer.chrome.com/devtools/docs/debugger-protocol 4 | package godet 5 | 6 | import ( 7 | "encoding/base64" 8 | "encoding/json" 9 | "errors" 10 | "fmt" 11 | "io/ioutil" 12 | "log" 13 | "net" 14 | "os" 15 | "path/filepath" 16 | "sync" 17 | "time" 18 | 19 | "github.com/gobs/httpclient" 20 | "github.com/gorilla/websocket" 21 | ) 22 | 23 | const ( 24 | // EventClosed represents the "RemoteDebugger.closed" event. 25 | // It is emitted when RemoteDebugger.Close() is called. 26 | EventClosed = "RemoteDebugger.closed" 27 | // EventClosed represents the "RemoteDebugger.disconnected" event. 28 | // It is emitted when we lose connection with the debugger and we stop reading events 29 | EventDisconnect = "RemoteDebugger.disconnected" 30 | 31 | // NavigationProceed allows the navigation 32 | NavigationProceed = NavigationResponse("Proceed") 33 | // NavigationCancel cancels the navigation 34 | NavigationCancel = NavigationResponse("Cancel") 35 | // NavigationCancelAndIgnore cancels the navigation and makes the requester of the navigation acts like the request was never made. 36 | NavigationCancelAndIgnore = NavigationResponse("CancelAndIgnore") 37 | 38 | ErrorReasonFailed = ErrorReason("Failed") 39 | ErrorReasonAborted = ErrorReason("Aborted") 40 | ErrorReasonTimedOut = ErrorReason("TimedOut") 41 | ErrorReasonAccessDenied = ErrorReason("AccessDenied") 42 | ErrorReasonConnectionClosed = ErrorReason("ConnectionClosed") 43 | ErrorReasonConnectionReset = ErrorReason("ConnectionReset") 44 | ErrorReasonConnectionRefused = ErrorReason("ConnectionRefused") 45 | ErrorReasonConnectionAborted = ErrorReason("ConnectionAborted") 46 | ErrorReasonConnectionFailed = ErrorReason("ConnectionFailed") 47 | ErrorReasonNameNotResolved = ErrorReason("NameNotResolved") 48 | ErrorReasonInternetDisconnected = ErrorReason("InternetDisconnected") 49 | ErrorReasonAddressUnreachable = ErrorReason("AddressUnreachable") 50 | ErrorReasonBlockedByClient = ErrorReason("BlockedByClient") 51 | ErrorReasonBlockedByResponse = ErrorReason("BlockedByResponse") 52 | 53 | // VirtualTimePolicyAdvance specifies that if the scheduler runs out of immediate work, the virtual time base may fast forward to allow the next delayed task (if any) to run 54 | VirtualTimePolicyAdvance = VirtualTimePolicy("advance") 55 | // VirtualTimePolicyPause specifies that the virtual time base may not advance 56 | VirtualTimePolicyPause = VirtualTimePolicy("pause") 57 | // VirtualTimePolicyPauseIfNetworkFetchesPending specifies that the virtual time base may not advance if there are any pending resource fetches. 58 | VirtualTimePolicyPauseIfNetworkFetchesPending = VirtualTimePolicy("pauseIfNetworkFetchesPending") 59 | 60 | AllowDownload = DownloadBehavior("allow") 61 | NameDownload = DownloadBehavior("allowAndName") 62 | DenyDownload = DownloadBehavior("deny") 63 | DefaultDownload = DownloadBehavior("default") 64 | ) 65 | 66 | type IdType int 67 | 68 | const ( 69 | NodeId IdType = iota 70 | BackendNodeId 71 | ObjectId 72 | ) 73 | 74 | var ( 75 | // ErrorNoActiveTab is returned if there are no active tabs (of type "page") 76 | ErrorNoActiveTab = errors.New("no active tab") 77 | // ErrorNoWsURL is returned if the active tab has no websocket URL 78 | ErrorNoWsURL = errors.New("no websocket URL") 79 | // ErrorNoResponse is returned if a method was expecting a response but got nil instead 80 | ErrorNoResponse = errors.New("no response") 81 | // ErrorClose is returned if a method is called after the connection has been close 82 | ErrorClose = errors.New("closed") 83 | 84 | MaxReadBufferSize = 0 // default gorilla/websocket buffer size 85 | MaxWriteBufferSize = 100 * 1024 // this should be large enough to send large scripts 86 | ) 87 | 88 | // NavigationResponse defines the type for ProcessNavigation `response` 89 | type NavigationResponse string 90 | 91 | // ErrorReason defines what error should be generated to abort a request in ContinueInterceptedRequest 92 | type ErrorReason string 93 | 94 | // VirtualTimePolicy defines the type for Emulation.SetVirtualTimePolicy 95 | type VirtualTimePolicy string 96 | 97 | // DownloadBehaviour defines the type for Page.SetDownloadBehavior 98 | type DownloadBehavior string 99 | 100 | func decode(resp *httpclient.HttpResponse, v interface{}) error { 101 | err := json.NewDecoder(resp.Body).Decode(v) 102 | resp.Close() 103 | 104 | return err 105 | } 106 | 107 | func unmarshal(payload []byte) (map[string]interface{}, error) { 108 | var response map[string]interface{} 109 | err := json.Unmarshal(payload, &response) 110 | if err != nil { 111 | log.Println("unmarshal", string(payload), len(payload), err) 112 | } 113 | return response, err 114 | } 115 | 116 | func responseError(resp *httpclient.HttpResponse, err error) (*httpclient.HttpResponse, error) { 117 | if err == nil { 118 | return resp, resp.ResponseError() 119 | } 120 | 121 | return resp, err 122 | } 123 | 124 | // Version holds the DevTools version information. 125 | type Version struct { 126 | Browser string `json:"Browser"` 127 | ProtocolVersion string `json:"Protocol-Version"` 128 | UserAgent string `json:"User-Agent"` 129 | V8Version string `json:"V8-Version"` 130 | WebKitVersion string `json:"WebKit-Version"` 131 | } 132 | 133 | // Domain holds a domain name and version. 134 | type Domain struct { 135 | Name string `json:"name"` 136 | Version string `json:"version"` 137 | } 138 | 139 | // Tab represents an opened tab/page. 140 | type Tab struct { 141 | ID string `json:"id"` 142 | Type string `json:"type"` 143 | Description string `json:"description"` 144 | Title string `json:"title"` 145 | URL string `json:"url"` 146 | WsURL string `json:"webSocketDebuggerUrl"` 147 | DevURL string `json:"devtoolsFrontendUrl"` 148 | } 149 | 150 | // NavigationEntry represent a navigation history entry. 151 | type NavigationEntry struct { 152 | ID int64 `json:"id"` 153 | URL string `json:"url"` 154 | Title string `json:"title"` 155 | } 156 | 157 | // Profile represents a profile data structure. 158 | type Profile struct { 159 | Nodes []ProfileNode `json:"nodes"` 160 | StartTime int64 `json:"startTime"` 161 | EndTime int64 `json:"endTime"` 162 | Samples []int64 `json:"samples"` 163 | TimeDeltas []int64 `json:"timeDeltas"` 164 | } 165 | 166 | // ProfileNode represents a profile node data structure. 167 | // The experimental fields are kept as json.RawMessage, so you may decode them with your own code, see: https://chromedevtools.github.io/debugger-protocol-viewer/tot/Profiler/ 168 | type ProfileNode struct { 169 | ID int64 `json:"id"` 170 | CallFrame json.RawMessage `json:"callFrame"` 171 | HitCount int64 `json:"hitCount"` 172 | Children []int64 `json:"children"` 173 | DeoptReason string `json:"deoptReason"` 174 | PositionTicks json.RawMessage `json:"positionTicks"` 175 | } 176 | 177 | // EvaluateError is returned by Evaluate in case of expression errors. 178 | type EvaluateError struct { 179 | ErrorDetails map[string]interface{} 180 | ExceptionDetails map[string]interface{} 181 | } 182 | 183 | func (err EvaluateError) Error() string { 184 | desc := err.ErrorDetails["description"].(string) 185 | if excp := err.ExceptionDetails; excp != nil { 186 | if excp["exception"] != nil { 187 | desc += fmt.Sprintf(" at line %v col %v", 188 | excp["lineNumber"].(float64), excp["columnNumber"].(float64)) 189 | } 190 | } 191 | 192 | return desc 193 | } 194 | 195 | type NavigationError string 196 | 197 | func (err NavigationError) Error() string { 198 | return "NavigationError:" + string(err) 199 | } 200 | 201 | // RemoteDebugger implements an interface for Chrome DevTools. 202 | type RemoteDebugger struct { 203 | http *httpclient.HttpClient 204 | ws *websocket.Conn 205 | current string 206 | reqID int 207 | verbose bool 208 | 209 | sync.Mutex 210 | closed chan bool 211 | 212 | requests chan Params 213 | responses map[int]chan json.RawMessage 214 | callbacks map[string]EventCallback 215 | domains map[string]bool 216 | events chan wsMessage 217 | } 218 | 219 | // Params is a type alias for the event params structure. 220 | type Params map[string]interface{} 221 | 222 | func (p Params) String(k string) string { 223 | val, _ := p[k].(string) 224 | return val 225 | } 226 | 227 | func (p Params) Int(k string) int { 228 | val, _ := p[k].(float64) 229 | return int(val) 230 | } 231 | 232 | func (p Params) Bool(k string) bool { 233 | val, _ := p[k].(bool) 234 | return val 235 | } 236 | 237 | func (p Params) Map(k string) map[string]interface{} { 238 | val, _ := p[k].(map[string]interface{}) 239 | return val 240 | } 241 | 242 | // EventCallback represents a callback event, associated with a method. 243 | type EventCallback func(params Params) 244 | 245 | type ConnectOption func(c *httpclient.HttpClient) 246 | 247 | // Host set the host header 248 | func Host(host string) ConnectOption { 249 | return func(c *httpclient.HttpClient) { 250 | c.Host = host 251 | } 252 | } 253 | 254 | // Headers set specified HTTP headers 255 | func Headers(headers map[string]string) ConnectOption { 256 | return func(c *httpclient.HttpClient) { 257 | c.Headers = headers 258 | } 259 | } 260 | 261 | // Connect to the remote debugger and return `RemoteDebugger` object. 262 | func Connect(port string, verbose bool, options ...ConnectOption) (*RemoteDebugger, error) { 263 | client := httpclient.NewHttpClient("http://" + port) 264 | 265 | for _, setOption := range options { 266 | setOption(client) 267 | } 268 | 269 | remote := &RemoteDebugger{ 270 | http: client, 271 | requests: make(chan Params), 272 | responses: map[int]chan json.RawMessage{}, 273 | callbacks: map[string]EventCallback{}, 274 | domains: map[string]bool{}, 275 | events: make(chan wsMessage, 256), 276 | closed: make(chan bool), 277 | verbose: verbose, 278 | } 279 | 280 | // remote.http.Verbose = verbose 281 | if verbose { 282 | httpclient.StartLogging(false, true, false) 283 | } 284 | 285 | if err := remote.connectWs(nil); err != nil { 286 | return nil, err 287 | } 288 | 289 | go remote.sendMessages() 290 | go remote.processEvents() 291 | return remote, nil 292 | } 293 | 294 | func (remote *RemoteDebugger) connectWs(tab *Tab) error { 295 | if tab == nil || len(tab.WsURL) == 0 { 296 | tabs, err := remote.TabList("page") 297 | if err != nil { 298 | return err 299 | } 300 | 301 | if len(tabs) == 0 { 302 | return ErrorNoActiveTab 303 | } 304 | 305 | if tab == nil { 306 | tab = tabs[0] 307 | } else { 308 | for _, t := range tabs { 309 | if tab.ID == t.ID { 310 | tab.WsURL = t.WsURL 311 | break 312 | } 313 | } 314 | } 315 | } 316 | 317 | if remote.ws != nil { 318 | if tab.ID == remote.current { 319 | // nothing to do 320 | return nil 321 | } 322 | 323 | if remote.verbose { 324 | log.Println("disconnecting from current tab, id", remote.current) 325 | } 326 | 327 | remote.Lock() 328 | ws := remote.ws 329 | remote.ws, remote.current = nil, "" 330 | remote.Unlock() 331 | 332 | _ = ws.Close() 333 | } 334 | 335 | if len(tab.WsURL) == 0 { 336 | return ErrorNoWsURL 337 | } 338 | 339 | // check websocket connection 340 | if remote.verbose { 341 | log.Println("connecting to tab", tab.WsURL) 342 | } 343 | 344 | d := &websocket.Dialer{ 345 | ReadBufferSize: MaxReadBufferSize, 346 | WriteBufferSize: MaxWriteBufferSize, 347 | } 348 | 349 | ws, _, err := d.Dial(tab.WsURL, nil) 350 | if err != nil { 351 | if remote.verbose { 352 | log.Println("dial error:", err) 353 | } 354 | return err 355 | } 356 | 357 | remote.Lock() 358 | remote.ws = ws 359 | remote.current = tab.ID 360 | remote.Unlock() 361 | 362 | go remote.readMessages(ws) 363 | return nil 364 | } 365 | 366 | func (remote *RemoteDebugger) socket() (ws *websocket.Conn) { 367 | remote.Lock() 368 | ws = remote.ws 369 | remote.Unlock() 370 | return 371 | } 372 | 373 | // Close the RemoteDebugger connection. 374 | func (remote *RemoteDebugger) Close() (err error) { 375 | remote.Lock() 376 | ws := remote.ws 377 | remote.ws = nil 378 | remote.Unlock() 379 | 380 | if ws != nil { // already closed 381 | close(remote.requests) 382 | close(remote.closed) 383 | err = ws.Close() 384 | } 385 | 386 | if remote.verbose { 387 | httpclient.StopLogging() 388 | } 389 | 390 | return 391 | } 392 | 393 | func (remote *RemoteDebugger) Verbose(v bool) { 394 | remote.verbose = v 395 | } 396 | 397 | type wsMessage struct { 398 | ID int `json:"id"` 399 | Result json.RawMessage `json:"result"` 400 | 401 | Method string `json:"Method"` 402 | Params json.RawMessage `json:"Params"` 403 | } 404 | 405 | // SendRequest sends a request and returns the reply as a a map. 406 | func (remote *RemoteDebugger) SendRequest(method string, params Params) (map[string]interface{}, error) { 407 | rawReply, err := remote.sendRawReplyRequest(method, params) 408 | if err != nil || rawReply == nil { 409 | return nil, err 410 | } 411 | return unmarshal(rawReply) 412 | } 413 | 414 | // sendRawReplyRequest sends a request and returns the reply bytes. 415 | func (remote *RemoteDebugger) sendRawReplyRequest(method string, params Params) ([]byte, error) { 416 | remote.Lock() 417 | if remote.ws == nil { 418 | remote.Unlock() 419 | return nil, ErrorClose 420 | } 421 | 422 | responseChan := make(chan json.RawMessage, 1) 423 | reqID := remote.reqID 424 | remote.responses[reqID] = responseChan 425 | remote.reqID++ 426 | remote.Unlock() 427 | 428 | command := Params{ 429 | "id": reqID, 430 | "method": method, 431 | "params": params, 432 | } 433 | 434 | remote.requests <- command 435 | reply := <-responseChan 436 | 437 | remote.Lock() 438 | delete(remote.responses, reqID) 439 | remote.Unlock() 440 | 441 | return reply, nil 442 | } 443 | 444 | func (remote *RemoteDebugger) sendMessages() { 445 | for message := range remote.requests { 446 | ws := remote.socket() 447 | if ws == nil { // the socket is now closed 448 | break 449 | } 450 | 451 | if remote.verbose { 452 | log.Printf("SEND %#v\n", message) 453 | } 454 | 455 | err := ws.WriteJSON(message) 456 | if err != nil { 457 | log.Println("write message:", err) 458 | } 459 | } 460 | } 461 | 462 | func permanentError(err error) bool { 463 | if websocket.IsUnexpectedCloseError(err) { 464 | log.Println("unexpected close error") 465 | return true 466 | } 467 | 468 | if neterr, ok := err.(net.Error); ok && !neterr.Temporary() { 469 | log.Println("permanent network error") 470 | return true 471 | } 472 | 473 | return false 474 | } 475 | 476 | func (remote *RemoteDebugger) readMessages(ws *websocket.Conn) { 477 | remoteClosed := false 478 | 479 | loop: 480 | for { 481 | select { 482 | case <-remote.closed: 483 | remoteClosed = true 484 | break loop 485 | 486 | default: 487 | if remote.socket() != ws { // this socket is now closed 488 | break loop 489 | } 490 | 491 | var message wsMessage 492 | 493 | err := ws.ReadJSON(&message) 494 | if err != nil { 495 | if remote.socket() != ws { // this socket is now closed 496 | continue // one more check for remote.closed 497 | } 498 | 499 | log.Println("read message:", err) 500 | if permanentError(err) { 501 | break loop 502 | } 503 | } else if message.Method != "" { 504 | if remote.verbose { 505 | log.Println("EVENT", message.Method, string(message.Params), len(remote.events)) 506 | } 507 | 508 | remote.Lock() 509 | _, ok := remote.callbacks[message.Method] 510 | remote.Unlock() 511 | 512 | if !ok { 513 | continue // don't queue unrequested events 514 | } 515 | 516 | select { 517 | case remote.events <- message: 518 | 519 | case <-remote.closed: 520 | remoteClosed = true 521 | break loop 522 | } 523 | } else { 524 | // 525 | // should be a method reply 526 | // 527 | if remote.verbose { 528 | log.Println("REPLY", message.ID, string(message.Result)) 529 | } 530 | 531 | remote.Lock() 532 | ch := remote.responses[message.ID] 533 | remote.Unlock() 534 | 535 | if ch != nil { 536 | ch <- message.Result 537 | } 538 | } 539 | } 540 | } 541 | 542 | // log.Println("exit readMessages", remoteClosed) 543 | 544 | if remoteClosed { 545 | remote.events <- wsMessage{Method: EventClosed, Params: []byte("{}")} 546 | close(remote.events) 547 | } else if remote.socket() == ws { // we should still be connected but something is wrong 548 | remote.events <- wsMessage{Method: EventDisconnect, Params: []byte("{}")} 549 | } 550 | } 551 | 552 | func (remote *RemoteDebugger) processEvents() { 553 | for ev := range remote.events { 554 | remote.Lock() 555 | cb := remote.callbacks[ev.Method] 556 | remote.Unlock() 557 | 558 | if cb != nil { 559 | var params Params 560 | if err := json.Unmarshal(ev.Params, ¶ms); err != nil { 561 | log.Println("unmarshal", string(ev.Params), len(ev.Params), err) 562 | } else { 563 | cb(params) 564 | } 565 | } 566 | } 567 | } 568 | 569 | // Version returns version information (protocol, browser, etc.). 570 | func (remote *RemoteDebugger) Version() (*Version, error) { 571 | resp, err := responseError(remote.http.Get("/json/version", nil, nil)) 572 | if err != nil { 573 | return nil, err 574 | } 575 | 576 | var version Version 577 | 578 | if err = decode(resp, &version); err != nil { 579 | return nil, err 580 | } 581 | 582 | return &version, nil 583 | } 584 | 585 | // Protocol returns the DevTools protocol specification 586 | func (remote *RemoteDebugger) Protocol() (map[string]interface{}, error) { 587 | resp, err := responseError(remote.http.Get("/json/protocol", nil, nil)) 588 | if err != nil { 589 | return nil, err 590 | } 591 | 592 | var proto map[string]interface{} 593 | if err = decode(resp, &proto); err != nil { 594 | return nil, err 595 | } 596 | 597 | return proto, nil 598 | } 599 | 600 | // TabList returns a list of opened tabs/pages. 601 | // If filter is not empty, only tabs of the specified type are returned (i.e. "page"). 602 | // 603 | // Note that tabs are ordered by activitiy time (most recently used first) so the 604 | // current tab is the first one of type "page". 605 | func (remote *RemoteDebugger) TabList(filter string) ([]*Tab, error) { 606 | resp, err := responseError(remote.http.Get("/json/list", nil, nil)) 607 | if err != nil { 608 | return nil, err 609 | } 610 | 611 | var tabs []*Tab 612 | 613 | if err = decode(resp, &tabs); err != nil { 614 | return nil, err 615 | } 616 | 617 | if filter == "" { 618 | return tabs, nil 619 | } 620 | 621 | var filtered []*Tab 622 | 623 | for _, t := range tabs { 624 | if t.Type == filter { 625 | filtered = append(filtered, t) 626 | } 627 | } 628 | 629 | return filtered, nil 630 | } 631 | 632 | // ActivateTab activates the specified tab. 633 | func (remote *RemoteDebugger) ActivateTab(tab *Tab) error { 634 | resp, err := responseError(remote.http.Get("/json/activate/"+tab.ID, nil, nil)) 635 | resp.Close() 636 | 637 | if err == nil { 638 | err = remote.connectWs(tab) 639 | 640 | if err == nil { 641 | for domain, state := range remote.domains { 642 | remote.DomainEvents(domain, state) 643 | } 644 | } 645 | } 646 | 647 | return err 648 | } 649 | 650 | // CloseTab closes the specified tab. 651 | func (remote *RemoteDebugger) CloseTab(tab *Tab) error { 652 | resp, err := responseError(remote.http.Get("/json/close/"+tab.ID, nil, nil)) 653 | resp.Close() 654 | return err 655 | } 656 | 657 | // NewTab creates a new tab. 658 | func (remote *RemoteDebugger) NewTab(url string) (*Tab, error) { 659 | path := "/json/new" 660 | if url != "" { 661 | path += "?" + url 662 | } 663 | 664 | resp, err := responseError(remote.http.Do(remote.http.Request("PUT", path, nil, nil))) 665 | if err != nil { 666 | return nil, err 667 | } 668 | 669 | var tab Tab 670 | if err = decode(resp, &tab); err != nil { 671 | return nil, err 672 | } 673 | 674 | if err = remote.connectWs(&tab); err != nil { 675 | return nil, err 676 | } 677 | 678 | return &tab, nil 679 | } 680 | 681 | // GetDomains lists the available DevTools domains. 682 | // 683 | // Deprecated: The Schema domain is now deprecated. 684 | func (remote *RemoteDebugger) GetDomains() ([]Domain, error) { 685 | res, err := remote.sendRawReplyRequest("Schema.getDomains", nil) 686 | if err != nil { 687 | return nil, err 688 | } 689 | 690 | var domains struct { 691 | Domains []Domain 692 | } 693 | 694 | err = json.Unmarshal(res, &domains) 695 | if err != nil { 696 | return nil, err 697 | } 698 | 699 | return domains.Domains, nil 700 | } 701 | 702 | // Navigate navigates to the specified URL. 703 | func (remote *RemoteDebugger) Navigate(url string) (string, error) { 704 | return remote.NavigateTransition(url, NoTransition) 705 | } 706 | 707 | type TransitionType string 708 | 709 | const ( 710 | NoTransition = TransitionType("") 711 | Reload = TransitionType("reload") 712 | 713 | /* 714 | "link", 715 | "typed", 716 | "address_bar", 717 | "auto_bookmark", 718 | "auto_subframe", 719 | "manual_subframe", 720 | "generated", 721 | "auto_toplevel", 722 | "form_submit", 723 | "reload", 724 | "keyword", 725 | "keyword_generated", 726 | "other" 727 | */ 728 | ) 729 | 730 | func (remote *RemoteDebugger) NavigateTransition(url string, trans TransitionType) (string, error) { 731 | params := Params{ 732 | "url": url, 733 | } 734 | 735 | if trans != NoTransition { 736 | params["transitionType"] = string(trans) 737 | } 738 | 739 | res, err := remote.SendRequest("Page.navigate", params) 740 | if err != nil { 741 | return "", err 742 | } 743 | 744 | if errorText, ok := res["errorText"]; ok { 745 | return "", NavigationError(errorText.(string)) 746 | } 747 | 748 | frameID, ok := res["frameId"] 749 | if !ok { 750 | return "", nil 751 | } 752 | return frameID.(string), nil 753 | } 754 | 755 | // Reload reloads the current page. 756 | func (remote *RemoteDebugger) Reload() error { 757 | _, err := remote.SendRequest("Page.reload", Params{ 758 | "ignoreCache": true, 759 | }) 760 | 761 | return err 762 | } 763 | 764 | // GetNavigationHistory returns navigation history for the current page. 765 | func (remote *RemoteDebugger) GetNavigationHistory() (int, []NavigationEntry, error) { 766 | rawReply, err := remote.sendRawReplyRequest("Page.getNavigationHistory", nil) 767 | 768 | if err != nil { 769 | return 0, nil, err 770 | } 771 | 772 | var history struct { 773 | Current int64 `json:"currentIndex"` 774 | Entries []NavigationEntry `json:"entries"` 775 | } 776 | 777 | if err := json.Unmarshal(rawReply, &history); err != nil { 778 | return 0, nil, err 779 | } 780 | 781 | return int(history.Current), history.Entries, nil 782 | } 783 | 784 | // SetControlNavigations toggles navigation throttling which allows programatic control over navigation and redirect response. 785 | func (remote *RemoteDebugger) SetControlNavigations(enabled bool) error { 786 | _, err := remote.SendRequest("Page.setControlNavigations", Params{ 787 | "enabled": enabled, 788 | }) 789 | 790 | return err 791 | } 792 | 793 | // ProcessNavigation should be sent in response to a navigationRequested or a redirectRequested event, telling the browser how to handle the navigation. 794 | func (remote *RemoteDebugger) ProcessNavigation(navigationID int, navigation NavigationResponse) error { 795 | _, err := remote.SendRequest("Page.processNavigation", Params{ 796 | "response": navigation, 797 | "navigationId": navigationID, 798 | }) 799 | 800 | return err 801 | } 802 | 803 | // CaptureScreenshot takes a screenshot, uses "png" as default format. 804 | func (remote *RemoteDebugger) CaptureScreenshot(format string, quality int, fromSurface bool) ([]byte, error) { 805 | if format == "" { 806 | format = "png" 807 | } 808 | 809 | res, err := remote.SendRequest("Page.captureScreenshot", Params{ 810 | "format": format, 811 | "quality": quality, 812 | "fromSurface": fromSurface, 813 | }) 814 | 815 | if err != nil { 816 | return nil, err 817 | } 818 | 819 | if res == nil { 820 | return nil, ErrorNoResponse 821 | } 822 | 823 | return base64.StdEncoding.DecodeString(res["data"].(string)) 824 | } 825 | 826 | // SaveScreenshot takes a screenshot and saves it to a file. 827 | func (remote *RemoteDebugger) SaveScreenshot(filename string, perm os.FileMode, quality int, fromSurface bool) error { 828 | var format string 829 | ext := filepath.Ext(filename) 830 | switch ext { 831 | case ".jpg": 832 | format = "jpeg" 833 | case ".png": 834 | format = "png" 835 | default: 836 | return errors.New("Image format not supported") 837 | } 838 | rawScreenshot, err := remote.CaptureScreenshot(format, quality, fromSurface) 839 | if err != nil { 840 | return err 841 | } 842 | return ioutil.WriteFile(filename, rawScreenshot, perm) 843 | } 844 | 845 | // PrintToPDFOption defines the functional option for PrintToPDF 846 | type PrintToPDFOption func(map[string]interface{}) 847 | 848 | // LandscapeMode instructs PrintToPDF to print pages in landscape mode 849 | func LandscapeMode() PrintToPDFOption { 850 | return func(o map[string]interface{}) { 851 | o["landscape"] = true 852 | } 853 | } 854 | 855 | // PortraitMode instructs PrintToPDF to print pages in portrait mode 856 | func PortraitMode() PrintToPDFOption { 857 | return func(o map[string]interface{}) { 858 | o["landscape"] = false 859 | } 860 | } 861 | 862 | // DisplayHeaderFooter instructs PrintToPDF to print headers/footers or not 863 | func DisplayHeaderFooter() PrintToPDFOption { 864 | return func(o map[string]interface{}) { 865 | o["displayHeaderFooter"] = true 866 | } 867 | } 868 | 869 | // printBackground instructs PrintToPDF to print background graphics 870 | func PrintBackground() PrintToPDFOption { 871 | return func(o map[string]interface{}) { 872 | o["printBackground"] = true 873 | } 874 | } 875 | 876 | // Scale instructs PrintToPDF to scale the pages (1.0 is current scale) 877 | func Scale(n float64) PrintToPDFOption { 878 | return func(o map[string]interface{}) { 879 | o["scale"] = n 880 | } 881 | } 882 | 883 | // Dimensions sets the current page dimensions for PrintToPDF 884 | func Dimensions(width, height float64) PrintToPDFOption { 885 | return func(o map[string]interface{}) { 886 | o["paperWidth"] = width 887 | o["paperHeight"] = height 888 | } 889 | } 890 | 891 | // Margins sets the margin sizes for PrintToPDF 892 | func Margins(top, bottom, left, right float64) PrintToPDFOption { 893 | return func(o map[string]interface{}) { 894 | o["marginTop"] = top 895 | o["marginBottom"] = bottom 896 | o["marginLeft"] = left 897 | o["marginRight"] = right 898 | } 899 | } 900 | 901 | // PageRanges instructs PrintToPDF to print only the specified range of pages 902 | func PageRanges(ranges string) PrintToPDFOption { 903 | return func(o map[string]interface{}) { 904 | o["pageRanges"] = ranges 905 | } 906 | } 907 | 908 | // PrintToPDF print the current page as PDF. 909 | func (remote *RemoteDebugger) PrintToPDF(options ...PrintToPDFOption) ([]byte, error) { 910 | mOptions := map[string]interface{}{} 911 | 912 | for _, o := range options { 913 | o(mOptions) 914 | } 915 | 916 | res, err := remote.SendRequest("Page.printToPDF", mOptions) 917 | if err != nil { 918 | return nil, err 919 | } 920 | 921 | if res == nil { 922 | return nil, ErrorNoResponse 923 | } 924 | 925 | return base64.StdEncoding.DecodeString(res["data"].(string)) 926 | } 927 | 928 | // SavePDF print current page as PDF and save to file 929 | func (remote *RemoteDebugger) SavePDF(filename string, perm os.FileMode, options ...PrintToPDFOption) error { 930 | rawPDF, err := remote.PrintToPDF(options...) 931 | if err != nil { 932 | return err 933 | } 934 | 935 | return ioutil.WriteFile(filename, rawPDF, perm) 936 | } 937 | 938 | // HandleJavaScriptDialog accepts or dismisses a Javascript initiated dialog. 939 | func (remote *RemoteDebugger) HandleJavaScriptDialog(accept bool, promptText string) error { 940 | _, err := remote.SendRequest("Page.handleJavaScriptDialog", Params{ 941 | "accept": accept, 942 | "promptText": promptText, 943 | }) 944 | 945 | return err 946 | } 947 | 948 | // SetDownloadBehaviour enable/disable downloads. 949 | func (remote *RemoteDebugger) SetDownloadBehavior(behavior DownloadBehavior, downloadPath string) error { 950 | params := Params{"behavior": behavior} 951 | if len(downloadPath) > 0 { 952 | params["downloadPath"] = downloadPath 953 | } 954 | 955 | _, err := remote.SendRequest("Page.setDownloadBehavior", params) 956 | return err 957 | } 958 | 959 | // GetResponseBody returns the response body of a given requestId (from the Network.responseReceived payload). 960 | func (remote *RemoteDebugger) GetResponseBody(req string) ([]byte, error) { 961 | res, err := remote.SendRequest("Network.getResponseBody", Params{ 962 | "requestId": req, 963 | }) 964 | 965 | if err != nil { 966 | return nil, err 967 | } 968 | 969 | body := res["body"] 970 | if body == nil { 971 | return nil, nil 972 | } 973 | 974 | if b, ok := res["base64Encoded"]; ok && b.(bool) { 975 | return base64.StdEncoding.DecodeString(body.(string)) 976 | } else { 977 | return []byte(body.(string)), nil 978 | } 979 | } 980 | 981 | func (remote *RemoteDebugger) GetResponseBodyForInterception(iid string) ([]byte, error) { 982 | res, err := remote.SendRequest("Network.getResponseBodyForInterception", Params{ 983 | "interceptionId": iid, 984 | }) 985 | 986 | if err != nil { 987 | return nil, err 988 | } else if b, ok := res["base64Encoded"]; ok && b.(bool) { 989 | return base64.StdEncoding.DecodeString(res["body"].(string)) 990 | } else { 991 | return []byte(res["body"].(string)), nil 992 | } 993 | } 994 | 995 | type Cookie struct { 996 | Name string `json:"name"` 997 | Value string `json:"value"` 998 | Domain string `json:"domain"` 999 | Path string `json:"path"` 1000 | Size int `json:"size"` 1001 | Expires float64 `json:"expires"` 1002 | HttpOnly bool `json:"httpOnly"` 1003 | Secure bool `json:"secure"` 1004 | Session bool `json:"session"` 1005 | SameSite string `json:"sameSite"` 1006 | } 1007 | 1008 | // GetCookies returns all browser cookies for the current URL. 1009 | // Depending on the backend support, will return detailed cookie information in the `cookies` field. 1010 | func (remote *RemoteDebugger) GetCookies(urls []string) ([]Cookie, error) { 1011 | params := Params{} 1012 | 1013 | if urls != nil { 1014 | params["urls"] = urls 1015 | } 1016 | 1017 | rawReply, err := remote.sendRawReplyRequest("Network.getCookies", params) 1018 | if err != nil { 1019 | return nil, err 1020 | } 1021 | 1022 | var cookies struct { 1023 | Cookies []Cookie `json:"cookies"` 1024 | } 1025 | 1026 | err = json.Unmarshal(rawReply, &cookies) 1027 | if err != nil { 1028 | log.Println("unmarshal:", err) 1029 | log.Println(string(rawReply)) 1030 | 1031 | return nil, err 1032 | } 1033 | 1034 | return cookies.Cookies, nil 1035 | } 1036 | 1037 | // GetAllCookies returns all browser cookies. Depending on the backend support, 1038 | // will return detailed cookie information in the `cookies` field. 1039 | func (remote *RemoteDebugger) GetAllCookies() ([]Cookie, error) { 1040 | rawReply, err := remote.sendRawReplyRequest("Network.getCookies", nil) 1041 | if err != nil { 1042 | return nil, err 1043 | } 1044 | 1045 | var cookies struct { 1046 | Cookies []Cookie `json:"cookies"` 1047 | } 1048 | 1049 | err = json.Unmarshal(rawReply, &cookies) 1050 | if err != nil { 1051 | log.Println("unmarshal:", err) 1052 | log.Println(string(rawReply)) 1053 | 1054 | return nil, err 1055 | } 1056 | 1057 | return cookies.Cookies, nil 1058 | } 1059 | 1060 | // Set browser cookies. 1061 | func (remote *RemoteDebugger) SetCookies(cookies []Cookie) error { 1062 | params := Params{} 1063 | params["cookies"] = cookies 1064 | 1065 | _, err := remote.SendRequest("Network.setCookies", params) 1066 | return err 1067 | } 1068 | 1069 | // Deletes browser cookies with matching name and url or domain/path pair. 1070 | // 1071 | // Parameters: 1072 | // 1073 | // name string: Name of the cookies to remove. 1074 | // url string: If specified, deletes all the cookies with the given name where domain and path match provided URL. 1075 | // domain string: If specified, deletes only cookies with the exact domain. 1076 | // path string: If specified, deletes only cookies with the exact path. 1077 | func (remote *RemoteDebugger) DeleteCookies(name, url, domain, path string) error { 1078 | params := Params{} 1079 | params["name"] = name 1080 | if url != "" { 1081 | params["url"] = url 1082 | } 1083 | if domain != "" { 1084 | params["domain"] = domain 1085 | } 1086 | if path != "" { 1087 | params["path"] = path 1088 | } 1089 | _, err := remote.SendRequest("Network.deleteCookies", params) 1090 | return err 1091 | } 1092 | 1093 | // Set browser cookie 1094 | func (remote *RemoteDebugger) SetCookie(cookie Cookie) bool { 1095 | params := Params{} 1096 | params["name"] = cookie.Name 1097 | params["value"] = cookie.Value 1098 | if cookie.Domain != "" { 1099 | params["domain"] = cookie.Domain 1100 | } 1101 | if cookie.Path != "" { 1102 | params["path"] = cookie.Path 1103 | } 1104 | if cookie.Secure { 1105 | params["secure"] = cookie.Secure 1106 | } 1107 | if cookie.HttpOnly { 1108 | params["httpOnly"] = cookie.HttpOnly 1109 | } 1110 | if cookie.SameSite != "" { 1111 | params["sameSite"] = cookie.SameSite 1112 | } 1113 | if cookie.Expires > 0 { 1114 | params["expires"] = cookie.Expires 1115 | } 1116 | 1117 | _, err := remote.SendRequest("Network.setCookies", params) 1118 | if err != nil { 1119 | return false 1120 | } 1121 | 1122 | return true 1123 | } 1124 | 1125 | type ResourceType string 1126 | 1127 | const ( 1128 | ResourceTypeDocument = ResourceType("Document") 1129 | ResourceTypeStylesheet = ResourceType("Stylesheet") 1130 | ResourceTypeImage = ResourceType("Image") 1131 | ResourceTypeMedia = ResourceType("Media") 1132 | ResourceTypeFont = ResourceType("Font") 1133 | ResourceTypeScript = ResourceType("Script") 1134 | ResourceTypeTextTrack = ResourceType("TextTrack") 1135 | ResourceTypeXHR = ResourceType("XHR") 1136 | ResourceTypeFetch = ResourceType("Fetch") 1137 | ResourceTypeEventSource = ResourceType("EventSource") 1138 | ResourceTypeWebSocket = ResourceType("WebSocket") 1139 | ResourceTypeManifest = ResourceType("Manifest") 1140 | ResourceTypeSignedExchange = ResourceType("SignedExchange") 1141 | ResourceTypePing = ResourceType("Ping") 1142 | ResourceTypeCSPViolationReport = ResourceType("CSPViolationReport") 1143 | ResourceTypeOther = ResourceType("Other") 1144 | ) 1145 | 1146 | type InterceptionStage string 1147 | type RequestStage string 1148 | 1149 | const ( 1150 | StageRequest = InterceptionStage("Request") 1151 | StageHeadersReceived = InterceptionStage("HeadersReceived") 1152 | 1153 | RequestStageRequest = RequestStage("Request") 1154 | RequestStageResponse = RequestStage("Response") 1155 | ) 1156 | 1157 | type RequestPattern struct { 1158 | UrlPattern string `json:"urlPattern,omitempty"` 1159 | ResourceType ResourceType `json:"resourceType,omitempty"` 1160 | InterceptionStage InterceptionStage `json:"interceptionStage,omitempty"` 1161 | } 1162 | 1163 | type FetchRequestPattern struct { 1164 | UrlPattern string `json:"urlPattern,omitempty"` 1165 | ResourceType ResourceType `json:"resourceType,omitempty"` 1166 | RequestStage RequestStage `json:"requestStage,omitempty"` 1167 | } 1168 | 1169 | // SetRequestInterception sets the requests to intercept that match the provided patterns 1170 | // and optionally resource types. 1171 | // 1172 | // Deprecated: use EnableRequestPaused instead. 1173 | func (remote *RemoteDebugger) SetRequestInterception(patterns ...RequestPattern) error { 1174 | _, err := remote.SendRequest("Network.setRequestInterception", Params{ 1175 | "patterns": patterns, 1176 | }) 1177 | return err 1178 | } 1179 | 1180 | // EnableRequestInterception enables interception, modification or cancellation of network requests 1181 | func (remote *RemoteDebugger) EnableRequestInterception(enabled bool) error { 1182 | if enabled { 1183 | return remote.SetRequestInterception(RequestPattern{UrlPattern: "*"}) 1184 | } else { 1185 | return remote.SetRequestInterception() 1186 | } 1187 | } 1188 | 1189 | // ContinueInterceptedRequest is the response to Network.requestIntercepted 1190 | // which either modifies the request to continue with any modifications, or blocks it, 1191 | // or completes it with the provided response bytes. 1192 | // 1193 | // If a network fetch occurs as a result which encounters a redirect an additional Network.requestIntercepted 1194 | // event will be sent with the same InterceptionId. 1195 | // 1196 | // Parameters: 1197 | // 1198 | // errorReason ErrorReason - if set this causes the request to fail with the given reason. 1199 | // rawResponse string - if set the requests completes using with the provided base64 encoded raw response, including HTTP status line and headers etc... 1200 | // url string - if set the request url will be modified in a way that's not observable by page. 1201 | // method string - if set this allows the request method to be overridden. 1202 | // postData string - if set this allows postData to be set. 1203 | // headers Headers - if set this allows the request headers to be changed. 1204 | // 1205 | // Deprecated: use ContinueRequest, FulfillRequest and FailRequest instead. 1206 | func (remote *RemoteDebugger) ContinueInterceptedRequest(interceptionID string, 1207 | errorReason ErrorReason, 1208 | rawResponse string, 1209 | url string, 1210 | method string, 1211 | postData string, 1212 | headers map[string]string) error { 1213 | params := Params{ 1214 | "interceptionId": interceptionID, 1215 | } 1216 | 1217 | if errorReason != "" { 1218 | params["errorReason"] = string(errorReason) 1219 | } 1220 | if rawResponse != "" { 1221 | params["rawResponse"] = rawResponse 1222 | } 1223 | if url != "" { 1224 | params["url"] = url 1225 | } 1226 | if method != "" { 1227 | params["method"] = method 1228 | } 1229 | if postData != "" { 1230 | params["postData"] = postData 1231 | } 1232 | if headers != nil { 1233 | params["headers"] = headers 1234 | } 1235 | 1236 | _, err := remote.SendRequest("Network.continueInterceptedRequest", params) 1237 | return err 1238 | } 1239 | 1240 | // EnableRequestPaused enables issuing of requestPaused events. 1241 | // A request will be paused until client calls one of 1242 | // failRequest, fulfillRequest or continueRequest/continueWithAuth. 1243 | // 1244 | // If patterns is specified, only requests matching any of these patterns will produce 1245 | // fetchRequested event and will be paused until clients response. 1246 | // If not set,all requests will be affected. 1247 | func (remote *RemoteDebugger) EnableRequestPaused(enable bool, patterns ...FetchRequestPattern) error { 1248 | if !enable { 1249 | _, err := remote.SendRequest("Fetch.disable", nil) 1250 | return err 1251 | } 1252 | 1253 | var params Params 1254 | 1255 | if len(patterns) > 0 { 1256 | params = Params{"patterns": patterns} 1257 | } 1258 | 1259 | _, err := remote.SendRequest("Fetch.enable", params) 1260 | return err 1261 | } 1262 | 1263 | // ContinueRequest is the response to Fetch.requestPaused 1264 | // which either modifies the request to continue with any modifications, or blocks it, 1265 | // or completes it with the provided response bytes. 1266 | // 1267 | // Parameters: 1268 | // 1269 | // url string - if set the request url will be modified in a way that's not observable by page. 1270 | // method string - if set this allows the request method to be overridden. 1271 | // postData string - if set this allows postData to be set. 1272 | // headers Headers - if set this allows the request headers to be changed. 1273 | func (remote *RemoteDebugger) ContinueRequest(requestID string, 1274 | url string, 1275 | method string, 1276 | postData string, 1277 | headers map[string]string) error { 1278 | params := Params{ 1279 | "requestId": requestID, 1280 | } 1281 | 1282 | if url != "" { 1283 | params["url"] = url 1284 | } 1285 | if method != "" { 1286 | params["method"] = method 1287 | } 1288 | if postData != "" { 1289 | params["postData"] = postData 1290 | } 1291 | if headers != nil { 1292 | params["headers"] = headers 1293 | } 1294 | 1295 | _, err := remote.SendRequest("Fetch.continueRequest", params) 1296 | return err 1297 | } 1298 | 1299 | // FailRequest causes the request to fail with specified reason. 1300 | func (remote *RemoteDebugger) FailRequest(requestID string, errorReason ErrorReason) error { 1301 | _, err := remote.SendRequest("Fetch.failRequest", Params{ 1302 | "requestId": requestID, 1303 | "errorReason": errorReason, 1304 | }) 1305 | 1306 | return err 1307 | } 1308 | 1309 | // FulfillRequest provides a response to the request. 1310 | func (remote *RemoteDebugger) FulfillRequest(requestID string, responseCode int, responsePhrase string, headers map[string]string, body []byte) error { 1311 | params := Params{ 1312 | "requestId": requestID, 1313 | "responseCode": responseCode, 1314 | } 1315 | 1316 | if responsePhrase != "" { 1317 | params["responsePhrase"] = responsePhrase 1318 | } 1319 | 1320 | if len(headers) > 0 { 1321 | var hlist = []map[string]string{} 1322 | 1323 | for k, v := range headers { 1324 | hlist = append(hlist, map[string]string{"name": k, "value": v}) 1325 | } 1326 | 1327 | params["responseHeaders"] = hlist 1328 | } 1329 | 1330 | if len(body) > 0 { 1331 | params["body"] = body 1332 | } 1333 | 1334 | _, err := remote.SendRequest("Fetch.fulfillRequest", params) 1335 | return err 1336 | } 1337 | 1338 | func (remote *RemoteDebugger) FetchResponseBody(requestId string) ([]byte, error) { 1339 | res, err := remote.SendRequest("Fetch.getResponseBody", Params{ 1340 | "requestId": requestId, 1341 | }) 1342 | 1343 | if err != nil { 1344 | return nil, err 1345 | } else if b, ok := res["base64Encoded"]; ok && b.(bool) { 1346 | return base64.StdEncoding.DecodeString(res["body"].(string)) 1347 | } else { 1348 | return []byte(res["body"].(string)), nil 1349 | } 1350 | } 1351 | 1352 | // GetDocument gets the "Document" object as a DevTool node. 1353 | func (remote *RemoteDebugger) GetDocument() (map[string]interface{}, error) { 1354 | return remote.SendRequest("DOM.getDocument", nil) 1355 | } 1356 | 1357 | // QuerySelector gets the nodeId for a specified selector. 1358 | func (remote *RemoteDebugger) QuerySelector(nodeID int, selector string) (map[string]interface{}, error) { 1359 | return remote.SendRequest("DOM.querySelector", Params{ 1360 | "nodeId": nodeID, 1361 | "selector": selector, 1362 | }) 1363 | } 1364 | 1365 | // QuerySelectorAll gets a list of nodeId for the specified selectors. 1366 | func (remote *RemoteDebugger) QuerySelectorAll(nodeID int, selector string) (map[string]interface{}, error) { 1367 | return remote.SendRequest("DOM.querySelectorAll", Params{ 1368 | "nodeId": nodeID, 1369 | "selector": selector, 1370 | }) 1371 | } 1372 | 1373 | // ResolveNode returns some information about the node. 1374 | func (remote *RemoteDebugger) ResolveNode(nodeID int) (map[string]interface{}, error) { 1375 | return remote.SendRequest("DOM.resolveNode", Params{ 1376 | "nodeId": nodeID, 1377 | }) 1378 | } 1379 | 1380 | // RequestNode requests a node, the response is generated as a DOM.setChildNodes event. 1381 | func (remote *RemoteDebugger) RequestNode(nodeID int) error { 1382 | _, err := remote.SendRequest("DOM.requestChildNodes", Params{ 1383 | "nodeId": nodeID, 1384 | }) 1385 | 1386 | return err 1387 | } 1388 | 1389 | // Focus sets focus on a specified node. 1390 | func (remote *RemoteDebugger) Focus(nodeID int) error { 1391 | _, err := remote.SendRequest("DOM.focus", Params{ 1392 | "nodeId": nodeID, 1393 | }) 1394 | 1395 | return err 1396 | } 1397 | 1398 | // SetInputFiles attaches input files to a specified node (an input[type=file] element?). 1399 | // Note: this has been renamed SetFileInputFiles 1400 | func (remote *RemoteDebugger) SetInputFiles(nodeID int, files []string) error { 1401 | return remote.SetFileInputFiles(nodeID, files, NodeId) 1402 | } 1403 | 1404 | // SetFileInputFiles sets files for the given file input element. 1405 | func (remote *RemoteDebugger) SetFileInputFiles(id int, files []string, idType IdType) error { 1406 | params := Params{"files": files} 1407 | 1408 | switch idType { 1409 | case NodeId: 1410 | params["nodeId"] = id 1411 | case BackendNodeId: 1412 | params["backendNodeId"] = id 1413 | case ObjectId: 1414 | params["objectId"] = id 1415 | } 1416 | 1417 | _, err := remote.SendRequest("DOM.setFileInputFiles", params) 1418 | return err 1419 | } 1420 | 1421 | // SetAttributeValue sets the value for a specified attribute. 1422 | func (remote *RemoteDebugger) SetAttributeValue(nodeID int, name, value string) error { 1423 | _, err := remote.SendRequest("DOM.setAttributeValue", Params{ 1424 | "nodeId": nodeID, 1425 | "name": name, 1426 | "value": value, 1427 | }) 1428 | 1429 | return err 1430 | } 1431 | 1432 | // GetOuterHTML returns node's HTML markup. 1433 | func (remote *RemoteDebugger) GetOuterHTML(nodeID int) (string, error) { 1434 | res, err := remote.SendRequest("DOM.getOuterHTML", Params{ 1435 | "nodeId": nodeID, 1436 | }) 1437 | 1438 | if err != nil { 1439 | return "", err 1440 | } 1441 | 1442 | return res["outerHTML"].(string), nil 1443 | } 1444 | 1445 | // SetOuterHTML sets node HTML markup. 1446 | func (remote *RemoteDebugger) SetOuterHTML(nodeID int, outerHTML string) error { 1447 | _, err := remote.SendRequest("DOM.setOuterHTML", Params{ 1448 | "nodeId": nodeID, 1449 | "outerHTML": outerHTML, 1450 | }) 1451 | 1452 | return err 1453 | } 1454 | 1455 | // GetBoxModel returns boxes for a DOM node identified by nodeId. 1456 | func (remote *RemoteDebugger) GetBoxModel(nodeID int) (map[string]interface{}, error) { 1457 | return remote.SendRequest("DOM.getBoxModel", Params{ 1458 | "nodeId": nodeID, 1459 | }) 1460 | } 1461 | 1462 | // GetComputedStyleForNode returns the computed style for a DOM node identified by nodeId. 1463 | func (remote *RemoteDebugger) GetComputedStyleForNode(nodeID int) (map[string]interface{}, error) { 1464 | return remote.SendRequest("CSS.getComputedStyleForNode", Params{ 1465 | "nodeId": nodeID, 1466 | }) 1467 | } 1468 | 1469 | // SetVisibleSize resizes the frame/viewport of the page. 1470 | // Note that this does not affect the frame's container (e.g. browser window). 1471 | // Can be used to produce screenshots of the specified size. 1472 | // 1473 | // Deprecated: Emulation.setVisibleSize is now deprecated. 1474 | func (remote *RemoteDebugger) SetVisibleSize(width, height int) error { 1475 | _, err := remote.SendRequest("Emulation.setVisibleSize", Params{ 1476 | "width": float64(width), 1477 | "height": float64(height), 1478 | }) 1479 | 1480 | return err 1481 | } 1482 | 1483 | // SetDeviceMetricsOverride sets mobile and fitWindow on top of device dimensions 1484 | // Can be used to produce screenshots of mobile viewports. 1485 | func (remote *RemoteDebugger) SetDeviceMetricsOverride(width int, height int, deviceScaleFactor float64, mobile bool, fitWindow bool) error { 1486 | _, err := remote.SendRequest("Emulation.setDeviceMetricsOverride", Params{ 1487 | "width": width, 1488 | "height": height, 1489 | "deviceScaleFactor": deviceScaleFactor, 1490 | "mobile": mobile, 1491 | "fitWindow": fitWindow}) 1492 | 1493 | return err 1494 | } 1495 | 1496 | type setVirtualTimerPolicyOption func(p Params) 1497 | 1498 | // If set, after this many virtual milliseconds have elapsed virtual time will be paused and a\nvirtualTimeBudgetExpired event is sent. 1499 | func Budget(budget int) setVirtualTimerPolicyOption { 1500 | return func(p Params) { 1501 | p["budget"] = float64(budget) 1502 | } 1503 | } 1504 | 1505 | // If set this specifies the maximum number of tasks that can be run before virtual is forced\nforwards to prevent deadlock. 1506 | func MaxVirtualTimeTaskStarvationCount(max int) setVirtualTimerPolicyOption { 1507 | return func(p Params) { 1508 | p["maxVirtualTimeTaskStarvationCount"] = float64(max) 1509 | } 1510 | } 1511 | 1512 | // If set the virtual time policy change should be deferred until any frame starts navigating.\nNote any previous deferred policy change is superseded. 1513 | func WaitForNavigation(wait bool) setVirtualTimerPolicyOption { 1514 | return func(p Params) { 1515 | p["waitForNavigation"] = wait 1516 | } 1517 | } 1518 | 1519 | // If set, base::Time::Now will be overriden to initially return this value. 1520 | func InitialVirtualTime(t time.Time) setVirtualTimerPolicyOption { 1521 | return func(p Params) { 1522 | p["initialVirtualTime"] = float64(t.Unix()) 1523 | } 1524 | } 1525 | 1526 | // SetVirtualTimePolicy turns on virtual time for all frames (replacing real-time with a synthetic time source) and sets the current virtual time policy. Note this supersedes any previous time budget. 1527 | func (remote *RemoteDebugger) SetVirtualTimePolicy(policy VirtualTimePolicy, budget int, options ...setVirtualTimerPolicyOption) error { 1528 | params := Params{"policy": policy} 1529 | 1530 | if budget > 0 { 1531 | params["budget"] = float64(budget) 1532 | params["waitForNavigation"] = true 1533 | } 1534 | 1535 | for _, opt := range options { 1536 | opt(params) 1537 | } 1538 | 1539 | _, err := remote.SendRequest("Emulation.setVirtualTimePolicy", params) 1540 | return err 1541 | } 1542 | 1543 | // SendRune sends a character as keyboard input. 1544 | func (remote *RemoteDebugger) SendRune(c rune) error { 1545 | if _, err := remote.SendRequest("Input.dispatchKeyEvent", Params{ 1546 | "type": "rawKeyDown", 1547 | "windowsVirtualKeyCode": int(c), 1548 | "nativeVirtualKeyCode": int(c), 1549 | "unmodifiedText": string(c), 1550 | "text": string(c), 1551 | }); err != nil { 1552 | return err 1553 | } 1554 | if _, err := remote.SendRequest("Input.dispatchKeyEvent", Params{ 1555 | "type": "char", 1556 | "windowsVirtualKeyCode": int(c), 1557 | "nativeVirtualKeyCode": int(c), 1558 | "unmodifiedText": string(c), 1559 | "text": string(c), 1560 | }); err != nil { 1561 | return err 1562 | } 1563 | _, err := remote.SendRequest("Input.dispatchKeyEvent", Params{ 1564 | "type": "keyUp", 1565 | "windowsVirtualKeyCode": int(c), 1566 | "nativeVirtualKeyCode": int(c), 1567 | "unmodifiedText": string(c), 1568 | "text": string(c), 1569 | }) 1570 | return err 1571 | } 1572 | 1573 | type MouseEvent string 1574 | type KeyModifier int 1575 | 1576 | const ( 1577 | MouseMove MouseEvent = "mouseMoved" 1578 | MousePress MouseEvent = "mousePressed" 1579 | MouseRelease MouseEvent = "mouseReleased" 1580 | 1581 | NoModifier KeyModifier = 0 1582 | AltKey KeyModifier = 1 1583 | CtrlKey KeyModifier = 2 1584 | MetaKey KeyModifier = 4 1585 | CommandKey KeyModifier = 4 1586 | ShiftKey KeyModifier = 8 1587 | ) 1588 | 1589 | type MouseOption func(p Params) 1590 | 1591 | func LeftButton() MouseOption { 1592 | return func(p Params) { 1593 | p["button"] = "left" 1594 | } 1595 | } 1596 | 1597 | func RightButton() MouseOption { 1598 | return func(p Params) { 1599 | p["button"] = "right" 1600 | } 1601 | } 1602 | 1603 | func MiddleButton() MouseOption { 1604 | return func(p Params) { 1605 | p["button"] = "middle" 1606 | } 1607 | } 1608 | 1609 | func Modifiers(m KeyModifier) MouseOption { 1610 | return func(p Params) { 1611 | p["modifiers"] = m 1612 | } 1613 | } 1614 | 1615 | func Clicks(c int) MouseOption { 1616 | return func(p Params) { 1617 | p["clickCount"] = c 1618 | } 1619 | } 1620 | 1621 | // MouseEvent dispatches a mouse event to the page. An event can be MouseMove, MousePressed and MouseReleased. 1622 | // An event always requires mouse coordinates, while other parameters are optional. 1623 | // 1624 | // To simulate mouse button presses, pass LeftButton()/RightButton()/MiddleButton() options and possibily key modifiers. 1625 | // It is also possible to pass the number of clicks (2 for double clicks, etc.). 1626 | func (remote *RemoteDebugger) MouseEvent(ev MouseEvent, x, y int, options ...MouseOption) error { 1627 | params := Params{ 1628 | "type": ev, 1629 | "x": x, 1630 | "y": y, 1631 | } 1632 | 1633 | for _, o := range options { 1634 | o(params) 1635 | } 1636 | 1637 | _, err := remote.SendRequest("Input.dispatchMouseEvent", params) 1638 | return err 1639 | } 1640 | 1641 | type EvaluateOption func(params Params) 1642 | 1643 | func UserGesture(enable bool) EvaluateOption { 1644 | return func(params Params) { 1645 | params["userGesture"] = enable 1646 | } 1647 | } 1648 | 1649 | func ReturnByValue(enable bool) EvaluateOption { 1650 | return func(params Params) { 1651 | params["returnByValue"] = enable 1652 | } 1653 | } 1654 | 1655 | func Silent(enable bool) EvaluateOption { 1656 | return func(params Params) { 1657 | params["silent"] = enable 1658 | } 1659 | } 1660 | 1661 | func IncludeCommandLineAPI(enable bool) EvaluateOption { 1662 | return func(params Params) { 1663 | params["includeCommandLineAPI"] = enable 1664 | } 1665 | } 1666 | 1667 | func GeneratePreview(enable bool) EvaluateOption { 1668 | return func(params Params) { 1669 | params["generatePreview"] = enable 1670 | } 1671 | } 1672 | 1673 | func ThrowOnSideEffect(enable bool) EvaluateOption { 1674 | return func(params Params) { 1675 | params["throwOnSideEffect"] = enable 1676 | } 1677 | } 1678 | 1679 | // Evaluate evalutes a Javascript function in the context of the current page. 1680 | func (remote *RemoteDebugger) Evaluate(expr string, options ...EvaluateOption) (interface{}, error) { 1681 | params := Params{ 1682 | "expression": expr, 1683 | "returnByValue": true, 1684 | } 1685 | 1686 | for _, opt := range options { 1687 | opt(params) 1688 | } 1689 | 1690 | res, err := remote.SendRequest("Runtime.evaluate", params) 1691 | if err != nil { 1692 | return nil, err 1693 | } 1694 | 1695 | if res == nil { 1696 | return nil, nil 1697 | } 1698 | 1699 | result := res["result"].(map[string]interface{}) 1700 | if subtype, ok := result["subtype"]; ok && subtype.(string) == "error" { 1701 | // this is actually an error 1702 | exception := res["exceptionDetails"].(map[string]interface{}) 1703 | return nil, EvaluateError{ErrorDetails: result, ExceptionDetails: exception} 1704 | } 1705 | 1706 | return result["value"], nil 1707 | } 1708 | 1709 | // EvaluateWrap evaluates a list of expressions, EvaluateWrap wraps them in `(function(){ ... })()`. 1710 | // Use a return statement to return a value. 1711 | func (remote *RemoteDebugger) EvaluateWrap(expr string, options ...EvaluateOption) (interface{}, error) { 1712 | expr = fmt.Sprintf("(function(){%v})()", expr) 1713 | return remote.Evaluate(expr, options...) 1714 | } 1715 | 1716 | // SetBlockedURLs blocks URLs from loading (wildcards '*' are allowed) 1717 | func (remote *RemoteDebugger) SetBlockedURLs(urls ...string) error { 1718 | _, err := remote.SendRequest("Network.setBlockedURLs", Params{ 1719 | "urls": urls, 1720 | }) 1721 | return err 1722 | } 1723 | 1724 | // SetUserAgent overrides the default user agent. 1725 | func (remote *RemoteDebugger) SetUserAgent(userAgent string) error { 1726 | _, err := remote.SendRequest("Network.setUserAgentOverride", Params{ 1727 | "userAgent": userAgent, 1728 | }) 1729 | return err 1730 | } 1731 | 1732 | func (remote *RemoteDebugger) GetCertificate(origin string) ([]string, error) { 1733 | resp, err := remote.SendRequest("Network.getCertificate", Params{ 1734 | "origin": origin, 1735 | }) 1736 | 1737 | tableNames := resp["tableNames"].([]interface{}) 1738 | certs := make([]string, len(tableNames)) 1739 | 1740 | for _, item := range tableNames { 1741 | certs = append(certs, item.(string)) 1742 | } 1743 | return certs, err 1744 | } 1745 | 1746 | func (remote *RemoteDebugger) ClearBrowserCache() error { 1747 | _, err := remote.SendRequest("Network.clearBrowserCache", nil) 1748 | return err 1749 | } 1750 | 1751 | func (remote *RemoteDebugger) ClearBrowserCookies() error { 1752 | _, err := remote.SendRequest("Network.clearBrowserCookies", nil) 1753 | return err 1754 | } 1755 | 1756 | // SetCacheDisabled toggles ignoring cache for each request. If `true`, cache will not be used. 1757 | func (remote *RemoteDebugger) SetCacheDisabled(disabled bool) error { 1758 | _, err := remote.SendRequest("Network.setCacheDisabled", Params{ 1759 | "cacheDisabled": disabled, 1760 | }) 1761 | return err 1762 | } 1763 | 1764 | // SetBypassServiceWorker toggles ignoring of service worker for each request 1765 | func (remote *RemoteDebugger) SetBypassServiceWorker(bypass bool) error { 1766 | _, err := remote.SendRequest("Network.setBypassServiceWorker", Params{ 1767 | "bypass": bypass, 1768 | }) 1769 | return err 1770 | } 1771 | 1772 | // CallbackEvent sets a callback for the specified event. 1773 | func (remote *RemoteDebugger) CallbackEvent(method string, cb EventCallback) { 1774 | remote.Lock() 1775 | remote.callbacks[method] = cb 1776 | remote.Unlock() 1777 | } 1778 | 1779 | // StartProfiler starts the profiler. 1780 | func (remote *RemoteDebugger) StartProfiler() error { 1781 | _, err := remote.SendRequest("Profiler.start", nil) 1782 | return err 1783 | } 1784 | 1785 | // StopProfiler stops the profiler. 1786 | // Returns a Profile data structure, as specified here: https://chromedevtools.github.io/debugger-protocol-viewer/tot/Profiler/#type-Profile 1787 | func (remote *RemoteDebugger) StopProfiler() (p Profile, err error) { 1788 | res, err := remote.sendRawReplyRequest("Profiler.stop", nil) 1789 | if err != nil { 1790 | return p, err 1791 | } 1792 | var response map[string]json.RawMessage 1793 | err = json.Unmarshal(res, &response) 1794 | if err != nil { 1795 | return p, err 1796 | } 1797 | err = json.Unmarshal(response["profile"], &p) 1798 | return p, err 1799 | } 1800 | 1801 | // SetProfilerSamplingInterval sets the profiler sampling interval in microseconds, must be called before StartProfiler. 1802 | func (remote *RemoteDebugger) SetProfilerSamplingInterval(n int64) error { 1803 | _, err := remote.SendRequest("Profiler.setSamplingInterval", Params{ 1804 | "interval": n, 1805 | }) 1806 | return err 1807 | } 1808 | 1809 | // StartPreciseCoverage enable precise code coverage. 1810 | func (remote *RemoteDebugger) StartPreciseCoverage(callCount, detailed bool) error { 1811 | _, err := remote.SendRequest("Profiler.startPreciseCoverage", Params{ 1812 | "callCount": callCount, 1813 | "detailed": detailed, 1814 | }) 1815 | return err 1816 | } 1817 | 1818 | // StopPreciseCoverage disable precise code coverage. 1819 | func (remote *RemoteDebugger) StopPreciseCoverage() error { 1820 | _, err := remote.SendRequest("Profiler.stopPreciseCoverage", nil) 1821 | return err 1822 | } 1823 | 1824 | // GetPreciseCoverage collects coverage data for the current isolate and resets execution counters. 1825 | func (remote *RemoteDebugger) GetPreciseCoverage(precise bool) ([]interface{}, error) { 1826 | var res map[string]interface{} 1827 | var err error 1828 | 1829 | if precise { 1830 | res, err = remote.SendRequest("Profiler.takePreciseCoverage", nil) 1831 | } else { 1832 | res, err = remote.SendRequest("Profiler.getBestEffortCoverage", nil) 1833 | } 1834 | if res == nil || err != nil { 1835 | return nil, err 1836 | } 1837 | //log.Println(res) 1838 | return res["result"].([]interface{}), nil 1839 | } 1840 | 1841 | // CloseBrowser gracefully closes the browser we are connected to 1842 | func (remote *RemoteDebugger) CloseBrowser() { 1843 | _, err := remote.SendRequest("Browser.close", nil) 1844 | if err != nil { 1845 | log.Println(err) 1846 | } 1847 | } 1848 | 1849 | // DomainEvents enables event listening in the specified domain. 1850 | func (remote *RemoteDebugger) DomainEvents(domain string, enable bool) error { 1851 | method := domain 1852 | 1853 | if enable { 1854 | remote.domains[method] = true 1855 | method += ".enable" 1856 | } else { 1857 | delete(remote.domains, method) 1858 | method += ".disable" 1859 | } 1860 | 1861 | _, err := remote.SendRequest(method, nil) 1862 | return err 1863 | } 1864 | 1865 | // AllEvents enables event listening for all domains. 1866 | func (remote *RemoteDebugger) AllEvents(enable bool) error { 1867 | domains, err := remote.GetDomains() 1868 | if err != nil { 1869 | return err 1870 | } 1871 | 1872 | for _, domain := range domains { 1873 | if err := remote.DomainEvents(domain.Name, enable); err != nil { 1874 | return err 1875 | } 1876 | } 1877 | 1878 | return nil 1879 | } 1880 | 1881 | // DOMEvents enables DOM events listening. 1882 | func (remote *RemoteDebugger) DOMEvents(enable bool) error { 1883 | return remote.DomainEvents("DOM", enable) 1884 | } 1885 | 1886 | // PageEvents enables Page events listening. 1887 | func (remote *RemoteDebugger) PageEvents(enable bool) error { 1888 | return remote.DomainEvents("Page", enable) 1889 | } 1890 | 1891 | // NetworkEvents enables Network events listening. 1892 | func (remote *RemoteDebugger) NetworkEvents(enable bool) error { 1893 | return remote.DomainEvents("Network", enable) 1894 | } 1895 | 1896 | // TargetEvents enables Target events listening. 1897 | func (remote *RemoteDebugger) TargetEvents(enable bool) error { 1898 | return remote.DomainEvents("Target", enable) 1899 | } 1900 | 1901 | // Retrieves a list of available targets. 1902 | func (remote *RemoteDebugger) GetTargets() (map[string]interface{}, error) { 1903 | resp, err := remote.SendRequest("Target.getTargets", Params{}) 1904 | 1905 | return resp, err 1906 | } 1907 | 1908 | // Controls whether to discover available targets and notify via 1909 | // `targetCreated/targetInfoChanged/targetDestroyed` events." 1910 | func (remote *RemoteDebugger) SetDiscoverTargets(discover bool) error { 1911 | _, err := remote.SendRequest("Target.setDiscoverTargets", Params{ 1912 | "discover": discover, 1913 | }) 1914 | return err 1915 | } 1916 | 1917 | // Controls whether to automatically attach to new targets which are considered to be related to 1918 | // this one. When turned on, attaches to all existing related targets as well. When turned off, 1919 | // automatically detaches from all currently attached targets. 1920 | // This also clears all targets added by `autoAttachRelated` from the list of targets to watch 1921 | // for creation of related targets.", 1922 | func (remote *RemoteDebugger) SetAutoAttach(autoAttach bool) error { 1923 | _, err := remote.SendRequest("Target.setAutoAttach", Params{ 1924 | "autoAttach": autoAttach, 1925 | }) 1926 | return err 1927 | } 1928 | 1929 | // Attaches to the target with given id. 1930 | func (remote *RemoteDebugger) AttachToTarget(targetId string) (string, error) { 1931 | res, err := remote.SendRequest("Target.attachToTarget", Params{ 1932 | "targetId": targetId, 1933 | }) 1934 | 1935 | if err != nil { 1936 | return "", err 1937 | } 1938 | 1939 | return res["sessionId"].(string), nil 1940 | } 1941 | 1942 | // RuntimeEvents enables Runtime events listening. 1943 | func (remote *RemoteDebugger) RuntimeEvents(enable bool) error { 1944 | return remote.DomainEvents("Runtime", enable) 1945 | } 1946 | 1947 | // LogEvents enables Log events listening. 1948 | func (remote *RemoteDebugger) LogEvents(enable bool) error { 1949 | return remote.DomainEvents("Log", enable) 1950 | } 1951 | 1952 | // DebuggerEvents enables DebugLog events listening. 1953 | func (remote *RemoteDebugger) DebuggerEvents(enable bool) error { 1954 | return remote.DomainEvents("Debugger", enable) 1955 | } 1956 | 1957 | func (remote *RemoteDebugger) DebuggerPause() error { 1958 | _, err := remote.SendRequest("Debugger.pause", nil) 1959 | return err 1960 | } 1961 | 1962 | func (remote *RemoteDebugger) DebuggerResume(terminateOnResume bool) error { 1963 | _, err := remote.SendRequest("Debugger.resume", Params{ 1964 | "terminateOnResume": terminateOnResume, 1965 | }) 1966 | return err 1967 | } 1968 | 1969 | func (remote *RemoteDebugger) DebuggerSkipAllPauses(skip bool) error { 1970 | _, err := remote.SendRequest("Debugger.setSkipAllPauses", Params{ 1971 | "skip": skip, 1972 | }) 1973 | return err 1974 | } 1975 | 1976 | func (remote *RemoteDebugger) DebuggerSetBreakpointsActive(active bool) error { 1977 | _, err := remote.SendRequest("Debugger.setBreakpointsActive", Params{ 1978 | "active": active, 1979 | }) 1980 | return err 1981 | } 1982 | 1983 | func (remote *RemoteDebugger) GetScriptSource(id string) (string, error) { 1984 | res, err := remote.SendRequest("Debugger.getScriptSource", Params{ 1985 | "scriptId": id, 1986 | }) 1987 | 1988 | if err != nil { 1989 | return "", err 1990 | } 1991 | 1992 | // should check for bytecode (wasm) 1993 | ss := res["scriptSource"] 1994 | if ss == nil { 1995 | return "", nil 1996 | } 1997 | 1998 | return ss.(string), nil 1999 | } 2000 | 2001 | func (remote *RemoteDebugger) SetScriptSource(id, source string) error { 2002 | res, err := remote.SendRequest("Debugger.setScriptSource", Params{ 2003 | "scriptId": id, 2004 | "scriptSource": source, 2005 | }) 2006 | 2007 | if err != nil { 2008 | return err 2009 | } 2010 | 2011 | if res == nil { 2012 | //log.Println("SetScriptSource: NO RESPONSE") 2013 | return nil 2014 | } 2015 | 2016 | status := res["status"].(string) 2017 | if status == "Ok" { 2018 | return nil 2019 | } 2020 | 2021 | return errors.New(status) 2022 | } 2023 | 2024 | // ProfilerEvents enables Profiler events listening. 2025 | func (remote *RemoteDebugger) ProfilerEvents(enable bool) error { 2026 | return remote.DomainEvents("Profiler", enable) 2027 | } 2028 | 2029 | // EmulationEvents enables Emulation events listening. 2030 | func (remote *RemoteDebugger) EmulationEvents(enable bool) error { 2031 | return remote.DomainEvents("Emulation", enable) 2032 | } 2033 | 2034 | // ServiceWorkerEvents enables ServiceWorker events listening. 2035 | func (remote *RemoteDebugger) ServiceWorkerEvents(enable bool) error { 2036 | return remote.DomainEvents("ServiceWorker", enable) 2037 | } 2038 | 2039 | // ConsoleAPICallback processes the Runtime.consolAPICalled event and returns printable info 2040 | func ConsoleAPICallback(cb func([]interface{})) EventCallback { 2041 | return func(params Params) { 2042 | l := []interface{}{"console." + params["type"].(string)} 2043 | 2044 | for _, a := range params["args"].([]interface{}) { 2045 | arg := a.(map[string]interface{}) 2046 | 2047 | if arg["value"] != nil { 2048 | l = append(l, arg["value"]) 2049 | } else if arg["preview"] != nil { 2050 | arg := arg["preview"].(map[string]interface{}) 2051 | 2052 | v := arg["description"].(string) + "{" 2053 | 2054 | for i, p := range arg["properties"].([]interface{}) { 2055 | if i > 0 { 2056 | v += ", " 2057 | } 2058 | 2059 | prop := p.(map[string]interface{}) 2060 | if prop["name"] != nil { 2061 | v += fmt.Sprintf("%q: ", prop["name"]) 2062 | } 2063 | 2064 | v += fmt.Sprintf("%v", prop["value"]) 2065 | } 2066 | 2067 | v += "}" 2068 | l = append(l, v) 2069 | } else { 2070 | l = append(l, arg["type"].(string)) 2071 | } 2072 | 2073 | } 2074 | 2075 | cb(l) 2076 | } 2077 | } 2078 | --------------------------------------------------------------------------------