├── .eslintrc.js ├── .flake8 ├── .github ├── dependabot.yaml └── workflows │ ├── binder-badge.yaml │ ├── release.yaml │ └── test.yaml ├── .gitignore ├── .pre-commit-config.yaml ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── MANIFEST.in ├── README.md ├── RELEASE.md ├── dev-requirements.txt ├── environment.yml ├── js ├── clipboard.css ├── clipboard.js ├── index.css └── index.js ├── jupyter-config ├── jupyter_notebook_config.d │ └── jupyter_remote_desktop_proxy.json └── jupyter_server_config.d │ └── jupyter_remote_desktop_proxy.json ├── jupyter_remote_desktop_proxy ├── __init__.py ├── handlers.py ├── server_extension.py ├── setup_websockify.py ├── share │ └── xstartup ├── static │ ├── clipboard.svg │ └── jupyter-logo.svg └── templates │ └── index.html ├── package.json ├── pyproject.toml ├── setup.py ├── tests ├── conftest.py ├── reference │ ├── desktop-turbovnc.png │ └── desktop.png └── test_browser.py └── webpack.config.js /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | }, 6 | extends: ["eslint:recommended"], 7 | overrides: [ 8 | { 9 | env: { 10 | node: true, 11 | }, 12 | files: [".eslintrc.{js,cjs}"], 13 | parserOptions: { 14 | sourceType: "script", 15 | }, 16 | }, 17 | ], 18 | parserOptions: { 19 | ecmaVersion: "latest", 20 | sourceType: "module", 21 | }, 22 | plugins: [], 23 | rules: { 24 | "no-unused-vars": ["error", { args: "after-used" }], 25 | }, 26 | ignorePatterns: [ 27 | "jupyter_remote_desktop_proxy/static/dist/**", 28 | "webpack.config.js", 29 | ], 30 | }; 31 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | # flake8 is used for linting Python code setup to automatically run with 2 | # pre-commit. 3 | # 4 | # ref: https://flake8.pycqa.org/en/latest/user/configuration.html 5 | # 6 | 7 | [flake8] 8 | # E: style errors 9 | # W: style warnings 10 | # C: complexity 11 | # D: docstring warnings (unused pydocstyle extension) 12 | # F841: local variable assigned but never used 13 | ignore = E, C, W, D, F841 14 | -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | # dependabot.yaml reference: https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 2 | # 3 | # Notes: 4 | # - Status and logs from dependabot are provided at 5 | # https://github.com/jupyterhub/jupyter-remote-desktop-proxy/network/updates. 6 | # - YAML anchors are not supported here or in GitHub Workflows. 7 | # 8 | version: 2 9 | updates: 10 | # Maintain dependencies in our GitHub Workflows 11 | - package-ecosystem: github-actions 12 | directory: / 13 | labels: [ci] 14 | schedule: 15 | interval: monthly 16 | time: "05:00" 17 | timezone: Etc/UTC 18 | 19 | # Bump dockerfile FROM 20 | - package-ecosystem: docker 21 | directory: / 22 | labels: [dependencies] 23 | schedule: 24 | interval: monthly 25 | -------------------------------------------------------------------------------- /.github/workflows/binder-badge.yaml: -------------------------------------------------------------------------------- 1 | #./.github/workflows/binder-badge.yaml 2 | name: Binder Badge 3 | on: 4 | pull_request_target: 5 | 6 | jobs: 7 | badge: 8 | runs-on: ubuntu-22.04 9 | permissions: 10 | pull-requests: write 11 | 12 | steps: 13 | - uses: manics/action-binderbadge@v3.0.0 14 | with: 15 | githubToken: ${{ secrets.GITHUB_TOKEN }} 16 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | # This is a GitHub workflow defining a set of jobs with a set of steps. 2 | # ref: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions 3 | # 4 | name: Release 5 | 6 | # Always tests wheel building, but only publish to PyPI on pushed tags. 7 | on: 8 | pull_request: 9 | paths-ignore: 10 | - "docs/**" 11 | - ".github/workflows/*.yaml" 12 | - "!.github/workflows/release.yaml" 13 | push: 14 | paths-ignore: 15 | - "docs/**" 16 | - ".github/workflows/*.yaml" 17 | - "!.github/workflows/release.yaml" 18 | branches-ignore: 19 | - "dependabot/**" 20 | - "pre-commit-ci-update-config" 21 | tags: ["**"] 22 | workflow_dispatch: 23 | 24 | jobs: 25 | build-release: 26 | runs-on: ubuntu-22.04 27 | permissions: 28 | # id-token=write is required for pypa/gh-action-pypi-publish, and the PyPI 29 | # project needs to be configured to trust this workflow. 30 | # 31 | # ref: https://github.com/jupyterhub/team-compass/issues/648 32 | # 33 | id-token: write 34 | 35 | steps: 36 | - uses: actions/checkout@v4 37 | - uses: actions/setup-python@v5 38 | with: 39 | python-version: "3.11" 40 | 41 | - name: install build package 42 | run: | 43 | pip install --upgrade pip 44 | pip install build 45 | pip freeze 46 | 47 | - name: build release 48 | run: | 49 | python -m build --sdist --wheel . 50 | ls -l dist 51 | 52 | - name: test to see if built js file is in the package 53 | run: | 54 | tar -tvf dist/*.tar.gz | grep dist/viewer.js 55 | unzip -l dist/*.whl | grep dist/viewer.js 56 | 57 | - name: publish to pypi 58 | uses: pypa/gh-action-pypi-publish@release/v1 59 | if: startsWith(github.ref, 'refs/tags/') 60 | 61 | publish-images: 62 | runs-on: ubuntu-22.04 63 | 64 | strategy: 65 | fail-fast: false 66 | matrix: 67 | include: 68 | - vncserver: tigervnc 69 | - vncserver: turbovnc 70 | 71 | steps: 72 | - uses: actions/checkout@v4 73 | 74 | - name: Set up QEMU (for docker buildx) 75 | uses: docker/setup-qemu-action@v3 76 | 77 | - name: Set up Docker Buildx (for multi-arch builds) 78 | uses: docker/setup-buildx-action@v3 79 | 80 | - name: Make decisions on pushing and suffix (based on vnc server chosen) 81 | id: decisions 82 | run: | 83 | if [ "${{ startsWith(github.ref, 'refs/tags/') || (github.ref == 'refs/heads/main') }}" = "true" ]; then 84 | echo "push=true" >> $GITHUB_OUTPUT 85 | else 86 | echo "push=false" >> $GITHUB_OUTPUT 87 | fi 88 | 89 | # We provide image tags with -tigervnc and -turbovnc suffixes to allow 90 | # for an explicit choice, but also ship with a default choice of 91 | # TigerVNC. 92 | if [ "${{ matrix.vncserver == 'tigervnc' }}" == "true" ]; then 93 | echo 'suffix="",-${{ matrix.vncserver }}' >> $GITHUB_OUTPUT 94 | else 95 | echo "suffix=-${{ matrix.vncserver }}" >> $GITHUB_OUTPUT 96 | fi 97 | 98 | # For builds triggered by a git tag 1.2.3, we calculate image tags like: 99 | # [{prefix}:1.2.3, {prefix}:1.2, {prefix}:1, {prefix}:latest] 100 | # 101 | # More details at 102 | # https://github.com/jupyterhub/action-major-minor-tag-calculator. 103 | # 104 | - name: Get image tags 105 | id: tags 106 | uses: jupyterhub/action-major-minor-tag-calculator@v3 107 | with: 108 | githubToken: ${{ secrets.GITHUB_TOKEN }} 109 | prefix: "quay.io/jupyterhub/jupyter-remote-desktop-proxy:" 110 | suffix: ${{ steps.decisions.outputs.suffix }} 111 | branchRegex: ^\w[\w-.]*$ 112 | defaultTag: quay.io/jupyterhub/jupyter-remote-desktop-proxy:noref 113 | 114 | - name: Login to container registry 115 | # Credentials to Quay.io was setup by... 116 | # 1. Creating a [Robot Account](https://quay.io/organization/jupyterhub?tab=robots) 117 | # 2. Giving it push permissions to the image repository 118 | # 3. Adding Robot Account credentials as workflow environment secrets 119 | if: steps.decisions.outputs.push == 'true' 120 | run: | 121 | docker login -u "${{ secrets.QUAY_USERNAME }}" -p "${{ secrets.QUAY_PASSWORD }}" quay.io 122 | 123 | - name: Build and push image 124 | uses: docker/build-push-action@v6 125 | with: 126 | build-args: | 127 | vncserver=${{ matrix.vncserver }} 128 | context: . 129 | platforms: linux/amd64,linux/arm64 130 | push: ${{ steps.decisions.outputs.push }} 131 | tags: ${{ join(fromJson(steps.tags.outputs.tags)) }} 132 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | # This is a GitHub workflow defining a set of jobs with a set of steps. 2 | # ref: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions 3 | # 4 | name: Test 5 | 6 | on: 7 | pull_request: 8 | push: 9 | branches-ignore: 10 | - "dependabot/**" 11 | - "pre-commit-ci-update-config" 12 | tags: ["**"] 13 | workflow_dispatch: 14 | 15 | defaults: 16 | run: 17 | # Both TigerVNC and TurboVNC reports "the input device is not a TTY" if 18 | # started without a TTY. GitHub Actions environments doesn't come with one, 19 | # so this provides one. 20 | # 21 | # ref: https://github.com/actions/runner/issues/241#issuecomment-842566950 22 | # 23 | shell: script --quiet --return --log-out /dev/null --command "bash -e {0}" 24 | 25 | jobs: 26 | image-test: 27 | runs-on: ubuntu-22.04 28 | timeout-minutes: 10 29 | strategy: 30 | fail-fast: false 31 | matrix: 32 | include: 33 | - vncserver: tigervnc 34 | - vncserver: turbovnc 35 | 36 | steps: 37 | - uses: actions/checkout@v4 38 | 39 | - uses: actions/setup-python@v5 40 | with: 41 | python-version: "3.11" 42 | 43 | - name: Cache playwright binaries 44 | uses: actions/cache@v4 45 | with: 46 | path: | 47 | ~/.cache/ms-playwright 48 | key: ${{ runner.os }}-playwright 49 | 50 | - name: Build image 51 | run: | 52 | docker build --progress=plain --build-arg vncserver=${{ matrix.vncserver }} -t test . 53 | 54 | - name: (inside container) vncserver -help 55 | run: | 56 | # -help flag is not available for TurboVNC, but it emits the -help 57 | # equivalent information anyhow if passed -help, but also errors. Due 58 | # to this, we fallback to use the errorcode of vncserver -list. 59 | docker run test bash -c "vncserver -help || vncserver -list > /dev/null" 60 | 61 | - name: Test vncserver 62 | run: | 63 | # TigerVNC needs to be configured with -rfbport -1 to not open a TCP 64 | # port, while TurboVNC doesn't support being passed -1 and won't open 65 | # a TCP port anyhow. 66 | rfbport_arg="-rfbport -1" 67 | if [ "${{ matrix.vncserver }}" == "turbovnc" ]; then rfbport_arg=""; fi 68 | 69 | container_id=$(docker run -d -it test vncserver -xstartup /opt/install/jupyter_remote_desktop_proxy/share/xstartup -verbose -fg -geometry 1680x1050 -SecurityTypes None -rfbunixpath /tmp/vncserver.socket $rfbport_arg) 70 | sleep 1 71 | 72 | echo "::group::Install netcat, a test dependency" 73 | docker exec --user root $container_id bash -c ' 74 | apt update 75 | apt install -y netcat-openbsd 76 | ' 77 | echo "::endgroup::" 78 | 79 | docker exec -it $container_id timeout --preserve-status 1 nc -vU /tmp/vncserver.socket 2>&1 | tee -a /dev/stderr | \ 80 | grep --quiet RFB && echo "Passed test" || { echo "Failed test" && TEST_OK=false; } 81 | 82 | echo "::group::Security - Verify TCP ports wasn't opened" 83 | ports=(5800 5801 5900 5901) 84 | for port in "${ports[@]}" 85 | do 86 | docker exec -it $container_id timeout --preserve-status 1 nc -vz localhost $port | tee -a /dev/stderr | \ 87 | grep --quiet succeeded && { echo "Failed security check - port $port open" && SECURITY_OK=false; } || echo "Passed security check - port $port not opened" 88 | done 89 | echo "::endgroup::" 90 | 91 | echo "::group::vncserver logs" 92 | docker exec $container_id bash -c 'cat ~/.vnc/*.log' 93 | echo "::endgroup::" 94 | 95 | docker stop $container_id > /dev/null 96 | if [ "$TEST_OK" == "false" ]; then 97 | echo "Test failed!" 98 | exit 1 99 | fi 100 | if [ "$SECURITY_OK" == "false" ]; then 101 | echo "Security check failed!" 102 | exit 1 103 | fi 104 | 105 | - name: Install playwright 106 | run: | 107 | python -mpip install -r dev-requirements.txt 108 | python -mplaywright install --with-deps 109 | 110 | - name: Playwright browser test 111 | run: | 112 | container_id=$(docker run -d -it -p 8888:8888 -e JUPYTER_TOKEN=secret test) 113 | sleep 3 114 | export CONTAINER_ID=$container_id 115 | export JUPYTER_HOST=http://localhost:8888 116 | export JUPYTER_TOKEN=secret 117 | export VNCSERVER=${{ matrix.vncserver }} 118 | 119 | python -mpytest -vs 120 | 121 | echo "::group::jupyter_server logs" 122 | docker logs $container_id 123 | echo "::endgroup::" 124 | 125 | echo "::group::vncserver logs" 126 | docker exec $container_id bash -c 'cat ~/.vnc/*.log' 127 | echo "::endgroup::" 128 | 129 | timeout 5 docker stop $container_id > /dev/null && echo "Passed SIGTERM test" || { echo "Failed SIGTERM test" && TEST_OK=false; } 130 | 131 | if [ "$TEST_OK" == "false" ]; then 132 | echo "One or more tests failed!" 133 | exit 1 134 | fi 135 | 136 | - name: Upload screenshot 137 | uses: actions/upload-artifact@v4 138 | if: always() 139 | with: 140 | name: screenshots-${{ matrix.vncserver }} 141 | path: screenshots/* 142 | if-no-files-found: error 143 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Extra ignore patterns specific to this project 2 | # Installed JS libraries 3 | node_modules/ 4 | package-lock.json 5 | # Built JS files 6 | jupyter_remote_desktop_proxy/static/dist 7 | 8 | # Standard python gitignore patterns 9 | # Byte-compiled / optimized / DLL files 10 | __pycache__/ 11 | *.py[cod] 12 | *$py.class 13 | 14 | # C extensions 15 | *.so 16 | 17 | # Distribution / packaging 18 | .Python 19 | build/ 20 | develop-eggs/ 21 | dist/ 22 | downloads/ 23 | eggs/ 24 | .eggs/ 25 | lib/ 26 | lib64/ 27 | parts/ 28 | sdist/ 29 | var/ 30 | wheels/ 31 | pip-wheel-metadata/ 32 | share/python-wheels/ 33 | *.egg-info/ 34 | .installed.cfg 35 | *.egg 36 | MANIFEST 37 | 38 | # PyInstaller 39 | # Usually these files are written by a python script from a template 40 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 41 | *.manifest 42 | *.spec 43 | 44 | # Installer logs 45 | pip-log.txt 46 | pip-delete-this-directory.txt 47 | 48 | # Unit test / coverage reports 49 | htmlcov/ 50 | .tox/ 51 | .nox/ 52 | .coverage 53 | .coverage.* 54 | .cache 55 | nosetests.xml 56 | coverage.xml 57 | *.cover 58 | *.py,cover 59 | .hypothesis/ 60 | .pytest_cache/ 61 | 62 | # Translations 63 | *.mo 64 | *.pot 65 | 66 | # Django stuff: 67 | *.log 68 | local_settings.py 69 | db.sqlite3 70 | db.sqlite3-journal 71 | 72 | # Flask stuff: 73 | instance/ 74 | .webassets-cache 75 | 76 | # Scrapy stuff: 77 | .scrapy 78 | 79 | # Sphinx documentation 80 | docs/_build/ 81 | 82 | # PyBuilder 83 | target/ 84 | 85 | # Jupyter Notebook 86 | .ipynb_checkpoints 87 | 88 | # IPython 89 | profile_default/ 90 | ipython_config.py 91 | 92 | # pyenv 93 | .python-version 94 | 95 | # pipenv 96 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 97 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 98 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 99 | # install all needed dependencies. 100 | #Pipfile.lock 101 | 102 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 103 | __pypackages__/ 104 | 105 | # Celery stuff 106 | celerybeat-schedule 107 | celerybeat.pid 108 | 109 | # SageMath parsed files 110 | *.sage.py 111 | 112 | # Environments 113 | .env 114 | .venv 115 | env/ 116 | venv/ 117 | ENV/ 118 | env.bak/ 119 | venv.bak/ 120 | 121 | # Spyder project settings 122 | .spyderproject 123 | .spyproject 124 | 125 | # Rope project settings 126 | .ropeproject 127 | 128 | # mkdocs documentation 129 | /site 130 | 131 | # mypy 132 | .mypy_cache/ 133 | .dmypy.json 134 | dmypy.json 135 | 136 | # Pyre type checker 137 | .pyre/ 138 | 139 | # Additional ignores 140 | screenshots/ 141 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # pre-commit is a tool to perform a predefined set of tasks manually and/or 2 | # automatically before git commits are made. 3 | # 4 | # Config reference: https://pre-commit.com/#pre-commit-configyaml---top-level 5 | # 6 | # Common tasks 7 | # 8 | # - Run on all files: pre-commit run --all-files 9 | # - Register git hooks: pre-commit install --install-hooks 10 | # 11 | repos: 12 | # Autoformat: Python code, syntax patterns are modernized 13 | - repo: https://github.com/asottile/pyupgrade 14 | rev: v3.19.1 15 | hooks: 16 | - id: pyupgrade 17 | args: 18 | - --py38-plus 19 | 20 | # Autoformat: Python code 21 | - repo: https://github.com/PyCQA/autoflake 22 | rev: v2.3.1 23 | hooks: 24 | - id: autoflake 25 | # args ref: https://github.com/PyCQA/autoflake#advanced-usage 26 | args: 27 | - --in-place 28 | 29 | # Autoformat: Python code 30 | - repo: https://github.com/pycqa/isort 31 | rev: 6.0.1 32 | hooks: 33 | - id: isort 34 | 35 | # Autoformat: Python code 36 | - repo: https://github.com/psf/black 37 | rev: 25.1.0 38 | hooks: 39 | - id: black 40 | 41 | # Autoformat: markdown, yaml 42 | - repo: https://github.com/pre-commit/mirrors-prettier 43 | rev: v4.0.0-alpha.8 44 | hooks: 45 | - id: prettier 46 | 47 | # Misc... 48 | - repo: https://github.com/pre-commit/pre-commit-hooks 49 | rev: v5.0.0 50 | # ref: https://github.com/pre-commit/pre-commit-hooks#hooks-available 51 | hooks: 52 | # Autoformat: Makes sure files end in a newline and only a newline. 53 | - id: end-of-file-fixer 54 | 55 | # Autoformat: Sorts entries in requirements.txt. 56 | - id: requirements-txt-fixer 57 | 58 | # Lint: Check for files with names that would conflict on a 59 | # case-insensitive filesystem like MacOS HFS+ or Windows FAT. 60 | - id: check-case-conflict 61 | 62 | # Lint: Checks that non-binary executables have a proper shebang. 63 | - id: check-executables-have-shebangs 64 | 65 | # Lint: Python code 66 | - repo: https://github.com/PyCQA/flake8 67 | rev: "7.1.2" 68 | hooks: 69 | - id: flake8 70 | 71 | # Lint: JS code 72 | - repo: https://github.com/pre-commit/mirrors-eslint 73 | rev: "v8.56.0" 74 | hooks: 75 | - id: eslint 76 | files: \.jsx?$ 77 | types: [file] 78 | exclude: jupyter_remote_desktop_proxy/static/dist 79 | 80 | # pre-commit.ci config reference: https://pre-commit.ci/#configuration 81 | ci: 82 | autoupdate_schedule: monthly 83 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v3.0 4 | 5 | ### v3.0.1 - 2025-04-11 6 | 7 | This is a security release for [GHSA-vrq4-9hc3-cgp7] impacting users of this 8 | project together with TigerVNC. 9 | 10 | [ghsa-vrq4-9hc3-cgp7]: https://github.com/jupyterhub/jupyter-remote-desktop-proxy/security/advisories/GHSA-vrq4-9hc3-cgp7 11 | 12 | #### Bugs fixed 13 | 14 | - Ensure TigerVNC isn't accessible via the network [#151](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/pull/151) ([@consideRatio](https://github.com/consideRatio), [@minrk](https://github.com/minrk)) 15 | 16 | #### Other merged PRs 17 | 18 | This changelog entry omits automated PRs, for example those updating 19 | dependencies in: images, github actions, pre-commit hooks. For a full list of 20 | changes, see the [full comparison](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/compare/v2.0.1...v3.0.0). 21 | 22 | ### v3.0.0 - 2025-03-22 23 | 24 | VNC servers' accessed via this project must be accessed via a unix 25 | socket instead of a TCP port. Previously only TigerVNC was accessed via a unix 26 | socket. This change means jupyter-remote-desktop-proxy can be used in shared 27 | environments as long as the file permissions on the unix socket restrict access 28 | to a single user. TCP connections to the VNC server are no longer supported. 29 | 30 | #### Breaking changes 31 | 32 | - TurboVNC version 3.1 or higher is now required ([#145](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/pull/145)). 33 | - VNC servers needs to support the `-rfbunixpath` flag (TigerVNC and TurboVNC does) ([#145](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/pull/145)). 34 | - This project no longer relies on `websockify` ([#119](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/pull/119)). 35 | 36 | #### New features added 37 | 38 | - Override xstartup with environment variable `JUPYTER_REMOTE_DESKTOP_PROXY_XSTARTUP` [#134](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/pull/134) ([@manics](https://github.com/manics), [@consideRatio](https://github.com/consideRatio)) 39 | 40 | #### Enhancements made 41 | 42 | - Access TurboVNC via a unix socket instead of a port [#145](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/pull/145) ([@consideRatio](https://github.com/consideRatio), [@manics](https://github.com/manics)) 43 | - Resize desktop to window, instead of scaling [#124](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/pull/124) ([@manics](https://github.com/manics), [@yuvipanda](https://github.com/yuvipanda)) 44 | - Close clipboard if clicked outside [#122](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/pull/122) ([@manics](https://github.com/manics), [@yuvipanda](https://github.com/yuvipanda)) 45 | - Remove websockify, add Playwright test [#119](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/pull/119) ([@manics](https://github.com/manics), [@consideRatio](https://github.com/consideRatio)) 46 | 47 | #### Bugs fixed 48 | 49 | - Build and package JS built with webpack --mode production [#147](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/pull/147) ([@consideRatio](https://github.com/consideRatio), [@manics](https://github.com/manics)) 50 | 51 | #### Maintenance and upkeep improvements 52 | 53 | - Switch to date tag for base image, install nodejs [#136](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/pull/136) ([@manics](https://github.com/manics), [@consideRatio](https://github.com/consideRatio)) 54 | - Remove tigervnc-xorg-extension [#132](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/pull/132) ([@manics](https://github.com/manics), [@consideRatio](https://github.com/consideRatio)) 55 | - Rename tooltip to clipboard [#121](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/pull/121) ([@manics](https://github.com/manics), [@consideRatio](https://github.com/consideRatio)) 56 | 57 | #### Dependency updates 58 | 59 | - Pin js dependency novnc to 1.5.0 until we support 1.6.0 [#142](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/pull/142) ([@consideRatio](https://github.com/consideRatio)) 60 | - Update to use novnc 1.5.0 [#123](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/pull/123) ([@manics](https://github.com/manics), [@yuvipanda](https://github.com/yuvipanda)) 61 | - Pin @novnc/novnc 1.4.x pending 1.5.x compatibility [#120](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/pull/120) ([@manics](https://github.com/manics), [@consideRatio](https://github.com/consideRatio)) 62 | 63 | #### Continuous integration improvements 64 | 65 | - Pin dockerfile SHA, bump monthly with dependabot [#130](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/pull/130) ([@manics](https://github.com/manics), [@consideRatio](https://github.com/consideRatio)) 66 | - Install netcat-openbsd in tests and update screenshots for ubuntu 24.04 [#129](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/pull/129) ([@manics](https://github.com/manics), [@consideRatio](https://github.com/consideRatio)) 67 | 68 | #### Other merged PRs 69 | 70 | This changelog entry omits automated PRs, for example those updating 71 | dependencies in: images, github actions, pre-commit hooks. For a full list of 72 | changes, see the [full comparison](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/compare/v2.0.1...v3.0.0). 73 | 74 | #### Contributors to this release 75 | 76 | The following people contributed discussions, new ideas, code and documentation contributions, and review. 77 | See [our definition of contributors](https://github-activity.readthedocs.io/en/latest/#how-does-this-tool-define-contributions-in-the-reports). 78 | 79 | ([GitHub contributors page for this release](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/graphs/contributors?from=2024-06-13&to=2025-03-19&type=c)) 80 | 81 | @benz0li ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyter-remote-desktop-proxy+involves%3Abenz0li+updated%3A2024-06-13..2025-03-19&type=Issues)) | @consideRatio ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyter-remote-desktop-proxy+involves%3AconsideRatio+updated%3A2024-06-13..2025-03-19&type=Issues)) | @manics ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyter-remote-desktop-proxy+involves%3Amanics+updated%3A2024-06-13..2025-03-19&type=Issues)) | @yuvipanda ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyter-remote-desktop-proxy+involves%3Ayuvipanda+updated%3A2024-06-13..2025-03-19&type=Issues)) 82 | 83 | ## v2.0 84 | 85 | ### v2.0.1 86 | 87 | ([full changelog](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/compare/v2.0.0...v2.0.1)) 88 | 89 | #### Bugs fixed 90 | 91 | - Retry a few times when the initial connection fails [#112](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/pull/112) ([@sunu](https://github.com/sunu), [@yuvipanda](https://github.com/yuvipanda)) 92 | 93 | #### Other merged PRs 94 | 95 | - [pre-commit.ci] pre-commit autoupdate [#111](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/pull/111) ([@consideRatio](https://github.com/consideRatio)) 96 | 97 | #### Contributors to this release 98 | 99 | The following people contributed discussions, new ideas, code and documentation contributions, and review. 100 | See [our definition of contributors](https://github-activity.readthedocs.io/en/latest/#how-does-this-tool-define-contributions-in-the-reports). 101 | 102 | ([GitHub contributors page for this release](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/graphs/contributors?from=2024-04-02&to=2024-06-13&type=c)) 103 | 104 | @consideRatio ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyter-remote-desktop-proxy+involves%3AconsideRatio+updated%3A2024-04-02..2024-06-13&type=Issues)) | @sunu ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyter-remote-desktop-proxy+involves%3Asunu+updated%3A2024-04-02..2024-06-13&type=Issues)) | @yuvipanda ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyter-remote-desktop-proxy+involves%3Ayuvipanda+updated%3A2024-04-02..2024-06-13&type=Issues)) 105 | 106 | ### v2.0.0 - 2024-04-02 107 | 108 | This release removes a bundled VNC server, use of `jupyter-remote-desktop-proxy` 109 | requires both `websockify` and a VNC server - TigerVNC and TurboVNC are 110 | officially supported. For tested examples on how to install `websockify` and 111 | officially supported VNC servers, see [this project's Dockerfile]. 112 | 113 | This project now publishes basic but tested images built on 114 | [quay.io/jupyter/base-notebook] from the [jupyter/docker-stacks] to 115 | [quay.io/jupyterhub/jupyter-remote-desktop-proxy]. Their purpose is currently 116 | not scoped beyond use for testing and providing an example on how to install 117 | officially supported VNC servers. 118 | 119 | The Ctrl-Alt-Delete button is currently removed, but intended to be added back. 120 | This is tracked by [this GitHub issue]. 121 | 122 | ([full changelog](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/compare/v1.2.1...v2.0.0)) 123 | 124 | [this project's Dockerfile]: https://github.com/jupyterhub/jupyter-remote-desktop-proxy/blob/main/Dockerfile 125 | [quay.io/jupyter/base-notebook]: https://quay.io/repository/jupyter/base-notebook?tab=tags 126 | [quay.io/jupyterhub/jupyter-remote-desktop-proxy]: https://quay.io/repository/jupyterhub/jupyter-remote-desktop-proxy?tab=tags 127 | [jupyter/docker-stacks]: https://github.com/jupyter/docker-stacks 128 | [this GitHub issue]: https://github.com/jupyterhub/jupyter-remote-desktop-proxy/issues/83 129 | 130 | #### Breaking Changes 131 | 132 | - Require jupyter-server-proxy 4+ [#91](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/pull/91) ([@consideRatio](https://github.com/consideRatio), [@yuvipanda](https://github.com/yuvipanda)) 133 | - Require python 3.8+, up from 3.6+ [#90](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/pull/90) ([@consideRatio](https://github.com/consideRatio), [@manics](https://github.com/manics)) 134 | - Remove bundled VNC server [#84](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/pull/84) ([@manics](https://github.com/manics), [@consideRatio](https://github.com/consideRatio), [@yuvipanda](https://github.com/yuvipanda)) 135 | 136 | #### New features added 137 | 138 | - Publish TigerVNC and TurboVNC image to quay.io [#94](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/pull/94) ([@consideRatio](https://github.com/consideRatio), [@yuvipanda](https://github.com/yuvipanda)) 139 | 140 | #### Enhancements made 141 | 142 | - Add a "Hub Control Panel" menu item if running inside a JupyterHub [#79](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/pull/79) ([@yuvipanda](https://github.com/yuvipanda), [@manics](https://github.com/manics), [@unode](https://github.com/unode)) 143 | - Cleanup the UI to be much nicer [#78](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/pull/78) ([@yuvipanda](https://github.com/yuvipanda), [@manics](https://github.com/manics)) 144 | 145 | #### Bugs fixed 146 | 147 | - MANIFEST.in: Include templates/ directory [#103](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/pull/103) ([@zmcgrew](https://github.com/zmcgrew), [@consideRatio](https://github.com/consideRatio)) 148 | - Fix failure to specify port for TurboVNC server [#99](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/pull/99) ([@consideRatio](https://github.com/consideRatio)) 149 | - Fix TigerVNC detection for non-apt installations [#96](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/pull/96) ([@consideRatio](https://github.com/consideRatio), [@yuvipanda](https://github.com/yuvipanda), [@goekce](https://github.com/goekce)) 150 | - [Docker image] Install fonts-dejavu for use by terminals [#86](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/pull/86) ([@yuvipanda](https://github.com/yuvipanda), [@manics](https://github.com/manics), [@consideRatio](https://github.com/consideRatio)) 151 | - Remove xfce4-screensaver [#76](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/pull/76) ([@manics](https://github.com/manics), [@yuvipanda](https://github.com/yuvipanda)) 152 | - Fix container build [#70](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/pull/70) ([@manics](https://github.com/manics), [@yuvipanda](https://github.com/yuvipanda), [@consideRatio](https://github.com/consideRatio)) 153 | 154 | #### Maintenance and upkeep improvements 155 | 156 | - Fail early on missing websockify executable [#107](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/pull/107) ([@consideRatio](https://github.com/consideRatio)) 157 | - refactor: small readability and consistency details [#104](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/pull/104) ([@consideRatio](https://github.com/consideRatio)) 158 | - Bump dependency requirement a patch version [#102](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/pull/102) ([@consideRatio](https://github.com/consideRatio)) 159 | - Fix image tests: vncserver, websockify, jupyter-remote-desktop-proxy [#101](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/pull/101) ([@consideRatio](https://github.com/consideRatio), [@yuvipanda](https://github.com/yuvipanda)) 160 | - Fix automation to publish tigervnc and turbovnc images [#95](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/pull/95) ([@consideRatio](https://github.com/consideRatio)) 161 | - Require jupyter-server-proxy 4+ [#91](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/pull/91) ([@consideRatio](https://github.com/consideRatio), [@yuvipanda](https://github.com/yuvipanda)) 162 | - Require python 3.8+, up from 3.6+ [#90](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/pull/90) ([@consideRatio](https://github.com/consideRatio), [@manics](https://github.com/manics)) 163 | - Remove bundled VNC server [#84](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/pull/84) ([@manics](https://github.com/manics), [@consideRatio](https://github.com/consideRatio), [@yuvipanda](https://github.com/yuvipanda)) 164 | - Stop vendoring noVNC [#77](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/pull/77) ([@yuvipanda](https://github.com/yuvipanda), [@manics](https://github.com/manics), [@consideRatio](https://github.com/consideRatio)) 165 | 166 | #### Documentation improvements 167 | 168 | - Fix typo in README.md [#72](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/pull/72) ([@nthiery](https://github.com/nthiery), [@yuvipanda](https://github.com/yuvipanda)) 169 | 170 | #### Contributors to this release 171 | 172 | The following people contributed discussions, new ideas, code and documentation contributions, and review. 173 | See [our definition of contributors](https://github-activity.readthedocs.io/en/latest/#how-does-this-tool-define-contributions-in-the-reports). 174 | 175 | ([GitHub contributors page for this release](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/graphs/contributors?from=2023-09-27&to=2024-04-02&type=c)) 176 | 177 | @benz0li ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyter-remote-desktop-proxy+involves%3Abenz0li+updated%3A2023-09-27..2024-04-02&type=Issues)) | @consideRatio ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyter-remote-desktop-proxy+involves%3AconsideRatio+updated%3A2023-09-27..2024-04-02&type=Issues)) | @goekce ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyter-remote-desktop-proxy+involves%3Agoekce+updated%3A2023-09-27..2024-04-02&type=Issues)) | @manics ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyter-remote-desktop-proxy+involves%3Amanics+updated%3A2023-09-27..2024-04-02&type=Issues)) | @nthiery ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyter-remote-desktop-proxy+involves%3Anthiery+updated%3A2023-09-27..2024-04-02&type=Issues)) | @unode ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyter-remote-desktop-proxy+involves%3Aunode+updated%3A2023-09-27..2024-04-02&type=Issues)) | @yuvipanda ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyter-remote-desktop-proxy+involves%3Ayuvipanda+updated%3A2023-09-27..2024-04-02&type=Issues)) | @zmcgrew ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyter-remote-desktop-proxy+involves%3Azmcgrew+updated%3A2023-09-27..2024-04-02&type=Issues)) 178 | 179 | ## v1.2 180 | 181 | ### v1.2.1 - 2023-09-27 182 | 183 | ([full changelog](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/compare/v1.2.0...v1.2.1)) 184 | 185 | #### Bugs fixed 186 | 187 | - Revert "Simplify xtartup command" [#64](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/pull/64) ([@yuvipanda](https://github.com/yuvipanda), [@consideRatio](https://github.com/consideRatio)) 188 | 189 | ### v1.2.0 - 2023-09-25 190 | 191 | ([full changelog](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/compare/v1.1.0...v1.2.0)) 192 | 193 | #### New features added 194 | 195 | - Let user defines its own xstartup and geometry via ~/.vnc/xstartup [#35](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/pull/35) ([@cmd-ntrf](https://github.com/cmd-ntrf), [@yuvipanda](https://github.com/yuvipanda), [@consideRatio](https://github.com/consideRatio)) 196 | 197 | #### Bugs fixed 198 | 199 | - Fix module 'posixpath' has no attribute 'expand' [#61](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/pull/61) ([@cmd-ntrf](https://github.com/cmd-ntrf), [@consideRatio](https://github.com/consideRatio)) 200 | 201 | #### Maintenance and upkeep improvements 202 | 203 | - Simplify xtartup command [#59](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/pull/59) ([@yuvipanda](https://github.com/yuvipanda), [@consideRatio](https://github.com/consideRatio)) 204 | - Simplify developmental dockerfile [#58](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/pull/58) ([@yuvipanda](https://github.com/yuvipanda), [@consideRatio](https://github.com/consideRatio)) 205 | 206 | #### Documentation improvements 207 | 208 | - Document needing seccomp=unconfined [#53](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/pull/53) ([@yuvipanda](https://github.com/yuvipanda), [@consideRatio](https://github.com/consideRatio)) 209 | 210 | #### Contributors to this release 211 | 212 | The following people contributed discussions, new ideas, code and documentation contributions, and review. 213 | See [our definition of contributors](https://github-activity.readthedocs.io/en/latest/#how-does-this-tool-define-contributions-in-the-reports). 214 | 215 | ([GitHub contributors page for this release](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/graphs/contributors?from=2023-07-19&to=2023-09-25&type=c)) 216 | 217 | @benz0li ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyter-remote-desktop-proxy+involves%3Abenz0li+updated%3A2023-07-19..2023-09-25&type=Issues)) | @cmd-ntrf ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyter-remote-desktop-proxy+involves%3Acmd-ntrf+updated%3A2023-07-19..2023-09-25&type=Issues)) | @consideRatio ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyter-remote-desktop-proxy+involves%3AconsideRatio+updated%3A2023-07-19..2023-09-25&type=Issues)) | @domna ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyter-remote-desktop-proxy+involves%3Adomna+updated%3A2023-07-19..2023-09-25&type=Issues)) | @yuvipanda ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyter-remote-desktop-proxy+involves%3Ayuvipanda+updated%3A2023-07-19..2023-09-25&type=Issues)) 218 | 219 | ## v1.1 220 | 221 | ### v1.1.0 - 2023-07-18 222 | 223 | ([full changelog](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/compare/v1.0...v1.1.0)) 224 | 225 | #### Enhancements made 226 | 227 | - Add logic to determine if vncserver is TigerVNC [#32](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/pull/32) ([@cmd-ntrf](https://github.com/cmd-ntrf)) 228 | 229 | #### Bugs fixed 230 | 231 | - Fix path when using bundled tigervnc [#44](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/pull/44) ([@pnasrat](https://github.com/pnasrat)) 232 | - Remove hardcoded display number and port, avoids multi-user conflicts [#34](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/pull/34) ([@cmd-ntrf](https://github.com/cmd-ntrf)) 233 | 234 | #### Maintenance and upkeep improvements 235 | 236 | - Add RELEASE.md, adopt tbump, rename release workflow for consistency [#38](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/pull/38) ([@consideRatio](https://github.com/consideRatio)) 237 | - Remove "/usr/bin" prefix in front of dbus-launch in xstartup [#33](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/pull/33) ([@cmd-ntrf](https://github.com/cmd-ntrf)) 238 | - Add logic to determine if vncserver is TigerVNC [#32](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/pull/32) ([@cmd-ntrf](https://github.com/cmd-ntrf)) 239 | - dependabot: monthly updates of github actions [#30](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/pull/30) ([@consideRatio](https://github.com/consideRatio)) 240 | - maint: add pre-commit config [#25](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/pull/25) ([@consideRatio](https://github.com/consideRatio)) 241 | - Quieten binder-badge bot [#3](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/pull/3) ([@manics](https://github.com/manics)) 242 | 243 | #### Documentation improvements 244 | 245 | - Add PyPI/Issues/Forum badges for readme [#40](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/pull/40) ([@consideRatio](https://github.com/consideRatio)) 246 | - Backfill changelog for 1.0.0 and 0.1.3 [#37](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/pull/37) ([@consideRatio](https://github.com/consideRatio)) 247 | 248 | #### Continuous integration improvements 249 | 250 | - Fix permissions required for trusted workflow [#48](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/pull/48) ([@yuvipanda](https://github.com/yuvipanda)) 251 | - Use trusted publishing to push to PyPI [#46](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/pull/46) ([@yuvipanda](https://github.com/yuvipanda)) 252 | - ci: fix typo in manics/action-binderbadge version [#39](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/pull/39) ([@consideRatio](https://github.com/consideRatio)) 253 | 254 | #### Contributors to this release 255 | 256 | ([GitHub contributors page for this release](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/graphs/contributors?from=2023-01-19&to=2023-07-18&type=c)) 257 | 258 | [@cmd-ntrf](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyter-remote-desktop-proxy+involves%3Acmd-ntrf+updated%3A2023-01-19..2023-07-18&type=Issues) | [@consideRatio](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyter-remote-desktop-proxy+involves%3AconsideRatio+updated%3A2023-01-19..2023-07-18&type=Issues) | | [@manics](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyter-remote-desktop-proxy+involves%3Amanics+updated%3A2023-01-19..2023-07-18&type=Issues) | [@pnasrat](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyter-remote-desktop-proxy+involves%3Apnasrat+updated%3A2023-01-19..2023-07-18&type=Issues) | [@yuvipanda](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyter-remote-desktop-proxy+involves%3Ayuvipanda+updated%3A2023-01-19..2023-07-18&type=Issues) 259 | 260 | ## v1.0 261 | 262 | ### v1.0.0 - 2023-01-19 263 | 264 | With this release, the project has relocated from `jupyter-desktop-server` to 265 | `jupyter-remote-desktop-proxy` and relocated from 266 | [yuvipanda/jupyter-desktop-server](https://github.com/yuvipanda/jupyter-desktop-server) to 267 | [jupyterhub/jupyter-remote-desktop-proxy](https://github.com/jupyterhub/jupyter-remote-desktop-proxy). 268 | 269 | #### New features added 270 | 271 | - Add a shared clipboard [#10](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/pull/10) ([@manics](https://github.com/manics), [@yuvipanda](https://github.com/yuvipanda), [@satra](https://github.com/satra), [@fperez](https://github.com/fperez)) 272 | 273 | #### Enhancements made 274 | 275 | - Use TurboVNC if installed [#29 (in previous github repo)](https://github.com/yuvipanda/jupyter-desktop-server/pull/29) ([@manics](https://github.com/manics), [@yuvipanda](https://github.com/yuvipanda), [@cslocum](https://github.com/cslocum)) 276 | 277 | #### Maintenance and upkeep improvements 278 | 279 | - maint: add dependabot for github actions [#22](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/pull/22) ([@consideRatio](https://github.com/consideRatio), [@yuvipanda](https://github.com/yuvipanda)) 280 | - Complete rename of the project - from jupyter_desktop/jupyter-desktop-server to jupyter_remote_desktop_proxy [#20](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/pull/20) ([@yuvipanda](https://github.com/yuvipanda), [@consideRatio](https://github.com/consideRatio)) 281 | - Remove apt.txt and refactor Dockerfile [#13](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/pull/13) ([@consideRatio](https://github.com/consideRatio), [@yuvipanda](https://github.com/yuvipanda), [@manics](https://github.com/manics)) 282 | - Update TurboVNC to 2.2.6 [#11](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/pull/11) ([@manics](https://github.com/manics), [@consideRatio](https://github.com/consideRatio)) 283 | - Update noVNC to 1.2.0 [#6](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/pull/6) ([@manics](https://github.com/manics)) 284 | - Add setup.py metadata [#5](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/pull/5) ([@manics](https://github.com/manics), [@yuvipanda](https://github.com/yuvipanda)) 285 | - Add pypi publish workflow [#4](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/pull/4) ([@manics](https://github.com/manics)) 286 | - Rename repo jupyter-desktop-server ➡️ jupyter-remote-desktop-proxy [#2](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/pull/2) ([@manics](https://github.com/manics), [@choldgraf](https://github.com/choldgraf)) 287 | - Fix permissions on ~/.cache [#22 (in previous github repo)](https://github.com/yuvipanda/jupyter-desktop-server/pull/22) ([@manics](https://github.com/manics), [@yuvipanda](https://github.com/yuvipanda), [@djangoliv](https://github.com/djangoliv), [@nthiery](https://github.com/nthiery)) 288 | - Use conda-forge/websockify, use environment.yml in Dockerfile [#21 (in previous github repo)](https://github.com/yuvipanda/jupyter-desktop-server/pull/21) ([@manics](https://github.com/manics), [@yuvipanda](https://github.com/yuvipanda)) 289 | 290 | #### Documentation improvements 291 | 292 | - Fix typo [#24](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/pull/24) ([@yuvipanda](https://github.com/yuvipanda), [@consideRatio](https://github.com/consideRatio)) 293 | - Add installation instructions [#21](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/pull/21) ([@yuvipanda](https://github.com/yuvipanda), [@consideRatio](https://github.com/consideRatio)) 294 | - Add a section on limitations - OpenGL is currently not supported [#19](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/pull/19) ([@yuvipanda](https://github.com/yuvipanda), [@consideRatio](https://github.com/consideRatio)) 295 | 296 | #### Continuous integration improvements 297 | 298 | - ci: update outdated github actions [#23](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/pull/23) ([@consideRatio](https://github.com/consideRatio), [@yuvipanda](https://github.com/yuvipanda)) 299 | - binder-badge workflow needs permissions.pull-requests:write [#9](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/pull/9) ([@manics](https://github.com/manics)) 300 | - binder-badge workflow needs permissions.issues:write [#8](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/pull/8) ([@manics](https://github.com/manics)) 301 | - Add binder-badge.yaml [#23 (in previous github repo)](https://github.com/yuvipanda/jupyter-desktop-server/pull/23) ([@manics](https://github.com/manics), [@yuvipanda](https://github.com/yuvipanda)) 302 | 303 | ## v0.1 304 | 305 | ### v0.1.3 - 2020-07-07 306 | 307 | - add Dockerfile and adjust readme to reflect a quick start [#19 (in previous github repo)](https://github.com/yuvipanda/jupyter-desktop-server/pull/19) ([@kniec](https://github.com/kniec), [@yuvipanda](https://github.com/yuvipanda)) 308 | - Start session from $HOME [#17 (in previous github repo)](https://github.com/yuvipanda/jupyter-desktop-server/pull/17) ([@mjuric](https://github.com/mjuric), [@yuvipanda](https://github.com/yuvipanda)) 309 | - add Dockerfile and adjust readme to reflect a quick start [#19 (in previous github repo)](https://github.com/yuvipanda/jupyter-desktop-server/pull/19) ([@kniec](https://github.com/kniec), [@yuvipanda](https://github.com/yuvipanda)) 310 | - Support jupyter-server-proxy >= 1.4.0 [daecbdb](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/commit/daecbdb) ([@yuvipanda](https://github.com/yuvipanda)) 311 | - Revert "Add a jupyter server extension to render desktop/" [b6dee24](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/commit/b6dee24) ([@yuvipanda](https://github.com/yuvipanda)) 312 | - Add a jupyter server extension to render desktop/ [18d7fb7](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/commit/18d7fb7) ([@yuvipanda](https://github.com/yuvipanda)) 313 | - Fix README to point to new name [bda9a7e](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/commit/bda9a7e) ([@yuvipanda](https://github.com/yuvipanda)) 314 | - Explicitly specify version of jupyter-server-proxy needed [98b7723](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/commit/98b7723) ([@yuvipanda](https://github.com/yuvipanda)) 315 | - Set CWD of desktop environment to CWD of notebook [360f9b0](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/commit/360f9b0) ([@yuvipanda](https://github.com/yuvipanda)) 316 | 317 | ### v0.1.2 - 2019-11-12 318 | 319 | - Fix cross-origin issue in Safari (#9, thanks to @eslavich) 320 | 321 | ### v0.1.1 - 2019-11-06 322 | 323 | - Increase default resolution to 1680x1050. The wider screen 324 | matches how many user displays are, and there do not seem to 325 | be lag issues. 326 | 327 | ### v0.1 - 2019-11-01 328 | 329 | - Initial release 330 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM quay.io/jupyter/base-notebook:2025-05-30 2 | 3 | USER root 4 | 5 | RUN apt-get -y -qq update \ 6 | && apt-get -y -qq install \ 7 | dbus-x11 \ 8 | # xclip is added as jupyter-remote-desktop-proxy's tests requires it 9 | xclip \ 10 | xfce4 \ 11 | xfce4-panel \ 12 | xfce4-session \ 13 | xfce4-settings \ 14 | xorg \ 15 | xubuntu-icon-theme \ 16 | fonts-dejavu \ 17 | # Disable the automatic screenlock since the account password is unknown 18 | && apt-get -y -qq remove xfce4-screensaver \ 19 | # chown $HOME to workaround that the xorg installation creates a 20 | # /home/jovyan/.cache directory owned by root 21 | # Create /opt/install to ensure it's writable by pip 22 | && mkdir -p /opt/install \ 23 | && chown -R $NB_UID:$NB_GID $HOME /opt/install \ 24 | && rm -rf /var/lib/apt/lists/* 25 | 26 | # Install a VNC server, either TigerVNC (default) or TurboVNC 27 | ARG vncserver=tigervnc 28 | RUN if [ "${vncserver}" = "tigervnc" ]; then \ 29 | echo "Installing TigerVNC"; \ 30 | apt-get -y -qq update; \ 31 | apt-get -y -qq install \ 32 | tigervnc-standalone-server \ 33 | ; \ 34 | rm -rf /var/lib/apt/lists/*; \ 35 | fi 36 | ENV PATH=/opt/TurboVNC/bin:$PATH 37 | RUN if [ "${vncserver}" = "turbovnc" ]; then \ 38 | echo "Installing TurboVNC"; \ 39 | # Install instructions from https://turbovnc.org/Downloads/YUM 40 | wget -q -O- https://packagecloud.io/dcommander/turbovnc/gpgkey | \ 41 | gpg --dearmor >/etc/apt/trusted.gpg.d/TurboVNC.gpg; \ 42 | wget -O /etc/apt/sources.list.d/TurboVNC.list https://raw.githubusercontent.com/TurboVNC/repo/main/TurboVNC.list; \ 43 | apt-get -y -qq update; \ 44 | apt-get -y -qq install \ 45 | turbovnc \ 46 | ; \ 47 | rm -rf /var/lib/apt/lists/*; \ 48 | fi 49 | 50 | USER $NB_USER 51 | 52 | # Install the environment first, and then install the package separately for faster rebuilds 53 | COPY --chown=$NB_UID:$NB_GID environment.yml /tmp 54 | RUN . /opt/conda/bin/activate && \ 55 | mamba env update --quiet --file /tmp/environment.yml 56 | 57 | COPY --chown=$NB_UID:$NB_GID . /opt/install 58 | RUN . /opt/conda/bin/activate && \ 59 | mamba install -y -q "nodejs>=22" && \ 60 | pip install /opt/install 61 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2019, University of Dundee 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | graft jupyter_remote_desktop_proxy/share 2 | graft jupyter_remote_desktop_proxy/static 3 | graft jupyter_remote_desktop_proxy/templates 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Jupyter Remote Desktop Proxy 2 | 3 | [](https://mybinder.org/v2/gh/jupyterhub/jupyter-remote-desktop-proxy/HEAD?urlpath=desktop) 4 | [](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/actions/workflows/test.yaml) 5 | [](https://pypi.python.org/pypi/jupyter-remote-desktop-proxy) 6 | [](https://github.com/jupyterhub/jupyter-remote-desktop-proxy/issues) 7 | [](https://discourse.jupyter.org/c/jupyterhub) 8 | 9 | Run XFCE (or other desktop environments) on Jupyter. 10 | 11 | This is based on https://github.com/ryanlovett/nbnovnc. 12 | 13 | When this extension is launched it will run a Linux desktop on the Jupyter single-user server, and proxy it to your browser using VNC via Jupyter. 14 | 15 |  16 | 17 | ## VNC Server 18 | 19 | This extension requires installing a [VNC server] on the system (likely in a 20 | container image). Compatibility with the latest version of [TigerVNC] and 21 | [TurboVNC] is maintained and verified with tests, but other VNC servers 22 | available in `$PATH` as `vncserver` could also work. The `vncserver` binary 23 | needs to accept the [flags seen passed here], such as `-rfbunixpath` and a few 24 | others. 25 | 26 | For an example, see the [`Dockerfile`](./Dockerfile) in this repository which 27 | installs either TigerVNC or TurboVNC together with XFCE4. 28 | 29 | [vnc server]: https://en.wikipedia.org/wiki/Virtual_Network_Computing 30 | [tigervnc]: https://tigervnc.org/ 31 | [turbovnc]: https://www.turbovnc.org/ 32 | [flags seen passed here]: https://github.com/jupyterhub/jupyter-remote-desktop-proxy/blob/main/jupyter_remote_desktop_proxy/setup_websockify.py 33 | [xfce4]: https://www.xfce.org/ 34 | 35 | ## Installation 36 | 37 | 1. Install this package itself, with `pip` from `PyPI`: 38 | 39 | ```bash 40 | pip install jupyter-remote-desktop-proxy 41 | ``` 42 | 43 | 2. Install the packages needed to provide a VNC server and the actual Linux Desktop environment. 44 | You need to pick a desktop environment (there are many!) - here are the packages 45 | to use TigerVNC and the light-weight [XFCE4] desktop environment on Ubuntu 24.04: 46 | 47 | ``` 48 | dbus-x11 49 | xfce4 50 | xfce4-panel 51 | xfce4-session 52 | xfce4-settings 53 | xorg 54 | xubuntu-icon-theme 55 | tigervnc-standalone-server 56 | ``` 57 | 58 | The recommended way to install these is from your Linux system package manager 59 | of choice (such as apt). 60 | 61 | ## Docker 62 | 63 | To spin up such a notebook first build the container: 64 | 65 | ```bash 66 | $ docker build -t $(whoami)/$(basename ${PWD}) . 67 | ``` 68 | 69 | Now you can run the image: 70 | 71 | ```bash 72 | $ docker run --rm --security-opt seccomp=unconfined -p 8888:8888 $(whoami)/$(basename ${PWD}) 73 | Executing the command: jupyter notebook 74 | [I 12:43:59.148 NotebookApp] Writing notebook server cookie secret to /home/jovyan/.local/share/jupyter/runtime/notebook_cookie_secret 75 | [I 12:44:00.221 NotebookApp] JupyterLab extension loaded from /opt/conda/lib/python3.7/site-packages/jupyterlab 76 | [I 12:44:00.221 NotebookApp] JupyterLab application directory is /opt/conda/share/jupyter/lab 77 | [I 12:44:00.224 NotebookApp] Serving notebooks from local directory: /home/jovyan 78 | [I 12:44:00.225 NotebookApp] The Jupyter Notebook is running at: 79 | [I 12:44:00.225 NotebookApp] http://924904e0a646:8888/?token=40475e553b7671b9e93533b97afe584fa2030448505a7d83 80 | [I 12:44:00.225 NotebookApp] or http://127.0.0.1:8888/?token=40475e553b7671b9e93533b97afe584fa2030448505a7d83 81 | [I 12:44:00.225 NotebookApp] Use Control-C to stop this server and shut down all kernels (twice to skip confirmation). 82 | [C 12:44:00.229 NotebookApp] 83 | 84 | To access the notebook, open this file in a browser: 85 | file:///home/jovyan/.local/share/jupyter/runtime/nbserver-8-open.html 86 | Or copy and paste one of these URLs: 87 | http://924904e0a646:8888/?token=40475e553b7671b9e93533b97afe584fa2030448505a7d83 88 | or http://127.0.0.1:8888/?token=40475e553b7671b9e93533b97afe584fa2030448505a7d83 89 | *snip* 90 | ``` 91 | 92 | Now head to the URL shown and you will be greated with a XFCE desktop. 93 | 94 | Note the `--security-opt seccomp=unconfined` parameter - this is necessary 95 | to start daemons (such as dbus, pulseaudio, etc) necessary for linux desktop 96 | to work. This is the option kubernetes runs with by default, so most kubernetes 97 | based JupyterHubs will not need any modifications for this to work. 98 | 99 | ## Configuration 100 | 101 | The VNC server will default to launching `~/.vnc/xstartup`. 102 | If this file does not exist jupyter-remote-desktop-proxy will use a bundled `xstartup` file that launches `dbus-launch xfce4-session`. 103 | You can specify a custom script by setting the environment variable `JUPYTER_REMOTE_DESKTOP_PROXY_XSTARTUP`. 104 | 105 | ## Limitations 106 | 107 | 1. Desktop applications that require access to OpenGL are currently unsupported. 108 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # How to make a release 2 | 3 | `jupyter-remote-desktop-proxy` is a package available on [PyPI]. 4 | 5 | These are the instructions on how to make a release. 6 | 7 | ## Pre-requisites 8 | 9 | - Push rights to this GitHub repository 10 | 11 | ## Steps to make a release 12 | 13 | 1. Create a PR updating `CHANGELOG.md` with [github-activity] and continue when 14 | its merged. 15 | 16 | Advice on this procedure can be found in [this team compass 17 | issue](https://github.com/jupyterhub/team-compass/issues/563). 18 | 19 | 2. Checkout main and make sure it is up to date. 20 | 21 | ```shell 22 | git checkout main 23 | git fetch origin main 24 | git reset --hard origin/main 25 | ``` 26 | 27 | 3. Update the version, make commits, and push a git tag with `tbump`. 28 | 29 | ```shell 30 | pip install tbump 31 | ``` 32 | 33 | `tbump` will ask for confirmation before doing anything. 34 | 35 | ```shell 36 | # Example versions to set: 1.0.0, 1.0.0b1 37 | VERSION= 38 | tbump ${VERSION} 39 | ``` 40 | 41 | Following this, the [CI system] will build and publish a release. 42 | 43 | 4. Reset the version back to dev, e.g. `1.0.1.dev` after releasing `1.0.0`. 44 | 45 | ```shell 46 | # Example version to set: 1.0.1.dev 47 | NEXT_VERSION= 48 | tbump --no-tag ${NEXT_VERSION}.dev 49 | ``` 50 | 51 | [github-activity]: https://github.com/executablebooks/github-activity 52 | [pypi]: https://pypi.org/project/jupyter-remote-desktop-proxy/ 53 | [ci system]: https://github.com/jupyterhub/jupyter-remote-desktop-proxy/actions/workflows/release.yaml 54 | -------------------------------------------------------------------------------- /dev-requirements.txt: -------------------------------------------------------------------------------- 1 | pillow==10.3.0 2 | playwright==1.44.0 3 | pytest==8.2.2 4 | -------------------------------------------------------------------------------- /environment.yml: -------------------------------------------------------------------------------- 1 | channels: 2 | - conda-forge 3 | dependencies: 4 | - jupyter-server-proxy>=4.3.0 5 | - jupyterhub-singleuser 6 | - pip 7 | -------------------------------------------------------------------------------- /js/clipboard.css: -------------------------------------------------------------------------------- 1 | .hidden { 2 | display: none !important; 3 | } 4 | 5 | .clipboard-container { 6 | overflow: visible; /* Needed for the arrow to show up */ 7 | width: max-content; 8 | position: absolute; 9 | top: 0; 10 | left: 0; 11 | background: white; 12 | color: var(--jupyter-dark-grey); 13 | padding: 6px; 14 | border-radius: 4px; 15 | font-size: 90%; 16 | } 17 | 18 | .arrow { 19 | position: absolute; 20 | background: white; 21 | width: 8px; 22 | height: 8px; 23 | transform: rotate(45deg); 24 | } 25 | -------------------------------------------------------------------------------- /js/clipboard.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Setup simplest popover possible to provide popovers. 3 | * 4 | * Mostly follows https://floating-ui.com/docs/tutorial 5 | */ 6 | import { computePosition, flip, shift, offset, arrow } from "@floating-ui/dom"; 7 | import "./clipboard.css"; 8 | 9 | /** 10 | * Setup trigger element to toggle showing / hiding clipboard element 11 | * @param {Element} trigger 12 | * @param {Element} clipboard 13 | * @param {Array[Element]} closers array of elements that should close the clipboard if clicked 14 | */ 15 | export function setupClipboard(trigger, clipboard, closers) { 16 | const arrowElement = clipboard.querySelector(".arrow"); 17 | function updatePosition() { 18 | computePosition(trigger, clipboard, { 19 | placement: "bottom", 20 | middleware: [ 21 | offset(6), 22 | flip(), 23 | shift({ padding: 5 }), 24 | arrow({ element: arrowElement }), 25 | ], 26 | }).then(({ x, y, placement, middlewareData }) => { 27 | Object.assign(clipboard.style, { 28 | left: `${x}px`, 29 | top: `${y}px`, 30 | }); 31 | 32 | // Accessing the data 33 | const { x: arrowX, y: arrowY } = middlewareData.arrow; 34 | 35 | const staticSide = { 36 | top: "bottom", 37 | right: "left", 38 | bottom: "top", 39 | left: "right", 40 | }[placement.split("-")[0]]; 41 | 42 | Object.assign(arrowElement.style, { 43 | left: arrowX != null ? `${arrowX}px` : "", 44 | top: arrowY != null ? `${arrowY}px` : "", 45 | right: "", 46 | bottom: "", 47 | [staticSide]: "-4px", 48 | }); 49 | }); 50 | } 51 | 52 | trigger.addEventListener("click", (e) => { 53 | clipboard.classList.toggle("hidden"); 54 | trigger.classList.toggle("active"); 55 | updatePosition(); 56 | e.preventDefault(); 57 | e.stopPropagation(); 58 | }); 59 | 60 | // If the clipboard is clicked this should not be passed to the desktop 61 | clipboard.addEventListener("click", (e) => { 62 | e.stopPropagation(); 63 | }); 64 | // Close the popup if we click outside it 65 | closers.forEach((el) => { 66 | el.addEventListener("click", () => { 67 | if (trigger.classList.contains("active")) { 68 | clipboard.classList.toggle("hidden"); 69 | trigger.classList.toggle("active"); 70 | } 71 | }); 72 | }); 73 | } 74 | -------------------------------------------------------------------------------- /js/index.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Derived from https://github.com/novnc/noVNC/blob/v1.4.0/vnc_lite.html, which was licensed 3 | * under the 2-clause BSD license 4 | */ 5 | 6 | html { 7 | /** 8 | Colors from https://github.com/jupyter/design/blob/main/brandguide/brand_guide.pdf 9 | **/ 10 | --jupyter-main-brand-color: #f37626; 11 | --jupyter-dark-grey: #4d4d4d; 12 | --jupyter-medium-dark-grey: #616161; 13 | --jupyter-medium-grey: #757575; 14 | --jupyter-grey: #9e9e9e; 15 | 16 | --topbar-height: 32px; 17 | 18 | /* Use Jupyter Brand fonts */ 19 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 20 | } 21 | 22 | body { 23 | height: 100vh; 24 | display: flex; 25 | flex-direction: column; 26 | background-color: var(--jupyter-medium-dark-grey); 27 | } 28 | 29 | #top-bar { 30 | background-color: var(--jupyter-dark-grey); 31 | color: white; 32 | border-bottom: 1px white; 33 | display: flex; 34 | align-items: center; 35 | } 36 | 37 | #logo { 38 | padding: 0 24px; 39 | } 40 | 41 | #logo img { 42 | height: 24px; 43 | } 44 | 45 | #menu { 46 | display: flex; 47 | font-weight: bold; 48 | margin-left: auto; 49 | font-size: 12px; 50 | } 51 | 52 | #menu li { 53 | border-right: 1px var(--jupyter-grey) solid; 54 | padding: 12px 0px; 55 | } 56 | 57 | #menu li:last-child { 58 | border-right: 0; 59 | } 60 | 61 | #menu a { 62 | color: white; 63 | text-decoration: none; 64 | padding: 12px 8px; 65 | } 66 | 67 | #menu a:hover, 68 | #menu a.active { 69 | background-color: var(--jupyter-medium-grey); 70 | } 71 | 72 | li#status-container { 73 | padding-right: 8px; 74 | } 75 | 76 | #status-label { 77 | font-weight: normal; 78 | } 79 | 80 | #screen { 81 | flex: 1; 82 | /* fill remaining space */ 83 | overflow: hidden; 84 | } 85 | 86 | /* Clipboard */ 87 | #clipboard-content { 88 | display: flex; 89 | flex-direction: column; 90 | padding: 4px; 91 | gap: 4px; 92 | } 93 | 94 | #clipboard-text { 95 | min-width: 500px; 96 | max-width: 100%; 97 | } 98 | -------------------------------------------------------------------------------- /js/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Derived from https://github.com/novnc/noVNC/blob/v1.4.0/vnc_lite.html, which was licensed 3 | * under the 2-clause BSD license 4 | */ 5 | 6 | import "reset-css"; 7 | import "./index.css"; 8 | 9 | // RFB holds the API to connect and communicate with a VNC server 10 | import RFB from "@novnc/novnc/lib/rfb"; 11 | 12 | import { setupClipboard } from "./clipboard.js"; 13 | 14 | const maxRetryCount = 5; 15 | const retryInterval = 3; // seconds 16 | 17 | // When this function is called we have successfully connected to a server 18 | function connectedToServer() { 19 | status("Connected"); 20 | } 21 | 22 | // This function is called when we are disconnected 23 | function disconnectedFromServer(e) { 24 | if (e.detail.clean) { 25 | status("Disconnected"); 26 | } else { 27 | status("Something went wrong, connection is closed"); 28 | if (retryCount < maxRetryCount) { 29 | status(`Reconnecting in ${retryInterval} seconds`); 30 | setTimeout(() => { 31 | connect(); 32 | retryCount++; 33 | }, retryInterval * 1000); 34 | } else { 35 | status("Failed to connect, giving up"); 36 | } 37 | } 38 | } 39 | 40 | // Show a status text in the top bar 41 | function status(text) { 42 | document.getElementById("status").textContent = text; 43 | } 44 | 45 | // This page is served under the /desktop/, and the websockify websocket is served 46 | // under /desktop-websockify/ with the same base url as /desktop/. We resolve it relatively 47 | // this way. 48 | let websockifyUrl = new URL("../desktop-websockify/", window.location); 49 | websockifyUrl.protocol = window.location.protocol === "https:" ? "wss" : "ws"; 50 | 51 | let retryCount = 0; 52 | 53 | function connect() { 54 | // Creating a new RFB object will start a new connection 55 | let rfb = new RFB( 56 | document.getElementById("screen"), 57 | websockifyUrl.toString(), 58 | {}, 59 | ); 60 | 61 | // Add listeners to important events from the RFB module 62 | rfb.addEventListener("connect", connectedToServer); 63 | rfb.addEventListener("disconnect", disconnectedFromServer); 64 | 65 | // Resize our viewport so the user doesn't have to scroll 66 | rfb.resizeSession = true; 67 | 68 | // Use a CSS variable to set background color 69 | rfb.background = "var(--jupyter-medium-dark-grey)"; 70 | 71 | // Clipboard 72 | function clipboardReceive(e) { 73 | document.getElementById("clipboard-text").value = e.detail.text; 74 | } 75 | rfb.addEventListener("clipboard", clipboardReceive); 76 | 77 | function clipboardSend() { 78 | const text = document.getElementById("clipboard-text").value; 79 | rfb.clipboardPasteFrom(text); 80 | } 81 | document 82 | .getElementById("clipboard-text") 83 | .addEventListener("change", clipboardSend); 84 | 85 | setupClipboard( 86 | document.getElementById("clipboard-button"), 87 | document.getElementById("clipboard-container"), 88 | [document.body, document.getElementsByTagName("canvas")[0]], 89 | ); 90 | } 91 | 92 | // Start the connection 93 | connect(); 94 | -------------------------------------------------------------------------------- /jupyter-config/jupyter_notebook_config.d/jupyter_remote_desktop_proxy.json: -------------------------------------------------------------------------------- 1 | { 2 | "NotebookApp": { 3 | "nbserver_extensions": { 4 | "jupyter_remote_desktop_proxy": true 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /jupyter-config/jupyter_server_config.d/jupyter_remote_desktop_proxy.json: -------------------------------------------------------------------------------- 1 | { 2 | "ServerApp": { 3 | "jpserver_extensions": { 4 | "jupyter_remote_desktop_proxy": true 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /jupyter_remote_desktop_proxy/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from .server_extension import load_jupyter_server_extension 4 | 5 | HERE = os.path.dirname(os.path.abspath(__file__)) 6 | 7 | 8 | def _jupyter_server_extension_points(): 9 | """ 10 | Set up the server extension for collecting metrics 11 | """ 12 | return [{"module": "jupyter_remote_desktop_proxy"}] 13 | 14 | 15 | # For backward compatibility 16 | _load_jupyter_server_extension = load_jupyter_server_extension 17 | _jupyter_server_extension_paths = _jupyter_server_extension_points 18 | -------------------------------------------------------------------------------- /jupyter_remote_desktop_proxy/handlers.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import jinja2 4 | from jupyter_server.base.handlers import JupyterHandler 5 | from tornado import web 6 | 7 | jinja_env = jinja2.Environment( 8 | loader=jinja2.FileSystemLoader( 9 | os.path.join(os.path.dirname(__file__), 'templates') 10 | ), 11 | ) 12 | 13 | 14 | HERE = os.path.dirname(os.path.abspath(__file__)) 15 | 16 | 17 | class DesktopHandler(JupyterHandler): 18 | @web.authenticated 19 | async def get(self): 20 | template_params = { 21 | 'base_url': self.base_url, 22 | } 23 | template_params.update(self.serverapp.jinja_template_vars) 24 | self.write(jinja_env.get_template("index.html").render(**template_params)) 25 | -------------------------------------------------------------------------------- /jupyter_remote_desktop_proxy/server_extension.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from jupyter_server.base.handlers import AuthenticatedFileHandler 4 | from jupyter_server.utils import url_path_join 5 | from jupyter_server_proxy.handlers import AddSlashHandler 6 | 7 | from .handlers import DesktopHandler 8 | 9 | HERE = Path(__file__).parent 10 | 11 | 12 | def load_jupyter_server_extension(server_app): 13 | """ 14 | Called during notebook start 15 | """ 16 | base_url = server_app.web_app.settings["base_url"] 17 | 18 | server_app.web_app.add_handlers( 19 | ".*", 20 | [ 21 | # Serve our own static files 22 | ( 23 | url_path_join(base_url, "/desktop/static/(.*)"), 24 | AuthenticatedFileHandler, 25 | {"path": (str(HERE / "static"))}, 26 | ), 27 | # To simplify URL mapping, we make sure that /desktop/ always 28 | # has a trailing slash 29 | (url_path_join(base_url, "/desktop"), AddSlashHandler), 30 | (url_path_join(base_url, "/desktop/"), DesktopHandler), 31 | ], 32 | ) 33 | -------------------------------------------------------------------------------- /jupyter_remote_desktop_proxy/setup_websockify.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shlex 3 | from shutil import which 4 | 5 | HERE = os.path.dirname(os.path.abspath(__file__)) 6 | 7 | 8 | def setup_websockify(): 9 | vncserver = which('vncserver') 10 | if not vncserver: 11 | raise RuntimeError( 12 | "vncserver executable not found, please install a VNC server" 13 | ) 14 | 15 | # TurboVNC and TigerVNC share the same origin and both use a Perl script 16 | # as the executable vncserver. We can determine if vncserver is TigerVNC 17 | # by searching tigervnc string in the Perl script. 18 | # 19 | # The content of the vncserver executable can differ depending on how 20 | # TigerVNC and TurboVNC has been distributed. Below are files known to be 21 | # read in some situations: 22 | # 23 | # - https://github.com/TigerVNC/tigervnc/blob/v1.13.1/unix/vncserver/vncserver.in 24 | # - https://github.com/TurboVNC/turbovnc/blob/3.1.1/unix/vncserver.in 25 | # 26 | with open(vncserver) as vncserver_file: 27 | vncserver_file_text = vncserver_file.read().casefold() 28 | is_turbovnc = "turbovnc" in vncserver_file_text 29 | 30 | # {unix_socket} is expanded by jupyter-server-proxy 31 | vnc_args = [vncserver, '-rfbunixpath', "{unix_socket}", "-rfbport", "-1"] 32 | if is_turbovnc: 33 | # turbovnc doesn't handle being passed -rfbport -1, but turbovnc also 34 | # defaults to not opening a TCP port which is what we want to ensure 35 | vnc_args = [vncserver, '-rfbunixpath', "{unix_socket}"] 36 | 37 | xstartup = os.getenv("JUPYTER_REMOTE_DESKTOP_PROXY_XSTARTUP") 38 | if not xstartup and not os.path.exists(os.path.expanduser('~/.vnc/xstartup')): 39 | xstartup = os.path.join(HERE, 'share/xstartup') 40 | if xstartup: 41 | vnc_args.extend(['-xstartup', xstartup]) 42 | 43 | vnc_command = shlex.join( 44 | vnc_args 45 | + [ 46 | '-verbose', 47 | '-fg', 48 | '-geometry', 49 | '1680x1050', 50 | '-SecurityTypes', 51 | 'None', 52 | ] 53 | ) 54 | 55 | return { 56 | 'command': ['/bin/sh', '-c', f'cd {os.getcwd()} && {vnc_command}'], 57 | 'timeout': 30, 58 | 'new_browser_window': True, 59 | # We want the launcher entry to point to /desktop/, not to /desktop-websockify/ 60 | # /desktop/ is the user facing URL, while /desktop-websockify/ now *only* serves 61 | # websockets. 62 | "launcher_entry": {"title": "Desktop", "path_info": "desktop"}, 63 | "unix_socket": True, 64 | "raw_socket_proxy": True, 65 | } 66 | -------------------------------------------------------------------------------- /jupyter_remote_desktop_proxy/share/xstartup: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | cd "$HOME" 3 | exec dbus-launch xfce4-session 4 | -------------------------------------------------------------------------------- /jupyter_remote_desktop_proxy/static/clipboard.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 107 | -------------------------------------------------------------------------------- /jupyter_remote_desktop_proxy/static/jupyter-logo.svg: -------------------------------------------------------------------------------- 1 | 89 | -------------------------------------------------------------------------------- /jupyter_remote_desktop_proxy/templates/index.html: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 |
8 |((a|b|rc)\d+)|) 53 | \.? 54 | (?P(?<=\.)dev\d*|) 55 | ''' 56 | 57 | [tool.tbump.git] 58 | message_template = "Bump to {new_version}" 59 | tag_template = "v{new_version}" 60 | 61 | [[tool.tbump.file]] 62 | src = "setup.py" 63 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from subprocess import check_call 3 | 4 | from setuptools import find_packages, setup 5 | from setuptools.command.build_py import build_py 6 | from setuptools.command.sdist import sdist 7 | 8 | HERE = os.path.dirname(__file__) 9 | 10 | 11 | def webpacked_command(command): 12 | """ 13 | Return a command that inherits from command, but adds webpack JS building 14 | """ 15 | 16 | class WebPackedCommand(command): 17 | """ 18 | Run npm webpack to generate appropriate output files before given command. 19 | 20 | This generates all the js & css we need, and that is included via an 21 | entry in MANIFEST.in 22 | """ 23 | 24 | description = "build frontend files with webpack" 25 | 26 | def run(self): 27 | """ 28 | Call npm install & npm run webpack before packaging 29 | """ 30 | check_call( 31 | ["npm", "install", "--progress=false", "--unsafe-perm"], 32 | cwd=HERE, 33 | ) 34 | 35 | check_call(["npm", "run", "webpack"], cwd=HERE) 36 | 37 | return super().run() 38 | 39 | return WebPackedCommand 40 | 41 | 42 | with open("README.md") as f: 43 | readme = f.read() 44 | 45 | 46 | setup( 47 | name="jupyter-remote-desktop-proxy", 48 | packages=find_packages(), 49 | version='3.0.2.dev', 50 | author="Jupyter Development Team", 51 | author_email="jupyter@googlegroups.com", 52 | classifiers=[ 53 | "Intended Audience :: Developers", 54 | "Intended Audience :: System Administrators", 55 | "Intended Audience :: Science/Research", 56 | "License :: OSI Approved :: BSD License", 57 | "Programming Language :: Python", 58 | "Programming Language :: Python :: 3", 59 | ], 60 | description="Run a desktop environments on Jupyter", 61 | entry_points={ 62 | 'jupyter_serverproxy_servers': [ 63 | 'desktop-websockify = jupyter_remote_desktop_proxy.setup_websockify:setup_websockify', 64 | ] 65 | }, 66 | install_requires=[ 67 | 'jupyter-server-proxy>=4.3.0', 68 | ], 69 | include_package_data=True, 70 | keywords=["Interactive", "Desktop", "Jupyter"], 71 | license="BSD", 72 | long_description=readme, 73 | long_description_content_type="text/markdown", 74 | platforms="Linux", 75 | project_urls={ 76 | "Source": "https://github.com/jupyterhub/jupyter-remote-desktop-proxy/", 77 | "Tracker": "https://github.com/jupyterhub/jupyter-remote-desktop-proxy/issues", 78 | }, 79 | python_requires=">=3.8", 80 | url="https://jupyter.org", 81 | zip_safe=False, 82 | cmdclass={ 83 | # Handles making sdists and wheels 84 | "sdist": webpacked_command(sdist), 85 | # Handles `pip install` directly 86 | "build_py": webpacked_command(build_py), 87 | }, 88 | data_files=[ 89 | ( 90 | 'etc/jupyter/jupyter_server_config.d', 91 | [ 92 | 'jupyter-config/jupyter_server_config.d/jupyter_remote_desktop_proxy.json' 93 | ], 94 | ), 95 | ( 96 | 'etc/jupyter/jupyter_notebook_config.d', 97 | [ 98 | 'jupyter-config/jupyter_notebook_config.d/jupyter_remote_desktop_proxy.json' 99 | ], 100 | ), 101 | ], 102 | ) 103 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | from os import getenv 2 | 3 | import pytest 4 | from playwright.sync_api import sync_playwright 5 | 6 | HEADLESS = getenv("HEADLESS", "1") == "1" 7 | 8 | 9 | @pytest.fixture() 10 | def browser(): 11 | # browser_type in ["chromium", "firefox", "webkit"] 12 | with sync_playwright() as playwright: 13 | browser = playwright.firefox.launch(headless=HEADLESS) 14 | context = browser.new_context() 15 | page = context.new_page() 16 | yield page 17 | context.clear_cookies() 18 | browser.close() 19 | -------------------------------------------------------------------------------- /tests/reference/desktop-turbovnc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyterhub/jupyter-remote-desktop-proxy/7971fa44153c6fe4e58fc8ba436c327df611ac03/tests/reference/desktop-turbovnc.png -------------------------------------------------------------------------------- /tests/reference/desktop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyterhub/jupyter-remote-desktop-proxy/7971fa44153c6fe4e58fc8ba436c327df611ac03/tests/reference/desktop.png -------------------------------------------------------------------------------- /tests/test_browser.py: -------------------------------------------------------------------------------- 1 | from os import getenv 2 | from pathlib import Path 3 | from shutil import which 4 | from subprocess import check_output 5 | from uuid import uuid4 6 | 7 | from PIL import Image, ImageChops 8 | from playwright.sync_api import expect 9 | 10 | HERE = Path(__file__).absolute().parent 11 | 12 | CONTAINER_ID = getenv("CONTAINER_ID", "test") 13 | JUPYTER_HOST = getenv("JUPYTER_HOST", "http://localhost:8888") 14 | JUPYTER_TOKEN = getenv("JUPYTER_TOKEN", "secret") 15 | VNCSERVER = getenv("VNCSERVER") 16 | 17 | 18 | def compare_screenshot(test_image): 19 | # Compare images by calculating the mean absolute difference 20 | # Images must be the same size 21 | # threshold: Average difference per pixel, this depends on the image type 22 | # e.g. for 24 bit images (8 bit RGB pixels) threshold=1 means a maximum 23 | # difference of 1 bit per pixel per channel 24 | reference = Image.open(HERE / "reference" / "desktop.png") 25 | threshold = 2 26 | if VNCSERVER == "turbovnc": 27 | reference = Image.open(HERE / "reference" / "desktop-turbovnc.png") 28 | # The TurboVNC screenshot varies a lot more than TigerVNC 29 | threshold = 6 30 | test = Image.open(test_image) 31 | 32 | # Absolute difference 33 | # Convert to RGB, alpha channel breaks ImageChops 34 | diff = ImageChops.difference(reference.convert("RGB"), test.convert("RGB")) 35 | diff_data = diff.getdata() 36 | 37 | m = sum(sum(px) for px in diff_data) / diff_data.size[0] / diff_data.size[1] 38 | assert m < threshold 39 | 40 | 41 | # To debug this set environment variable HEADLESS=0 42 | def test_desktop(browser): 43 | page = browser 44 | page.goto(f"{JUPYTER_HOST}/lab?token={JUPYTER_TOKEN}") 45 | page.wait_for_url(f"{JUPYTER_HOST}/lab") 46 | 47 | # JupyterLab extension icon 48 | expect(page.get_by_text("Desktop [↗]")).to_be_visible() 49 | with page.expect_popup() as page1_info: 50 | page.get_by_text("Desktop [↗]").click() 51 | page1 = page1_info.value 52 | page1.wait_for_url(f"{JUPYTER_HOST}/desktop/") 53 | 54 | expect(page1.get_by_text("Status: Connected")).to_be_visible() 55 | expect(page1.locator("canvas")).to_be_visible() 56 | 57 | # Screenshot the desktop element only 58 | # May take a few seconds to load 59 | page1.wait_for_timeout(5000) 60 | # Use a non temporary folder so we can check it manually if necessary 61 | screenshot = Path("screenshots") / "desktop.png" 62 | page1.locator("body").screenshot(path=screenshot) 63 | 64 | # Open clipboard, enter random text, close clipboard 65 | clipboard_text = str(uuid4()) 66 | page1.get_by_role("link", name="Remote Clipboard").click() 67 | expect(page1.locator("#clipboard-text")).to_be_visible() 68 | page1.locator("#clipboard-text").click() 69 | page1.locator("#clipboard-text").fill(clipboard_text) 70 | # Click outside clipboard, it should be closed 71 | page1.locator("canvas").click(position={"x": 969, "y": 273}) 72 | expect(page1.locator("#clipboard-text")).not_to_be_visible() 73 | 74 | # Exec into container to check clipboard contents 75 | for engine in ["docker", "podman"]: 76 | if which(engine): 77 | break 78 | else: 79 | raise RuntimeError("Container engine not found") 80 | clipboard = check_output( 81 | [engine, "exec", "-eDISPLAY=:1", CONTAINER_ID, "xclip", "-o"] 82 | ) 83 | assert clipboard.decode() == clipboard_text 84 | 85 | compare_screenshot(screenshot) 86 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require("webpack"); 2 | const MiniCssExtractPlugin = require("mini-css-extract-plugin"); 3 | const path = require("path"); 4 | 5 | module.exports = { 6 | entry: path.resolve(__dirname, "js/index.js"), 7 | plugins: [ 8 | new MiniCssExtractPlugin({ 9 | filename: "index.css", 10 | }), 11 | ], 12 | devtool: "source-map", 13 | mode: "production", 14 | module: { 15 | rules: [ 16 | { 17 | test: /\.(js|jsx)/, 18 | exclude: /node_modules/, 19 | use: "babel-loader", 20 | }, 21 | { 22 | test: /\.(css)/, 23 | use: [MiniCssExtractPlugin.loader, "css-loader"], 24 | }, 25 | ], 26 | }, 27 | output: { 28 | publicPath: "/", 29 | filename: "viewer.js", 30 | path: path.resolve(__dirname, "jupyter_remote_desktop_proxy/static/dist"), 31 | }, 32 | resolve: { 33 | extensions: [".css", ".js", ".jsx"], 34 | }, 35 | }; 36 | --------------------------------------------------------------------------------