Skip to content

Commit

Permalink
Added ability to proxy downloaded files (fixes #168)
Browse files Browse the repository at this point in the history
  • Loading branch information
vania-pooh committed Apr 12, 2018
1 parent 99ae3a4 commit 27217b2
Show file tree
Hide file tree
Showing 5 changed files with 83 additions and 17 deletions.
14 changes: 14 additions & 0 deletions docs/download.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
== Proxying Downloaded Files

Similarly to proxying video files Ggr is able to return any files downloaded by browser in **running** Selenium session.

. Downloaded files are expected to be stored on the hub hosts and accessible via the following URL:

http://hub-host.example.com:4444/download/<real-session-id>/filename.txt

+
Such notation for example is supported by http://aerokube.com/selenoid/latest[Selenoid].
. To get downloaded file via Ggr just use the same request but with the session ID returned to test:

$ curl http://ggr-host.example.com:4444/video/<test-session-id>/filename.txt

1 change: 1 addition & 0 deletions docs/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ include::users-file.adoc[leveloffset=+1]
include::quota-files.adoc[leveloffset=+1]
include::quota-reload.adoc[leveloffset=+1]
include::video.adoc[leveloffset=+1]
include::download.adoc[leveloffset=+1]
include::how-it-works.adoc[leveloffset=+1]
include::multiple-instances.adoc[leveloffset=+1]
include::log-files.adoc[leveloffset=+1]
Expand Down
3 changes: 3 additions & 0 deletions docs/log-files.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,12 @@ The following statuses are available:
| CLIENT_DISCONNECTED | User disconnected and doing session attempts was interrupted
| INIT | Server initialization messages
| INVALID_HOST_VNC_URL | Failed to parse VNC host URL specified in quota configuration
| INVALID_DOWNLOAD_REQUEST_URL | Download request URL do not contain enough information to determine upstream host
| INVALID_VNC_REQUEST_URL | VNC request URL do not contain enough information to determine upstream host
| INVALID_VIDEO_REQUEST_URL | Video request URL do not contain enough information to determine upstream host
| INVALID_URL | Session ID does not contain information about host where it was created
| PROXYING | Proxying Selenium request (shown in verbose mode only)
| PROXYING_DOWNLOAD | Starting to proxy downloaded file from upstream host
| PROXYING_TO_VNC | Starting to proxy VNC traffic
| PROXYING_VIDEO | Starting to proxy video from upstream host
| QUOTA_INFO_REQUESTED | Quota information request arrived
Expand All @@ -53,6 +55,7 @@ The following statuses are available:
| SESSION_FAILED | Session attempt on specified host failed
| SESSION_NOT_CREATED | Attempts to create a new session on all hosts failed. An error was returned to user.
| SHUTTING_DOWN | Server is shutting down and waiting graceful shutdown timeout for currently proxied requests to finish
| UNKNOWN_DOWNLOAD_HOST | Requested to proxy downloaded file to host not present in quota
| UNKNOWN_VNC_HOST | Requested to proxy VNC to host not present in quota
| UNKNOWN_VIDEO_HOST | Requested to proxy video to host not present in quota
| UNSUPPORTED_BROWSER | Requested browser name and version is not present in quota
Expand Down
30 changes: 22 additions & 8 deletions proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ const (
proxyPath = routePath + "/"
vncPath = "/vnc/"
videoPath = "/video/"
downloadPath = "/download/"
head = len(proxyPath)
md5SumLength = 32
tail = head + md5SumLength
Expand Down Expand Up @@ -628,32 +629,44 @@ func proxyConn(id uint64, wsconn *websocket.Conn, conn net.Conn, err error, sess
}

func video(w http.ResponseWriter, r *http.Request) {
proxyStatic(w, r, videoPath, "INVALID_VIDEO_REQUEST_URL", "PROXYING_VIDEO", "UNKNOWN_VIDEO_HOST", func(sessionId string) string {
return fmt.Sprintf("/video/%s.mp4", sessionId)
})
}

func download(w http.ResponseWriter, r *http.Request) {
proxyStatic(w, r, downloadPath, "INVALID_DOWNLOAD_REQUEST_URL", "PROXYING_DOWNLOAD", "UNKNOWN_DOWNLOAD_HOST", func(remainder string) string {
return fmt.Sprintf("/download/%s", remainder)
})
}

func proxyStatic(w http.ResponseWriter, r *http.Request, route string, invalidUrlMessage string, proxyingMessage string, unknownHostMessage string, pathProvider func(string) string) {
confLock.RLock()
defer confLock.RUnlock()

id := serial()
user, remote := info(r)
head := len(videoPath)
head := len(route)
tail := head + md5SumLength
path := r.URL.Path
if len(path) < tail {
log.Printf("[%d] [-] [INVALID_VIDEO_REQUEST_URL] [%s] [%s] [%s] [-] [-] [-] [-]\n", id, user, remote, path)
reply(w, errMsg("invalid video request URL"), http.StatusNotFound)
log.Printf("[%d] [-] [%s] [%s] [%s] [%s] [-] [-] [-] [-]\n", id, invalidUrlMessage, user, remote, path)
reply(w, errMsg("invalid request URL"), http.StatusNotFound)
return
}
sum := path[head:tail]
sessionID := path[tail:]
remainder := path[tail:]
h, ok := routes[sum]
if ok {
(&httputil.ReverseProxy{Director: func(r *http.Request) {
r.URL.Scheme = "http"
r.URL.Host = h.net()
r.URL.Path = fmt.Sprintf("/video/%s.mp4", sessionID)
log.Printf("[%d] [-] [PROXYING_VIDEO] [%s] [%s] [%s] [-] [%s] [-] [-]\n", id, user, remote, r.URL, sessionID)
r.URL.Path = pathProvider(remainder)
log.Printf("[%d] [-] [%s] [%s] [%s] [%s] [-] [%s] [-] [-]\n", id, proxyingMessage, user, remote, r.URL, remainder)
}}).ServeHTTP(w, r)
} else {
log.Printf("[%d] [-] [UNKNOWN_VIDEO_HOST] [%s] [%s] [-] [-] [%s] [-] [-]\n", id, user, remote, sum)
reply(w, errMsg("unknown video host"), http.StatusNotFound)
log.Printf("[%d] [-] [%s] [%s] [%s] [-] [-] [%s] [-] [-]\n", id, unknownHostMessage, user, remote, sum)
reply(w, errMsg("unknown host"), http.StatusNotFound)
}
}

Expand All @@ -671,5 +684,6 @@ func mux() http.Handler {
mux.Handle(proxyPath, &httputil.ReverseProxy{Director: proxy})
mux.Handle(vncPath, websocket.Handler(vnc))
mux.HandleFunc(videoPath, WithSuitableAuthentication(authenticator, video))
mux.HandleFunc(downloadPath, WithSuitableAuthentication(authenticator, download))
return mux
}
52 changes: 43 additions & 9 deletions proxy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -276,28 +276,44 @@ func TestProxyScreenWebSocketsProtocol(t *testing.T) {
}

func TestProxyVideoFileWithoutAuth(t *testing.T) {
rsp, err := http.Get(gridrouter("/video/123.mp4"))
rsp, err := http.Get(gridrouter("/video/123"))

AssertThat(t, err, Is{nil})
AssertThat(t, rsp, Code{http.StatusUnauthorized})
}

func TestProxyVideoFile(t *testing.T) {

test.Lock()
defer test.Unlock()

fileServer, sessionID := prepareMockFileServer("/video/123.mp4")
defer fileServer.Close()

rsp, err := doBasicHTTPRequest(http.MethodGet, gridrouter(fmt.Sprintf("/video/%s", sessionID)), nil)
AssertThat(t, err, Is{nil})
AssertThat(t, rsp, Code{http.StatusOK})

rsp, err = doBasicHTTPRequest(http.MethodGet, gridrouter("/video/missing-file"), nil)
AssertThat(t, err, Is{nil})
AssertThat(t, rsp, Code{http.StatusNotFound})

rsp, err = doBasicHTTPRequest(http.MethodGet, gridrouter("/video/f7fd94f75c79c36e547c091632da440f_missing-file"), nil)
AssertThat(t, err, Is{nil})
AssertThat(t, rsp, Code{http.StatusNotFound})
}

func prepareMockFileServer(path string) (*httptest.Server, string) {
mux := http.NewServeMux()
mux.HandleFunc("/video/123.mp4", func(w http.ResponseWriter, r *http.Request) {
mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})

fileServer := httptest.NewServer(mux)
defer fileServer.Close()

host, port := hostportnum(fileServer.URL)
node := Host{Name: host, Port: port, Count: 1}

test.Lock()
defer test.Unlock()

browsers := Browsers{Browsers: []Browser{
{Name: "browser", DefaultVersion: "1.0", Versions: []Version{
{Number: "1.0", Regions: []Region{
Expand All @@ -310,15 +326,33 @@ func TestProxyVideoFile(t *testing.T) {

sessionID := node.sum() + "123"

rsp, err := doBasicHTTPRequest(http.MethodGet, gridrouter(fmt.Sprintf("/video/%s", sessionID)), nil)
return fileServer, sessionID
}

func TestProxyDownloadWithoutAuth(t *testing.T) {
rsp, err := http.Get(gridrouter("/download/123"))

AssertThat(t, err, Is{nil})
AssertThat(t, rsp, Code{http.StatusUnauthorized})
}

func TestProxyDownload(t *testing.T) {

test.Lock()
defer test.Unlock()

fileServer, sessionID := prepareMockFileServer("/download/123/somefile.txt")
defer fileServer.Close()

rsp, err := doBasicHTTPRequest(http.MethodGet, gridrouter(fmt.Sprintf("/download/%s/somefile.txt", sessionID)), nil)
AssertThat(t, err, Is{nil})
AssertThat(t, rsp, Code{http.StatusOK})

rsp, err = doBasicHTTPRequest(http.MethodGet, gridrouter("/video/missing-file"), nil)
rsp, err = doBasicHTTPRequest(http.MethodGet, gridrouter("/download/missing-file"), nil)
AssertThat(t, err, Is{nil})
AssertThat(t, rsp, Code{http.StatusNotFound})

rsp, err = doBasicHTTPRequest(http.MethodGet, gridrouter("/video/f7fd94f75c79c36e547c091632da440f_missing-file"), nil)
rsp, err = doBasicHTTPRequest(http.MethodGet, gridrouter("/download/f7fd94f75c79c36e547c091632da440f_missing-file"), nil)
AssertThat(t, err, Is{nil})
AssertThat(t, rsp, Code{http.StatusNotFound})
}
Expand Down

0 comments on commit 27217b2

Please sign in to comment.