-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: support BiDi protocol #1
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,8 +5,6 @@ import ( | |
"encoding/json" | ||
"flag" | ||
"fmt" | ||
"github.com/aerokube/selenoid/info" | ||
"github.com/docker/docker/api" | ||
"log" | ||
"net" | ||
"net/http" | ||
|
@@ -19,6 +17,9 @@ import ( | |
"syscall" | ||
"time" | ||
|
||
"github.com/aerokube/selenoid/info" | ||
"github.com/docker/docker/api" | ||
|
||
ggr "github.com/aerokube/ggr/config" | ||
"github.com/aerokube/selenoid/config" | ||
"github.com/aerokube/selenoid/jsonerror" | ||
|
@@ -389,9 +390,10 @@ func handler() http.Handler { | |
root.Handle(paths.VNC, websocket.Handler(vnc)) | ||
root.HandleFunc(paths.Logs, logs) | ||
root.HandleFunc(paths.Video, video) | ||
root.HandleFunc(paths.Download, reverseProxy(func(sess *session.Session) string { return sess.HostPort.Fileserver }, "DOWNLOADING_FILE")) | ||
root.HandleFunc(paths.Clipboard, reverseProxy(func(sess *session.Session) string { return sess.HostPort.Clipboard }, "CLIPBOARD")) | ||
root.HandleFunc(paths.Devtools, reverseProxy(func(sess *session.Session) string { return sess.HostPort.Devtools }, "DEVTOOLS")) | ||
root.HandleFunc(paths.Download, reverseProxy(func(sess *session.Session) string { return sess.HostPort.Fileserver }, ReverseProxyOpts{status: "DOWNLOADING_FILE"})) | ||
root.HandleFunc(paths.Clipboard, reverseProxy(func(sess *session.Session) string { return sess.HostPort.Clipboard }, ReverseProxyOpts{status: "CLIPBOARD"})) | ||
root.HandleFunc(paths.Devtools, reverseProxy(func(sess *session.Session) string { return sess.HostPort.Devtools }, ReverseProxyOpts{status: "DEVTOOLS"})) | ||
root.HandleFunc(seleniumPaths.ProxySession, reverseProxy(func(sess *session.Session) string { return sess.HostPort.BiDi }, ReverseProxyOpts{status: "BIDI", useOriginalPath: true})) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add new route |
||
if enableFileUpload { | ||
root.HandleFunc(paths.File, fileUpload) | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -56,6 +56,11 @@ type sess struct { | |
id string | ||
} | ||
|
||
type ReverseProxyOpts struct { | ||
status string | ||
useOriginalPath bool | ||
} | ||
|
||
// TODO There is simpler way to do this | ||
func (r request) localaddr() string { | ||
addr := r.Context().Value(http.LocalAddrContextKey).(net.Addr).String() | ||
|
@@ -433,6 +438,16 @@ func processBody(input []byte, host string) ([]byte, string, error) { | |
c["se:cdpVersion"] = bv | ||
} | ||
} | ||
|
||
if c["webSocketUrl"] != nil { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Modify |
||
u, err := url.Parse(c["webSocketUrl"].(string)) | ||
DudaGod marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if err != nil { | ||
return nil, sessionId, fmt.Errorf("parse 'websocketUrl' from response: %v", err) | ||
} else { | ||
u.Host = host | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Browser returns url with its own host which should be changed on selenoid host (like for |
||
c["webSocketUrl"] = u.String() | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
@@ -604,10 +619,14 @@ func defaultErrorHandler(requestId uint64) func(http.ResponseWriter, *http.Reque | |
} | ||
} | ||
|
||
func reverseProxy(hostFn func(sess *session.Session) string, status string) func(http.ResponseWriter, *http.Request) { | ||
func reverseProxy(hostFn func(sess *session.Session) string, opts ReverseProxyOpts) func(http.ResponseWriter, *http.Request) { | ||
return func(w http.ResponseWriter, r *http.Request) { | ||
requestId := serial() | ||
sid, remainingPath := splitRequestPath(r.URL.Path) | ||
if opts.useOriginalPath { | ||
remainingPath = r.URL.Path | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I need send request to browser with the same path which comes to selenoid. But for example for devtools protocol request to browser send just for another port. For example user send req to seleoid -> I just want to say that for BiDi protocol I don't need modify |
||
} | ||
|
||
sess, ok := sessions.Get(sid) | ||
if ok { | ||
select { | ||
|
@@ -623,7 +642,7 @@ func reverseProxy(hostFn func(sess *session.Session) string, status string) func | |
r.URL.Scheme = "http" | ||
r.URL.Host = hostFn(sess) | ||
r.URL.Path = remainingPath | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here for BiDi protocol I don't need to modify |
||
log.Printf("[%d] [%s] [%s] [%s]", requestId, status, sid, remainingPath) | ||
log.Printf("[%d] [%s] [%s] [%s]", requestId, opts.status, sid, remainingPath) | ||
}, | ||
ErrorHandler: defaultErrorHandler(requestId), | ||
}).ServeHTTP(w, r) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,6 +17,7 @@ import ( | |
|
||
ggr "github.com/aerokube/ggr/config" | ||
"github.com/aerokube/selenoid/config" | ||
"github.com/gorilla/websocket" | ||
"github.com/mafredri/cdp" | ||
"github.com/mafredri/cdp/rpcc" | ||
assert "github.com/stretchr/testify/require" | ||
|
@@ -980,6 +981,75 @@ func TestAddedSeCdpCapability(t *testing.T) { | |
queue.Release() | ||
} | ||
|
||
func TestBidi(t *testing.T) { | ||
DudaGod marked this conversation as resolved.
Show resolved
Hide resolved
|
||
manager = &HTTPTest{Handler: Selenium()} | ||
|
||
resp, err := http.Post(With(srv.URL).Path("/wd/hub/session"), "", bytes.NewReader([]byte("{}"))) | ||
assert.NoError(t, err) | ||
assert.Equal(t, resp.StatusCode, http.StatusOK) | ||
var sess map[string]string | ||
assert.NoError(t, json.NewDecoder(resp.Body).Decode(&sess)) | ||
|
||
u := fmt.Sprintf("ws://%s/session/%s", srv.Listener.Addr().String(), sess["sessionId"]) | ||
|
||
ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond) | ||
defer cancel() | ||
|
||
dialer := websocket.Dialer{} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here I use ws library to check bidi (in devtools used |
||
conn, _, err := dialer.DialContext(ctx, u, nil) | ||
assert.NoError(t, err) | ||
defer conn.Close() | ||
|
||
sessions.Remove(sess["sessionId"]) | ||
queue.Release() | ||
} | ||
|
||
func TestAddedWebSockerUrlCapability(t *testing.T) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Test writtern like for |
||
fn := func(input map[string]interface{}) { | ||
input["value"] = map[string]interface{}{ | ||
"sessionId": input["sessionId"], | ||
"capabilities": map[string]interface{}{"browserVersion": "some-version", "webSocketUrl": fmt.Sprintf("ws://localhost:4444/session/%s", input["sessionId"])}, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here I create |
||
} | ||
delete(input, "sessionId") | ||
} | ||
manager = &HTTPTest{Handler: Selenium(fn)} | ||
|
||
resp, err := http.Post(With(srv.URL).Path("/wd/hub/session"), "", bytes.NewReader([]byte("{}"))) | ||
assert.NoError(t, err) | ||
assert.Equal(t, resp.StatusCode, http.StatusOK) | ||
var sess map[string]interface{} | ||
assert.NoError(t, json.NewDecoder(resp.Body).Decode(&sess)) | ||
|
||
ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond) | ||
defer cancel() | ||
|
||
rv, ok := sess["value"] | ||
assert.True(t, ok) | ||
value, ok := rv.(map[string]interface{}) | ||
assert.True(t, ok) | ||
rc, ok := value["capabilities"] | ||
assert.True(t, ok) | ||
rs, ok := value["sessionId"] | ||
assert.True(t, ok) | ||
sessionId, ok := rs.(string) | ||
assert.True(t, ok) | ||
capabilities, ok := rc.(map[string]interface{}) | ||
assert.True(t, ok) | ||
rws, ok := capabilities["webSocketUrl"] | ||
assert.True(t, ok) | ||
ws, ok := rws.(string) | ||
assert.True(t, ok) | ||
assert.NotEmpty(t, ws) | ||
|
||
dialer := websocket.Dialer{} | ||
conn, _, err := dialer.DialContext(ctx, ws, nil) | ||
assert.NoError(t, err) | ||
defer conn.Close() | ||
|
||
sessions.Remove(sessionId) | ||
queue.Release() | ||
} | ||
|
||
func TestParseGgrHost(t *testing.T) { | ||
h := parseGgrHost("some-host.example.com:4444") | ||
assert.Equal(t, h.Name, "some-host.example.com") | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -54,6 +54,7 @@ func (m *HTTPTest) StartWithCancel() (*service.StartedService, error) { | |
Clipboard: u.Host, | ||
VNC: u.Host, | ||
Devtools: u.Host, | ||
BiDi: u.Host, | ||
}, | ||
Cancel: func() { | ||
log.Println("Stopping HTTPTest Service...") | ||
|
@@ -97,6 +98,41 @@ func (r With) Path(p string) string { | |
return fmt.Sprintf("%s%s", r, p) | ||
} | ||
|
||
func HandleWs(w http.ResponseWriter, r *http.Request) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Move code to handle ws connection to function in order to use it few routes (this code is used for testing purpose) |
||
upgrader := websocket.Upgrader{ | ||
CheckOrigin: func(_ *http.Request) bool { | ||
return true | ||
}, | ||
} | ||
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 | ||
} | ||
type req struct { | ||
ID uint64 `json:"id"` | ||
} | ||
var r req | ||
err = json.Unmarshal(message, &r) | ||
if err != nil { | ||
panic(err) | ||
} | ||
output, err := json.Marshal(r) | ||
DudaGod marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if err != nil { | ||
panic(err) | ||
} | ||
err = c.WriteMessage(mt, output) | ||
if err != nil { | ||
break | ||
} | ||
} | ||
} | ||
|
||
func Selenium(nsp ...func(map[string]interface{})) http.Handler { | ||
var lock sync.RWMutex | ||
sessions := make(map[string]struct{}) | ||
|
@@ -119,6 +155,10 @@ func Selenium(nsp ...func(map[string]interface{})) http.Handler { | |
_ = json.NewEncoder(w).Encode(&ret) | ||
}) | ||
mux.HandleFunc("/session/", func(w http.ResponseWriter, r *http.Request) { | ||
if r.Header.Get("Upgrade") != "" { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In order to handle ws connection by bidi url |
||
HandleWs(w, r) | ||
} | ||
|
||
u := strings.Split(r.URL.Path, "/")[2] | ||
lock.RLock() | ||
_, ok := sessions[u] | ||
|
@@ -147,40 +187,9 @@ func Selenium(nsp ...func(map[string]interface{})) http.Handler { | |
w.WriteHeader(http.StatusOK) | ||
_, _ = w.Write([]byte("test-data")) | ||
}) | ||
upgrader := websocket.Upgrader{ | ||
CheckOrigin: func(_ *http.Request) bool { | ||
return true | ||
}, | ||
} | ||
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { | ||
if r.Header.Get("Upgrade") != "" { | ||
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 | ||
} | ||
type req struct { | ||
ID uint64 `json:"id"` | ||
} | ||
var r req | ||
err = json.Unmarshal(message, &r) | ||
if err != nil { | ||
panic(err) | ||
} | ||
output, err := json.Marshal(r) | ||
if err != nil { | ||
panic(err) | ||
} | ||
err = c.WriteMessage(mt, output) | ||
if err != nil { | ||
break | ||
} | ||
} | ||
HandleWs(w, r) | ||
} | ||
w.WriteHeader(http.StatusOK) | ||
_, _ = w.Write([]byte("test-clipboard-value")) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it is changed by go extension in vscode (looks like some convention)