diff --git a/docs/download.adoc b/docs/download.adoc new file mode 100644 index 0000000..21b9ace --- /dev/null +++ b/docs/download.adoc @@ -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//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//filename.txt + diff --git a/docs/index.adoc b/docs/index.adoc index 7057628..dea808e 100644 --- a/docs/index.adoc +++ b/docs/index.adoc @@ -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] diff --git a/docs/log-files.adoc b/docs/log-files.adoc index cfa66d5..9370ad0 100644 --- a/docs/log-files.adoc +++ b/docs/log-files.adoc @@ -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 @@ -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 diff --git a/proxy.go b/proxy.go index bc1a973..0dedbac 100644 --- a/proxy.go +++ b/proxy.go @@ -36,6 +36,7 @@ const ( proxyPath = routePath + "/" vncPath = "/vnc/" videoPath = "/video/" + downloadPath = "/download/" head = len(proxyPath) md5SumLength = 32 tail = head + md5SumLength @@ -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) } } @@ -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 } diff --git a/proxy_test.go b/proxy_test.go index aa94ec2..4d55048 100644 --- a/proxy_test.go +++ b/proxy_test.go @@ -276,7 +276,7 @@ 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}) @@ -284,20 +284,36 @@ func TestProxyVideoFileWithoutAuth(t *testing.T) { 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{ @@ -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}) }