init
This commit is contained in:
@@ -0,0 +1 @@
|
||||
devtools
|
||||
@@ -0,0 +1,187 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/mafredri/cdp/devtool"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
devtoolsBaseDir = "/tmp"
|
||||
slash = "/"
|
||||
)
|
||||
|
||||
var (
|
||||
defaultDevtoolsHost = "127.0.0.1:9222"
|
||||
)
|
||||
|
||||
func root() http.Handler {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/browser", browser)
|
||||
mux.HandleFunc("/json/protocol", protocol)
|
||||
mux.HandleFunc("/page", page)
|
||||
mux.HandleFunc("/page/", page)
|
||||
mux.HandleFunc("/", browser)
|
||||
return mux
|
||||
}
|
||||
|
||||
func browser(w http.ResponseWriter, r *http.Request) {
|
||||
u, err := getBrowserWebSocketUrl()
|
||||
if err != nil {
|
||||
log.Printf("[BROWSER_URL_ERROR] [%v]", err)
|
||||
return
|
||||
}
|
||||
log.Printf("[BROWSER] [%s]", u.String())
|
||||
proxyWebSocket(w, r, u)
|
||||
}
|
||||
|
||||
func proxyWebSocket(w http.ResponseWriter, r *http.Request, u *url.URL) {
|
||||
u.Scheme = "http"
|
||||
(&httputil.ReverseProxy{
|
||||
Director: func(r *http.Request) {
|
||||
r.Host = "localhost"
|
||||
r.URL = u
|
||||
},
|
||||
}).ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func page(w http.ResponseWriter, r *http.Request) {
|
||||
fragments := strings.Split(r.URL.Path, slash)
|
||||
targetId := ""
|
||||
if len(fragments) == 3 {
|
||||
targetId = fragments[2]
|
||||
}
|
||||
u, err := getPageWebSocketUrl(targetId)
|
||||
if err != nil {
|
||||
log.Printf("[PAGE_URL_ERROR] [%v]", err)
|
||||
return
|
||||
}
|
||||
log.Printf("[PAGE] [%s]", u.String())
|
||||
proxyWebSocket(w, r, u)
|
||||
}
|
||||
|
||||
func protocol(w http.ResponseWriter, r *http.Request) {
|
||||
h, err := devtoolsHost()
|
||||
if err != nil {
|
||||
log.Printf("[DEVTOOLS_HOST_ERROR] [%v]", err)
|
||||
http.Error(w, fmt.Sprintf("Failed to detect devtools host: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
u := &url.URL{
|
||||
Host: h,
|
||||
Scheme: "http",
|
||||
Path: "/json/protocol",
|
||||
}
|
||||
log.Printf("[PROTOCOL] [%s]", u.String())
|
||||
(&httputil.ReverseProxy{
|
||||
Director: func(r *http.Request) {
|
||||
r.Host = "localhost"
|
||||
r.URL = u
|
||||
},
|
||||
}).ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func getBrowserWebSocketUrl() (*url.URL, error) {
|
||||
ctx := context.Background()
|
||||
h, err := devtoolsHost()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to detect devtools port: %v", err)
|
||||
}
|
||||
dt := devtool.New(fmt.Sprintf("http://%s", h))
|
||||
ver, err := dt.Version(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get browser websocket url: %v", err)
|
||||
}
|
||||
|
||||
wsUrl, err := url.Parse(ver.WebSocketDebuggerURL)
|
||||
if err == nil {
|
||||
return wsUrl, nil
|
||||
}
|
||||
return nil, errors.New("browser websocket URL information not found")
|
||||
}
|
||||
|
||||
func getPageWebSocketUrl(targetId string) (*url.URL, error) {
|
||||
ctx := context.Background()
|
||||
h, err := devtoolsHost()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to detect devtools port: %v", err)
|
||||
}
|
||||
dt := devtool.New(fmt.Sprintf("http://%s", h))
|
||||
targets, err := dt.List(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list targets: %v", err)
|
||||
}
|
||||
for _, t := range targets {
|
||||
if (targetId == "" && t.Type == devtool.Page) || targetId == t.ID {
|
||||
wsUrl, err := url.Parse(t.WebSocketDebuggerURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid websocket URL for matched target %s: %v", t.ID, err)
|
||||
}
|
||||
return wsUrl, nil
|
||||
}
|
||||
}
|
||||
return nil, errors.New("no matching target found")
|
||||
}
|
||||
|
||||
func devtoolsHost() (string, error) {
|
||||
if android {
|
||||
return androidDevtoolsHost()
|
||||
}
|
||||
return detectDevtoolsHost(devtoolsBaseDir), nil
|
||||
}
|
||||
|
||||
func androidDevtoolsHost() (string, error) {
|
||||
const androidDevtoolsPort = 9333
|
||||
cmd := exec.Command("adb", "forward", fmt.Sprintf("tcp:%d", androidDevtoolsPort), "localabstract:chrome_devtools_remote")
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to forward devtools port: %v", err)
|
||||
}
|
||||
return fmt.Sprintf("localhost:%d", androidDevtoolsPort), nil
|
||||
}
|
||||
|
||||
func detectDevtoolsHost(baseDir string) string {
|
||||
var candidates []string
|
||||
for _, glob := range []string{".com.google.Chrome*", ".org.chromium.Chromium*"} {
|
||||
cds, err := filepath.Glob(filepath.Join(baseDir, glob))
|
||||
if err == nil {
|
||||
candidates = append(candidates, cds...)
|
||||
}
|
||||
|
||||
}
|
||||
for _, c := range candidates {
|
||||
f, err := os.Stat(c)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if !f.IsDir() {
|
||||
continue
|
||||
}
|
||||
portFile := filepath.Join(c, "DevToolsActivePort")
|
||||
data, err := ioutil.ReadFile(portFile)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
lines := strings.Split(string(data), "\n")
|
||||
if len(lines) == 0 {
|
||||
continue
|
||||
}
|
||||
port, err := strconv.Atoi(lines[0])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
return fmt.Sprintf("127.0.0.1:%d", port)
|
||||
}
|
||||
return defaultDevtoolsHost
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
. "github.com/aandryashin/matchers"
|
||||
. "github.com/aandryashin/matchers/httpresp"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/mafredri/cdp"
|
||||
"github.com/mafredri/cdp/protocol/target"
|
||||
"github.com/mafredri/cdp/rpcc"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
srv *httptest.Server
|
||||
devtoolsSrv *httptest.Server
|
||||
)
|
||||
|
||||
func init() {
|
||||
srv = httptest.NewServer(root())
|
||||
listen = srv.Listener.Addr().String()
|
||||
devtoolsSrv = httptest.NewServer(mockDevtoolsMux())
|
||||
defaultDevtoolsHost = devtoolsSrv.Listener.Addr().String()
|
||||
}
|
||||
|
||||
func mockDevtoolsMux() http.Handler {
|
||||
mux := http.NewServeMux()
|
||||
version := func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
w.Write([]byte(fmt.Sprintf(`{
|
||||
"Browser": "Chrome/72.0.3601.0",
|
||||
"Protocol-Version": "1.3",
|
||||
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3601.0 Safari/537.36",
|
||||
"V8-Version": "7.2.233",
|
||||
"WebKit-Version": "537.36 (@cfede9db1d154de0468cb0538479f34c0755a0f4)",
|
||||
"webSocketDebuggerUrl": "ws://%s/devtools/browser/b0b8a4fb-bb17-4359-9533-a8d9f3908bd8"
|
||||
}`, defaultDevtoolsHost)))
|
||||
}
|
||||
mux.HandleFunc("/json/version", version)
|
||||
listTargets := func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
w.Write([]byte(fmt.Sprintf(`[ {
|
||||
"description": "",
|
||||
"devtoolsFrontendUrl": "/devtools/inspector.html?ws=localhost:9222/devtools/page/one",
|
||||
"id": "one",
|
||||
"title": "Aerokube",
|
||||
"type": "page",
|
||||
"url": "https://www.aerokube.com/",
|
||||
"webSocketDebuggerUrl": "ws://%s/devtools/page/one"
|
||||
}, {
|
||||
"description": "",
|
||||
"devtoolsFrontendUrl": "/devtools/inspector.html?ws=localhost:9222/devtools/page/two",
|
||||
"id": "two",
|
||||
"title": "Aerokube Selenoid",
|
||||
"type": "page",
|
||||
"url": "https://selenoid.aerokube.com/",
|
||||
"webSocketDebuggerUrl": "ws://%s/devtools/page/two"
|
||||
} ]`, defaultDevtoolsHost, defaultDevtoolsHost)))
|
||||
|
||||
}
|
||||
mux.HandleFunc("/json/list", listTargets)
|
||||
mux.HandleFunc("/json", listTargets)
|
||||
|
||||
mux.HandleFunc("/json/protocol", func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
w.Write([]byte("{}"))
|
||||
})
|
||||
|
||||
upgrader := websocket.Upgrader{
|
||||
CheckOrigin: func(_ *http.Request) bool {
|
||||
return true
|
||||
},
|
||||
}
|
||||
|
||||
devtoolsEcho := func(w http.ResponseWriter, r *http.Request) {
|
||||
//Echo request ID but omit Method
|
||||
c, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer c.Close()
|
||||
for {
|
||||
mt, message, err := c.ReadMessage()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
var req rpcc.Request
|
||||
err = json.Unmarshal(message, &req)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
resp := rpcc.Response{
|
||||
ID: req.ID,
|
||||
}
|
||||
if req.Method == "Target.getTargets" {
|
||||
result := target.GetTargetsReply{
|
||||
TargetInfos: []target.Info{
|
||||
{TargetID: "one", Type: "page"},
|
||||
{TargetID: "two", Type: "page"},
|
||||
},
|
||||
}
|
||||
resBytes, _ := json.Marshal(result)
|
||||
resp.Result = json.RawMessage(resBytes)
|
||||
}
|
||||
output, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
err = c.WriteMessage(mt, output)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mux.HandleFunc("/devtools/browser/", devtoolsEcho)
|
||||
mux.HandleFunc("/devtools/page/", devtoolsEcho)
|
||||
return mux
|
||||
}
|
||||
|
||||
func TestProtocol(t *testing.T) {
|
||||
u := fmt.Sprintf("http://%s/json/protocol", srv.Listener.Addr().String())
|
||||
resp, err := http.Get(u)
|
||||
AssertThat(t, err, Is{nil})
|
||||
AssertThat(t, resp, Code{200})
|
||||
}
|
||||
|
||||
func TestDevtools(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
browserWs := fmt.Sprintf("ws://%s/", srv.Listener.Addr().String())
|
||||
browserConn, err := rpcc.DialContext(ctx, browserWs)
|
||||
AssertThat(t, err, Is{nil})
|
||||
defer browserConn.Close()
|
||||
|
||||
browserClient := cdp.NewClient(browserConn)
|
||||
targets, err := browserClient.Target.GetTargets(ctx)
|
||||
AssertThat(t, err, Is{nil})
|
||||
AssertThat(t, len(targets.TargetInfos), EqualTo{2})
|
||||
|
||||
defaultPageWs := fmt.Sprintf("ws://%s/page", srv.Listener.Addr().String())
|
||||
defaultPageConn, err := rpcc.DialContext(ctx, defaultPageWs)
|
||||
AssertThat(t, err, Is{nil})
|
||||
defer defaultPageConn.Close()
|
||||
defaultPageClient := cdp.NewClient(defaultPageConn)
|
||||
err = defaultPageClient.Page.Enable(ctx)
|
||||
AssertThat(t, err, Is{nil})
|
||||
|
||||
selectedPageWs := fmt.Sprintf("ws://%s/page/%s", srv.Listener.Addr().String(), targets.TargetInfos[1].TargetID)
|
||||
selectedPageConn, err := rpcc.DialContext(ctx, selectedPageWs)
|
||||
AssertThat(t, err, Is{nil})
|
||||
defer selectedPageConn.Close()
|
||||
selectedPageClient := cdp.NewClient(selectedPageConn)
|
||||
err = selectedPageClient.Page.Enable(ctx)
|
||||
AssertThat(t, err, Is{nil})
|
||||
}
|
||||
|
||||
func TestDetectDevtoolsHost(t *testing.T) {
|
||||
name, _ := ioutil.TempDir("", "devtools")
|
||||
defer os.RemoveAll(name)
|
||||
profilePath := filepath.Join(name, ".org.chromium.Chromium.deadbee")
|
||||
os.MkdirAll(profilePath, os.ModePerm)
|
||||
portFile := filepath.Join(profilePath, "DevToolsActivePort")
|
||||
ioutil.WriteFile(portFile, []byte("12345\n/devtools/browser/6f37c7fe-a0a6-4346-a6e2-80c5da0687f0"), 0644)
|
||||
|
||||
AssertThat(t, detectDevtoolsHost(name), EqualTo{"127.0.0.1:12345"})
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
module github.com/aerokube/devtools
|
||||
|
||||
go 1.14
|
||||
|
||||
require (
|
||||
github.com/aandryashin/matchers v0.0.0-20161126170413-435295ea180e
|
||||
github.com/google/go-cmp v0.2.0 // indirect
|
||||
github.com/gorilla/websocket v1.4.2
|
||||
github.com/mafredri/cdp v0.23.4
|
||||
golang.org/x/net v0.0.0-20190107210223-45ffb0cd1ba0 // indirect
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f // indirect
|
||||
)
|
||||
@@ -0,0 +1,12 @@
|
||||
github.com/aandryashin/matchers v0.0.0-20161126170413-435295ea180e h1:ogUKYFNcdYUIBSLibE4+EjbTJazoHr5JsWWx21Lpn8c=
|
||||
github.com/aandryashin/matchers v0.0.0-20161126170413-435295ea180e/go.mod h1:cbmYNkm9xeQlNoWEPtOUcvNok2gSD7ErMnYkRW+eHi8=
|
||||
github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/mafredri/cdp v0.23.4 h1:ffp4qq6slfCL4rFWBDeRHapkLE776gER4tX5Z3LS8CY=
|
||||
github.com/mafredri/cdp v0.23.4/go.mod h1:hgdiA0yp1uqhSaDOHJWPgXpMbh+LAfUdD9vbN2AM8gE=
|
||||
golang.org/x/net v0.0.0-20190107210223-45ffb0cd1ba0 h1:1DW40AJQ7AP4nY6ORUGUdkpXyEC9W2GAXcOPaMZK0K8=
|
||||
golang.org/x/net v0.0.0-20190107210223-45ffb0cd1ba0/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -0,0 +1,23 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
var (
|
||||
listen string
|
||||
android bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
flag.StringVar(&listen, "listen", ":7070", "Network address to accept connections")
|
||||
flag.BoolVar(&android, "android", false, "Whether we need to forward Android Emulator devtools port")
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
log.Printf("[INIT] [Listening on %s]", listen)
|
||||
log.Fatal(http.ListenAndServe(listen, root()))
|
||||
}
|
||||
Reference in New Issue
Block a user