├── FUNDING.yml ├── rdiffweb ├── core │ ├── __init__.py │ ├── tests │ │ ├── __init__.py │ │ ├── rdiff-backup-data │ │ │ ├── error_log.2019-05-22T09:19:09-04:00.data.gz │ │ │ ├── error_log.2015-11-19T07:27:46-05:00.data │ │ │ ├── error_log.2015-11-20T07:27:46-05:00.data.gz │ │ │ ├── file_statistics.2014-11-05T16:05:07-05:00.data.gz │ │ │ ├── session_statistics.2014-11-02T09:16:43-05:00.data │ │ │ └── file_statistics.2014-11-05T16:05:07-05:00.data │ │ ├── test_publickey_ssh_rsa.pub │ │ ├── session_statistics.2014-11-02T09:16:43-05:00.data │ │ ├── test_publickey_ssh_dsa.pub │ │ ├── test_authorized_keys │ │ ├── test_rdw_helpers.py │ │ ├── test_passwd.py │ │ └── test_remove_older.py │ ├── model │ │ ├── tests │ │ │ └── __init__.py │ │ ├── __init__.py │ │ ├── _callbacks.py │ │ └── _timestamp.py │ ├── rdw_helpers.py │ ├── passwd.py │ └── remove_older.py ├── plugins │ ├── __init__.py │ ├── tests │ │ └── __init__.py │ └── restapi.py ├── tests │ ├── __init__.py │ ├── rdw.conf │ ├── rdw.db │ ├── testcases.tar.gz │ ├── test_app.py │ └── test_main.py ├── tools │ ├── __init__.py │ ├── errors.py │ ├── poppath.py │ ├── required_scope.py │ └── enrich_session.py ├── controller │ ├── tests │ │ ├── __init__.py │ │ ├── test_page_admin_sysinfo.py │ │ ├── test_page_admin.py │ │ ├── test_page_admin_activity.py │ │ ├── test_page_error.py │ │ ├── test_page_admin_repos.py │ │ └── test_check_links.py │ ├── page_admin_repos.py │ ├── filter_authorization.py │ ├── page_locations.py │ ├── page_history.py │ ├── page_prefs.py │ ├── page_browse.py │ ├── dispatch.py │ ├── page_admin.py │ ├── formdb.py │ ├── page_delete.py │ ├── page_admin_logs.py │ ├── api.py │ ├── page_logs.py │ ├── page_admin_session.py │ └── page_pref_session.py ├── static │ ├── robots.txt │ ├── logo1.png │ ├── favicon.ico │ ├── header-logo.png │ ├── images │ │ ├── sort_asc.png │ │ ├── sort_both.png │ │ ├── sort_desc.png │ │ ├── sort_asc_disabled.png │ │ └── sort_desc_disabled.png │ ├── fonts │ │ ├── fontawesome-webfont.eot │ │ ├── fontawesome-webfont.woff │ │ └── fontawesome-webfont.woff2 │ └── css │ │ └── responsive.dataTables.min.css ├── locales │ ├── ca │ │ └── LC_MESSAGES │ │ │ └── messages.mo │ ├── de │ │ └── LC_MESSAGES │ │ │ └── messages.mo │ ├── en │ │ └── LC_MESSAGES │ │ │ └── messages.mo │ ├── es │ │ └── LC_MESSAGES │ │ │ └── messages.mo │ ├── fr │ │ └── LC_MESSAGES │ │ │ └── messages.mo │ ├── pt │ │ └── LC_MESSAGES │ │ │ └── messages.mo │ └── ru │ │ └── LC_MESSAGES │ │ └── messages.mo ├── templates │ ├── components │ │ ├── log.html │ │ ├── nav.html │ │ └── form.html │ ├── include │ │ ├── empty.html │ │ ├── messages.html │ │ ├── panel.html │ │ ├── timerange.html │ │ ├── session.html │ │ ├── chartkick.html │ │ ├── storage_usage.html │ │ ├── datatables.html │ │ └── table.html │ ├── graphs_times.html │ ├── graphs_files.html │ ├── graphs_errors.html │ ├── graphs_sizes.html │ ├── graphs_activities.html │ ├── message.html │ ├── error_page_default.html │ ├── admin_user_new.html │ ├── email_password_changed.html │ ├── email_access_token_added.html │ ├── email_changed.html │ ├── email_repo_deleted.html │ ├── email_authorizedkey_added.html │ ├── email_repo_added.html │ ├── email_latest.html │ ├── prefs_mfa.html │ ├── email_verification_code.html │ ├── email_mfa.html │ ├── mfa.html │ ├── prefs_general.html │ ├── admin_sysinfo.html │ ├── prefs.html │ ├── admin.html │ ├── prefs_notification.html │ ├── layout_repo.html │ ├── graphs.html │ ├── admin_overview.html │ ├── email_notification.html │ ├── admin_activity.html │ ├── settings.html │ ├── admin_user_edit.html │ ├── login.html │ ├── admin_logs.html │ ├── restore.html │ ├── prefs_sshkeys.html │ ├── prefs_session.html │ ├── logs.html │ ├── stats.html │ └── status.html └── __init__.py ├── debian ├── TODO ├── rdiffweb.examples ├── source │ ├── format │ └── options ├── rdiffweb.docs ├── rdiffweb.install ├── py3dist-overrides ├── tests │ ├── control │ └── smoke ├── changelog ├── rdiffweb.service ├── .gitignore ├── upstream │ └── metadata ├── rdiffweb.postrm ├── rdiffweb.postinst ├── rules ├── control ├── copyright └── rdiffweb.links ├── doc ├── .gitignore ├── _static │ ├── logo.png │ └── banner.png ├── rdiffweb-users.png ├── repo-encoding.png ├── faq-good-encoding.png ├── faq-wrong-encoding.png ├── settings-user-role.png ├── usage.rst ├── index.rst ├── access_tokens.md ├── development.md ├── two_factor_authentication.md ├── quickstart.md ├── configuration-latest.md ├── fail2ban.md ├── api.md ├── hardening.md └── introduction.md ├── sonar-project.properties ├── babel.cfg ├── extras ├── systemd │ └── rdiffweb.service └── nginx │ ├── rdiffweb_root.conf │ └── rdiffweb.conf ├── .dockerignore ├── SECURITY.md ├── .gitignore ├── Dockerfile ├── rdw.conf ├── MANIFEST.in └── pyproject.toml /FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [ikus060] -------------------------------------------------------------------------------- /rdiffweb/core/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /rdiffweb/plugins/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /rdiffweb/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /rdiffweb/tools/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /rdiffweb/core/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /debian/TODO: -------------------------------------------------------------------------------- 1 | apache configuration 2 | -------------------------------------------------------------------------------- /debian/rdiffweb.examples: -------------------------------------------------------------------------------- 1 | rdw.conf 2 | -------------------------------------------------------------------------------- /debian/source/format: -------------------------------------------------------------------------------- 1 | 3.0 (quilt) 2 | -------------------------------------------------------------------------------- /rdiffweb/controller/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /rdiffweb/core/model/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /rdiffweb/plugins/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /debian/rdiffweb.docs: -------------------------------------------------------------------------------- 1 | README.md 2 | doc 3 | -------------------------------------------------------------------------------- /doc/.gitignore: -------------------------------------------------------------------------------- 1 | openapi.json 2 | endpoints.md -------------------------------------------------------------------------------- /rdiffweb/tests/rdw.conf: -------------------------------------------------------------------------------- 1 | SQLiteDBFile=rdw.db -------------------------------------------------------------------------------- /debian/rdiffweb.install: -------------------------------------------------------------------------------- 1 | rdw.conf /etc/rdiffweb 2 | -------------------------------------------------------------------------------- /rdiffweb/static/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: / -------------------------------------------------------------------------------- /debian/py3dist-overrides: -------------------------------------------------------------------------------- 1 | zxcvbn python3-zxcvbn | python3-python-zxcvbn-rs-py -------------------------------------------------------------------------------- /doc/_static/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikus060/rdiffweb/HEAD/doc/_static/logo.png -------------------------------------------------------------------------------- /rdiffweb/core/tests/rdiff-backup-data/error_log.2019-05-22T09:19:09-04:00.data.gz: -------------------------------------------------------------------------------- 1 | invalid 2 | -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | sonar.exclusions=**/test_*.py,rdiffweb/test.py,**/doc/conf.py,**/*.js -------------------------------------------------------------------------------- /doc/_static/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikus060/rdiffweb/HEAD/doc/_static/banner.png -------------------------------------------------------------------------------- /doc/rdiffweb-users.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikus060/rdiffweb/HEAD/doc/rdiffweb-users.png -------------------------------------------------------------------------------- /doc/repo-encoding.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikus060/rdiffweb/HEAD/doc/repo-encoding.png -------------------------------------------------------------------------------- /rdiffweb/tests/rdw.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikus060/rdiffweb/HEAD/rdiffweb/tests/rdw.db -------------------------------------------------------------------------------- /doc/faq-good-encoding.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikus060/rdiffweb/HEAD/doc/faq-good-encoding.png -------------------------------------------------------------------------------- /rdiffweb/static/logo1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikus060/rdiffweb/HEAD/rdiffweb/static/logo1.png -------------------------------------------------------------------------------- /babel.cfg: -------------------------------------------------------------------------------- 1 | [ignore: tests/**] 2 | 3 | [python: **.py] 4 | [jinja2: **/templates/**.html] 5 | encoding = utf-8 -------------------------------------------------------------------------------- /doc/faq-wrong-encoding.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikus060/rdiffweb/HEAD/doc/faq-wrong-encoding.png -------------------------------------------------------------------------------- /doc/settings-user-role.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikus060/rdiffweb/HEAD/doc/settings-user-role.png -------------------------------------------------------------------------------- /rdiffweb/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikus060/rdiffweb/HEAD/rdiffweb/static/favicon.ico -------------------------------------------------------------------------------- /debian/tests/control: -------------------------------------------------------------------------------- 1 | Tests: smoke 2 | Depends: 3 | rdiffweb, 4 | Restrictions: allow-stderr, superficial 5 | -------------------------------------------------------------------------------- /rdiffweb/static/header-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikus060/rdiffweb/HEAD/rdiffweb/static/header-logo.png -------------------------------------------------------------------------------- /rdiffweb/tests/testcases.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikus060/rdiffweb/HEAD/rdiffweb/tests/testcases.tar.gz -------------------------------------------------------------------------------- /rdiffweb/static/images/sort_asc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikus060/rdiffweb/HEAD/rdiffweb/static/images/sort_asc.png -------------------------------------------------------------------------------- /rdiffweb/static/images/sort_both.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikus060/rdiffweb/HEAD/rdiffweb/static/images/sort_both.png -------------------------------------------------------------------------------- /rdiffweb/static/images/sort_desc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikus060/rdiffweb/HEAD/rdiffweb/static/images/sort_desc.png -------------------------------------------------------------------------------- /rdiffweb/locales/ca/LC_MESSAGES/messages.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikus060/rdiffweb/HEAD/rdiffweb/locales/ca/LC_MESSAGES/messages.mo -------------------------------------------------------------------------------- /rdiffweb/locales/de/LC_MESSAGES/messages.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikus060/rdiffweb/HEAD/rdiffweb/locales/de/LC_MESSAGES/messages.mo -------------------------------------------------------------------------------- /rdiffweb/locales/en/LC_MESSAGES/messages.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikus060/rdiffweb/HEAD/rdiffweb/locales/en/LC_MESSAGES/messages.mo -------------------------------------------------------------------------------- /rdiffweb/locales/es/LC_MESSAGES/messages.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikus060/rdiffweb/HEAD/rdiffweb/locales/es/LC_MESSAGES/messages.mo -------------------------------------------------------------------------------- /rdiffweb/locales/fr/LC_MESSAGES/messages.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikus060/rdiffweb/HEAD/rdiffweb/locales/fr/LC_MESSAGES/messages.mo -------------------------------------------------------------------------------- /rdiffweb/locales/pt/LC_MESSAGES/messages.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikus060/rdiffweb/HEAD/rdiffweb/locales/pt/LC_MESSAGES/messages.mo -------------------------------------------------------------------------------- /rdiffweb/locales/ru/LC_MESSAGES/messages.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikus060/rdiffweb/HEAD/rdiffweb/locales/ru/LC_MESSAGES/messages.mo -------------------------------------------------------------------------------- /rdiffweb/core/tests/rdiff-backup-data/error_log.2015-11-19T07:27:46-05:00.data: -------------------------------------------------------------------------------- 1 | SpecialFileError home/coucou Socket error: AF_UNIX path too long -------------------------------------------------------------------------------- /rdiffweb/static/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikus060/rdiffweb/HEAD/rdiffweb/static/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /rdiffweb/static/images/sort_asc_disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikus060/rdiffweb/HEAD/rdiffweb/static/images/sort_asc_disabled.png -------------------------------------------------------------------------------- /rdiffweb/static/images/sort_desc_disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikus060/rdiffweb/HEAD/rdiffweb/static/images/sort_desc_disabled.png -------------------------------------------------------------------------------- /rdiffweb/static/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikus060/rdiffweb/HEAD/rdiffweb/static/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /rdiffweb/static/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikus060/rdiffweb/HEAD/rdiffweb/static/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /rdiffweb/templates/components/log.html: -------------------------------------------------------------------------------- 1 | {% macro pre_code(data) %} 2 |
{{data}}
3 | {% endmacro %} 4 | -------------------------------------------------------------------------------- /debian/changelog: -------------------------------------------------------------------------------- 1 | rdiffweb (2.8.6-1) unstable; urgency=medium 2 | 3 | * Initial upload to debian (Closes: #969974). 4 | 5 | -- Patrik Dufresne Mon, 08 Feb 2021 13:11:20 +0300 6 | -------------------------------------------------------------------------------- /debian/rdiffweb.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Rdiffweb server 3 | Documentation=man:rdiffweb(1) 4 | 5 | [Service] 6 | ExecStart=/usr/bin/rdiffweb 7 | 8 | [Install] 9 | WantedBy=multi-user.target 10 | -------------------------------------------------------------------------------- /extras/systemd/rdiffweb.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Rdiffweb Server 3 | Documentation=https://rdiffweb.org/ 4 | 5 | [Service] 6 | ExecStart=/usr/local/bin/rdiffweb 7 | 8 | [Install] 9 | WantedBy=multi-user.target -------------------------------------------------------------------------------- /rdiffweb/core/tests/rdiff-backup-data/error_log.2015-11-20T07:27:46-05:00.data.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikus060/rdiffweb/HEAD/rdiffweb/core/tests/rdiff-backup-data/error_log.2015-11-20T07:27:46-05:00.data.gz -------------------------------------------------------------------------------- /rdiffweb/core/tests/rdiff-backup-data/file_statistics.2014-11-05T16:05:07-05:00.data.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikus060/rdiffweb/HEAD/rdiffweb/core/tests/rdiff-backup-data/file_statistics.2014-11-05T16:05:07-05:00.data.gz -------------------------------------------------------------------------------- /debian/.gitignore: -------------------------------------------------------------------------------- 1 | /rdiffweb/ 2 | /.debhelper/ 3 | /rdiffweb.1 4 | /rdiffweb.postinst.debhelper 5 | /rdiffweb.prerm.debhelper 6 | /rdiffweb.substvars 7 | /debhelper-build-stamp 8 | /files 9 | /rdiffweb.postrm.debhelper 10 | -------------------------------------------------------------------------------- /debian/tests/smoke: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -ex 3 | 4 | # dep8 smoke test for rdiff-backup 5 | # Author: Patrik Dufresne 6 | # 7 | # This very simple test just checks that the binary starts 8 | 9 | rdiffweb --help 2>&1 | grep 'usage:' 10 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .eggs 2 | .externalToolBuilders 3 | .pybuild 4 | .pybuild_cache 5 | .settings 6 | .tox 7 | debian 8 | doc 9 | extras 10 | rdiffweb.egg-info 11 | .gitlab-ci.yml 12 | .venv 13 | .vscode 14 | *.eggs 15 | testrdw.conf 16 | rdw.db 17 | tox.ini 18 | vendor -------------------------------------------------------------------------------- /debian/upstream/metadata: -------------------------------------------------------------------------------- 1 | Name: rdiffweb 2 | Bug-Database: https://gitlab.com/ikus-soft/rdiffweb/-/issues 3 | Contact: https://gitlab.com/ikus-soft/rdiffweb/-/project_members 4 | Repository: git://git@gitlab.com:ikus-soft/rdiffweb.git 5 | Repository-Browse: https://gitlab.com/ikus-soft/rdiffweb/ 6 | -------------------------------------------------------------------------------- /rdiffweb/templates/include/empty.html: -------------------------------------------------------------------------------- 1 | {# Repo list #} 2 | {% macro empty(icon, title) -%} 3 |
4 |

5 | 6 |

7 |

{{ title }}

8 | {{ caller() }} 9 |
10 | {%- endmacro %} 11 | -------------------------------------------------------------------------------- /doc/usage.rst: -------------------------------------------------------------------------------- 1 | 2 | Use Rdiffweb 3 | ============ 4 | 5 | Learn how to use Rdiffweb end-to-end. Create users, manage user's permission and authentication. 6 | 7 | .. toctree:: 8 | :maxdepth: 1 9 | :caption: Contents: 10 | 11 | settings 12 | access_tokens 13 | two_factor_authentication 14 | -------------------------------------------------------------------------------- /rdiffweb/templates/graphs_times.html: -------------------------------------------------------------------------------- 1 | {% extends 'graphs.html' %} 2 | {% import 'include/chartkick.html' as chartkick with context %} 3 | {% block graph_body %} 4 |

{% trans %}Elapsed Time{% endtrans %}

5 |

{% trans %}Average time to complete backup.{% endtrans %}

6 | {{ chartkick.line_chart(data.elapsedtime) }} 7 | {% endblock %} 8 | -------------------------------------------------------------------------------- /rdiffweb/templates/graphs_files.html: -------------------------------------------------------------------------------- 1 | {% extends 'graphs.html' %} 2 | {% import 'include/chartkick.html' as chartkick with context %} 3 | {% block graph_body %} 4 |

{% trans %}File count{% endtrans %}

5 |

{% trans %}Number of files excluding history data.{% endtrans %}

6 | {{ chartkick.line_chart(data.filecount) }} 7 | {% endblock %} 8 | -------------------------------------------------------------------------------- /rdiffweb/templates/graphs_errors.html: -------------------------------------------------------------------------------- 1 | {% extends 'graphs.html' %} 2 | {% import 'include/chartkick.html' as chartkick with context %} 3 | {% block graph_body %} 4 |

{% trans %}Errors{% endtrans %}

5 |

{% trans %}Cumulative number of errors by period of time.{% endtrans %}

6 | {{ chartkick.line_chart(data.errors, colors=["#dc3912"]) }} 7 | {% endblock %} 8 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Reporting a Vulnerability 2 | 3 | If you discover a security vulnerability in Rdiffweb please disclose it via [our huntr page](https://huntr.dev/repos/ikus060/rdiffweb). Bounty eligibility, CVE assignment, response times and past reports are all there. 4 | 5 | You may alos contact-us directly by email `info@ikus-soft.com` 6 | 7 | Thank you for improving the security of Rdiffweb. 8 | -------------------------------------------------------------------------------- /rdiffweb/core/tests/test_publickey_ssh_rsa.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDDYeMPTCnRLFaLeSzsn++RD5jwuuez65GXrt9g7RYUqJka66cn7zHUhjDWx15fyEM3ikHGbmmWEP2csq11YCtvaTaz2GAnwcFNdt2NF0KGHMbE56Xq0eCkj1FCait/UyRBqkaFItYAoBdj4War9Xt+S5sV8qc5/TqTeku4Kg6ZBJRFCDHy6nR8Xf+tXiBrlfCnXvxamDI5kFP0B+npuBv+M4TjKFvwn5W8zYPPTEznilWnGvJFS71XwsOD/yHBGQb/Jz87aazNAeCznZRAJxfecJhgeChGZcGnXRAAdEeMbRyilYWaNquIpwrbNFElFlVf41EoDBk6woB8TeG0XFfz ikus060@ikus060-t530 2 | -------------------------------------------------------------------------------- /rdiffweb/templates/graphs_sizes.html: -------------------------------------------------------------------------------- 1 | {% extends 'graphs.html' %} 2 | {% import 'include/chartkick.html' as chartkick with context %} 3 | {% block graph_body %} 4 |

{% trans %}Source file size{% endtrans %}

5 |

{% trans %}Repository size excluding history data.{% endtrans %}

6 | {{ chartkick.line_chart(data.sourcefilesize) }} 7 |

{% trans %}Increment file size{% endtrans %}

8 | {{ chartkick.line_chart(data.filesize) }} 9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /doc/index.rst: -------------------------------------------------------------------------------- 1 | .. Rdiffweb documentation master file, created by 2 | sphinx-quickstart on Thu Apr 8 21:24:28 2021. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to Rdiffweb's documentation! 7 | ==================================== 8 | 9 | .. toctree:: 10 | introduction 11 | quickstart 12 | installation 13 | configuration 14 | hardening 15 | usage 16 | faq 17 | development 18 | api 19 | -------------------------------------------------------------------------------- /rdiffweb/templates/graphs_activities.html: -------------------------------------------------------------------------------- 1 | {% extends 'graphs.html' %} 2 | {% import 'include/chartkick.html' as chartkick with context %} 3 | {% set active_graph_page='activities' %} 4 | {% block graph_body %} 5 |

{% trans %}Activities{% endtrans %}

6 |

{% trans %}Cumulative number of new, deleted and changed files by period of time.{% endtrans %}

7 | {% set height=(22 * limit)|string + "px" %} 8 | {{ chartkick.bar_chart(data.activities, stacked=True, height=height) }} 9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /rdiffweb/templates/message.html: -------------------------------------------------------------------------------- 1 | {% for message, level in get_flashed_messages() %} 2 | 12 | {% endfor %} 13 | -------------------------------------------------------------------------------- /debian/rdiffweb.postrm: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | case "${1}" in 6 | purge) 7 | # Remove default session directory 8 | rm -rf /var/lib/rdiffweb 9 | 10 | # Remove symlink 11 | rm -f /usr/lib/python3/dist-packages/rdiffweb/static/js/chart.min.js 12 | rm -f /usr/lib/python3/dist-packages/rdiffweb/static/js/chartkick.min.js 13 | ;; 14 | 15 | remove|upgrade|failed-upgrade|abort-install|abort-upgrade|disappear) 16 | 17 | ;; 18 | 19 | *) 20 | echo "postrm called with unknown argument \`${1}'" >&2 21 | exit 1 22 | ;; 23 | esac 24 | 25 | #DEBHELPER# 26 | 27 | exit 0 28 | -------------------------------------------------------------------------------- /rdiffweb/templates/error_page_default.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | {% block title %}{{ status }}{% endblock %} 3 | {% block body %} 4 |
5 |
6 |
7 |

{% trans %}Oops!{% endtrans %}

8 |

{{ status }}

9 |
10 |

{% trans %}Sorry, an error has occured.{% endtrans %}

11 |

{{ message }}

12 |
13 |
14 | {% if traceback %}
{{ traceback }}
{% endif %} 15 |
16 | {% endblock %} 17 | -------------------------------------------------------------------------------- /rdiffweb/templates/admin_user_new.html: -------------------------------------------------------------------------------- 1 | {% extends 'admin.html' %} 2 | {% set admin_nav_active="users" %} 3 | {% block title %} 4 | {% trans %}Add user{% endtrans %} 5 | {% endblock %} 6 | {% block content %} 7 |
8 |

{% trans %}Add user{% endtrans %}

9 |
10 | {{ form }} 11 | 12 | {% trans %}Cancel{% endtrans %} 13 |
14 |
15 | {% endblock content %} 16 | -------------------------------------------------------------------------------- /rdiffweb/templates/email_password_changed.html: -------------------------------------------------------------------------------- 1 | {% extends 'email_layout.html' %} 2 | {% block title %} 3 | {% trans %}Password changed{% endtrans %} 4 | {% endblock title %} 5 | {% block body %} 6 |

7 | {% trans username=(user.fullname or user.username) %}Hey {{ username }},{% endtrans %} 8 |

9 |

{% trans %}You recently changed the password associated with your {{ header_name }} account.{% endtrans %}

10 |

11 | {% trans %}If you did not make this change and believe your account has been compromised, please contact your administrator.{% endtrans %} 12 |

13 | {% endblock body %} 14 | -------------------------------------------------------------------------------- /rdiffweb/core/tests/session_statistics.2014-11-02T09:16:43-05:00.data: -------------------------------------------------------------------------------- 1 | StartTime 1414937803.00 (Sun Nov 2 09:16:43 2014) 2 | EndTime 1414937764.82 (Sun Nov 2 09:16:04 2014) 3 | ElapsedTime -38.18 (59 minutes 21.82 seconds) 4 | SourceFiles 14 5 | SourceFileSize 3666973 (3.50 MB) 6 | MirrorFiles 13 7 | MirrorFileSize 30242 (29.5 KB) 8 | NewFiles 1 9 | NewFileSize 3636731 (3.47 MB) 10 | DeletedFiles 0 11 | DeletedFileSize 0 (0 bytes) 12 | ChangedFiles 1 13 | ChangedSourceSize 0 (0 bytes) 14 | ChangedMirrorSize 0 (0 bytes) 15 | IncrementFiles 2 16 | IncrementFileSize 0 (0 bytes) 17 | TotalDestinationSizeChange 3636731 (3.47 MB) 18 | Errors 0 19 | -------------------------------------------------------------------------------- /rdiffweb/core/tests/test_publickey_ssh_dsa.pub: -------------------------------------------------------------------------------- 1 | ssh-dss AAAAB3NzaC1kc3MAAACBAM8gRuUD+MFPypUVnq/sOXOnUN7RxXaJPZ2EunJ/kDUMc+e4wHtZCAH+NDrJN+ZnXFJ/OWEv5Pgd7rXECOT7TLI1Okpby8u2qEDiytNtkuK/vlvn95V4a3LpDhrEmILK5XWdaLGU9g5YJJXtwZLo4/HjBdAYrvmGst2RLc4qhLD7AAAAFQDAOU8BzRNU9TAeYAoxTVwD/o6lZQAAAIBJnAYfJQqBDQg+WCPafQUPQBBN7cD4a2Al1gQdbVh62QRsNlb9RXbIHoeGs5F++MXwHe3gg2HzXP7kj/fzOlMh197w9VDZW6Ywko61FA+urqT0xzI5oYlSPrcYM0Lkco1njQ4FddZD212eq/vlpp05CBla+iyFjbUmDGhy2BVdhQAAAIBAr9XPKh08XbBrhXR1gbgYVpfnc3JddDdKPNVarnl4k1vfxG2mGpAJMkc79y2KduuQKJXWjYwZur8ZdJZFGM43RRQoKarIv8fAMx+FtwQaxnzQu3gHT2cCLXtFVzTIWOL87qdf0haOXC6n6dUUBtYEZ0iUTvkM4cro4K7M7d3wOg== ikus060@ikus060-t530 2 | -------------------------------------------------------------------------------- /rdiffweb/templates/email_access_token_added.html: -------------------------------------------------------------------------------- 1 | {% extends 'email_layout.html' %} 2 | {% block title %} 3 | {% trans %}A new access token has been created{% endtrans %} 4 | {% endblock title %} 5 | {% block body %} 6 |

7 | {% trans username=(user.fullname or user.username) %}Hey {{ username }},{% endtrans %} 8 |

9 |

10 | {% trans %}A new access token, named "{{ name }}", has been created.{% endtrans %} 11 |

12 |

13 | {% trans %}If you did not make this change and believe your account has been compromised, please contact your administrator.{% endtrans %} 14 |

15 | {% endblock body %} 16 | -------------------------------------------------------------------------------- /rdiffweb/core/tests/rdiff-backup-data/session_statistics.2014-11-02T09:16:43-05:00.data: -------------------------------------------------------------------------------- 1 | StartTime 1414937803.00 (Sun Nov 2 09:16:43 2014) 2 | EndTime 1414937764.82 (Sun Nov 2 09:16:04 2014) 3 | ElapsedTime -38.18 (59 minutes 21.82 seconds) 4 | SourceFiles 14 5 | SourceFileSize 3666973 (3.50 MB) 6 | MirrorFiles 13 7 | MirrorFileSize 30242 (29.5 KB) 8 | NewFiles 1 9 | NewFileSize 3636731 (3.47 MB) 10 | DeletedFiles 0 11 | DeletedFileSize 0 (0 bytes) 12 | ChangedFiles 1 13 | ChangedSourceSize 0 (0 bytes) 14 | ChangedMirrorSize 0 (0 bytes) 15 | IncrementFiles 2 16 | IncrementFileSize 0 (0 bytes) 17 | TotalDestinationSizeChange 3636731 (3.47 MB) 18 | Errors 0 19 | -------------------------------------------------------------------------------- /rdiffweb/templates/email_changed.html: -------------------------------------------------------------------------------- 1 | {% extends 'email_layout.html' %} 2 | {% block title %} 3 | {% trans %}Email address changed{% endtrans %} 4 | {% endblock title %} 5 | {% block body %} 6 |

7 | {% trans username=(user.fullname or user.username) %}Hey {{ username }},{% endtrans %} 8 |

9 |

10 | {% trans %}You recently changed the email address associated with your {{ header_name }} account.{% endtrans %} 11 |

12 |

13 | {% trans %}If you did not make this change and believe your account has been compromised, please contact your administrator.{% endtrans %} 14 |

15 | {% endblock body %} 16 | -------------------------------------------------------------------------------- /rdiffweb/templates/email_repo_deleted.html: -------------------------------------------------------------------------------- 1 | {% extends 'email_layout.html' %} 2 | {% block title %} 3 | {% trans %}Repository deleted{% endtrans %} 4 | {% endblock title %} 5 | {% block body %} 6 |

7 | {% trans username=(user.fullname or user.username) %}Hey {{ username }},{% endtrans %} 8 |

9 |

10 | {% trans %}You receive this e-mail to inform you that your deposit named {{ repo_path }} has been deleted from your account.{% endtrans %} 11 |

12 |

13 | {% trans %}If you did not make this change and believe your account has been compromised, please contact your administrator.{% endtrans %} 14 |

15 | {% endblock body %} 16 | -------------------------------------------------------------------------------- /rdiffweb/templates/email_authorizedkey_added.html: -------------------------------------------------------------------------------- 1 | {% extends 'email_layout.html' %} 2 | {% block title %} 3 | {% trans %}A new SSH Key has been added{% endtrans %} 4 | {% endblock title %} 5 | {% block body %} 6 |

7 | {% trans username=(user.fullname or user.username) %}Hey {{ username }},{% endtrans %} 8 |

9 |

10 | {% trans %}A new SSH Key, titled "{{ comment }}" with fingerprint "{{ fingerprint }}" has been created in your account.{% endtrans %} 11 |

12 |

13 | {% trans %}If you did not make this change and believe your account has been compromised, please contact your administrator.{% endtrans %} 14 |

15 | {% endblock body %} 16 | -------------------------------------------------------------------------------- /rdiffweb/templates/email_repo_added.html: -------------------------------------------------------------------------------- 1 | {% extends 'email_layout.html' %} 2 | {% block title %} 3 | {% trans %}New Repository detected{% endtrans %} 4 | {% endblock title %} 5 | {% block body %} 6 |

7 | {% trans username=(user.fullname or user.username) %}Hey {{ username }},{% endtrans %} 8 |

9 |

10 | {% trans %}You are receiving this email to inform you that a new repository, named "{{ repo_path }}", has been added to your account.{% endtrans %} 11 |

12 |

13 | {% trans %}If you did not make this change and believe your account has been compromised, please contact your administrator.{% endtrans %} 14 |

15 | {% endblock body %} 16 | -------------------------------------------------------------------------------- /rdiffweb/templates/email_latest.html: -------------------------------------------------------------------------------- 1 | {% extends 'email_layout.html' %} 2 | {% block title %} 3 | {% trans %}Upgrade available for {{ header_name }}{% endtrans %} 4 | {% endblock title %} 5 | {% block body %} 6 |

7 | {% trans username=(user.fullname or user.username) %}Hey {{ username }},{% endtrans %} 8 |

9 |

10 | {% trans %}A new version of {{ header_name }} backup management system is now available for upgrade. It's recommend that you upgrade your server to take advantage of the enhancements and bug fixes included in this version.{% endtrans %} 11 |

12 |

13 | {% trans %}If you don't want to be notify about this. You need to review your server configuration.{% endtrans %} 14 |

15 | {% endblock body %} 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | *.log 3 | 4 | # Packages 5 | *.egg 6 | *.egg-info 7 | dist 8 | build 9 | eggs 10 | parts 11 | bin 12 | var 13 | sdist 14 | develop-eggs 15 | .installed.cfg 16 | .eggs 17 | .sass-cache 18 | .venv 19 | rdw.db 20 | rdw.db-shm 21 | rdw.db-wal 22 | 23 | # Installer logs 24 | pip-log.txt 25 | 26 | # Unit test / coverage reports 27 | .coverage 28 | .tox 29 | coverage*.xml 30 | xunit*.xml 31 | 32 | #Mr Developer 33 | .mr.developer.cfg 34 | /node_modules/ 35 | .project 36 | .externalToolBuilders 37 | .settings 38 | /ez_setup 39 | /rdwtest.conf 40 | /rdwtest.db 41 | /runtest.py 42 | .pydevproject 43 | /rdw.db 44 | /testrdw.conf 45 | .tox 46 | .pydevproject 47 | /htmlcov/ 48 | /.env/ 49 | /.pybuild/ 50 | /.pytest_cache/ 51 | /.coverage* 52 | /.vscode 53 | .DS_Store 54 | -------------------------------------------------------------------------------- /rdiffweb/templates/prefs_mfa.html: -------------------------------------------------------------------------------- 1 | {% extends 'prefs.html' %} 2 | {% from 'include/panel.html' import panel %} 3 | {% set active_panelid='mfa' %} 4 | {% block panel %} 5 |
6 |

{% trans %}Two-Factor Authentication{% endtrans %}

7 |
8 |

9 | {% trans %}You can enhance the security of your account by enabling two-factor authentication (2FA).{% endtrans %} 10 | {% trans %}When enabled, a verification code is sent to your email address each time you log in from a new location to verify that it is you.{% endtrans %} 11 |

12 | {% include 'message.html' %} 13 |
14 |
15 | {{ form }} 16 |
17 |
18 | {% endblock %} 19 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # rdiff-backup is compatible with Debian bookworm python 3.12 2 | FROM python:3.12-bookworm AS base 3 | 4 | LABEL author="Patrik Dufresne " 5 | 6 | EXPOSE 8080 7 | 8 | VOLUME ["/etc/rdiffweb", "/backups"] 9 | 10 | ENV RDIFFWEB_SERVER_HOST=0.0.0.0 11 | 12 | 13 | RUN set -x; \ 14 | apt -y update && \ 15 | apt install -y --no-install-recommends librsync-dev python3-pylibacl python3-pyxattr && \ 16 | rm -Rf /var/lib/apt/lists/* 17 | 18 | COPY . /src/ 19 | 20 | RUN set -x; \ 21 | cd /src/ && \ 22 | pip3 install --no-cache-dir rdiff-backup==2.2.6 23 | 24 | FROM base AS test 25 | RUN set -x; \ 26 | cd /src/ && \ 27 | pip3 install --no-cache-dir . ".[test]" && \ 28 | pytest && \ 29 | rm -Rf /tmp/* /src/ 30 | 31 | CMD ["/usr/local/bin/rdiffweb"] 32 | -------------------------------------------------------------------------------- /extras/nginx/rdiffweb_root.conf: -------------------------------------------------------------------------------- 1 | # rdiffweb, A web interface to rdiff-backup repositories 2 | # Copyright (C) 2012-2025 rdiffweb contributors 3 | # 4 | # Nginx reverse proxy configuration. 5 | # This configuration allow you to serve rdiffweb as http://example.com/ 6 | # 7 | 8 | location / { 9 | # Define proxy header for cherrypy logging. 10 | proxy_set_header Host $host; 11 | proxy_set_header X-Real-IP $remote_addr; 12 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 13 | proxy_set_header X-Forwarded-Host $server_name; 14 | # Proxy 15 | proxy_pass http://127.0.0.1:8080/; 16 | } 17 | 18 | location /static/ { 19 | alias /usr/lib/python3/dist-packages/rdiffweb/static/; 20 | } 21 | -------------------------------------------------------------------------------- /rdiffweb/templates/include/messages.html: -------------------------------------------------------------------------------- 1 | {% from 'include/table.html' import table %} 2 | {% macro messages(data_url) -%} 3 |
4 | {% set columns = [ 5 | {'name':'id', 'visible':False}, 6 | {'name':'author', 'visible':False}, 7 | {'name':'date', 'visible':False}, 8 | {'name':'type', 'visible':False }, 9 | {'name':'body', 'visible':False}, 10 | {'name':'changes', 'title':_('Activity'), 'render':'message_body' }] %} 11 | {{ table(data_url, 12 | columns=columns, 13 | order=[[ 5, 'desc' ]], 14 | state_save=False, 15 | searching=False, 16 | empty_message=_('No activity'), 17 | info_message=_('Displaying _START_-_END_ of _TOTAL_ most recent activities'), 18 | paging=True, 19 | layout="audit", 20 | page_length=10) }} 21 |
22 | {% endmacro %} 23 | -------------------------------------------------------------------------------- /rdw.conf: -------------------------------------------------------------------------------- 1 | # 2 | # rdw.conf: 3 | # An example configuration file for configuring Rdiffweb server. 4 | # 5 | ############################################################################### 6 | # 7 | # This file is intended to only be an example. 8 | # When the Rdiffweb server starts up, this is where it will look for it. 9 | # 10 | # All lines beginning with a '#' are comments and are intended for you 11 | # to read. All other lines must be `key=value`. 12 | # To get more details about the options available to configure Rdiffweb read 13 | # the man page or run `rdiffweb --help`. 14 | 15 | # Configure web server 16 | server-host=127.0.0.1 17 | server-port=8080 18 | 19 | # Configure default logging 20 | log-level=INFO 21 | log-file=/var/log/rdiffweb.log 22 | log-access-file=/var/log/rdiffweb-access.log 23 | 24 | # Configure default session 25 | rate-limit-dir=/var/lib/rdiffweb/session 26 | -------------------------------------------------------------------------------- /rdiffweb/templates/email_verification_code.html: -------------------------------------------------------------------------------- 1 | {% extends 'email_layout.html' %} 2 | {% block title %} 3 | {% trans %}Your verification code{% endtrans %} 4 | {% endblock title %} 5 | {% block body %} 6 |

7 | {% trans username=(user.fullname or user.username) %}Hey {{ username }},{% endtrans %} 8 |

9 |

10 | {% trans %}To help us make sure it's really you, here's the verification code you'll need to log in:{% endtrans %} 11 |

12 |

13 | {{ code }} 14 |

15 |

16 | {% trans %}If this wasn't you logging in, and you use a password to log in, please reset your password.{% endtrans %} 17 |

18 |

19 | {% trans %}This code will expire in 1 hour. Once the code expires, you will need to request a new verification code by going through the login procedure again.{% endtrans %} 20 |

21 | {% endblock body %} 22 | -------------------------------------------------------------------------------- /rdiffweb/templates/email_mfa.html: -------------------------------------------------------------------------------- 1 | {% extends 'email_layout.html' %} 2 | {% block title %} 3 | {% if user.mfa %} 4 | {% trans %}Two-Factor Authentication turned on{% endtrans %} 5 | {% else %} 6 | {% trans %}Two-Factor Authentication turned off{% endtrans %} 7 | {% endif %} 8 | {% endblock title %} 9 | {% block body %} 10 |

11 | {% trans username=(user.fullname or user.username) %}Hey {{ username }},{% endtrans %} 12 |

13 |

14 | {% if user.mfa %} 15 | {% trans %}Your {{ header_name }} Account is now protected with Two-Factor Authentication. When you sign in on a new or untrusted device, you'll need your second factor to verify your identity.{% endtrans %} 16 | {% else %} 17 | {% trans %}Your {{ header_name }} account is no longer protected with Two-Factor Authentication. You don't need your second factor to sign in.{% endtrans %} 18 | {% endif %} 19 |

20 | {% endblock body %} 21 | -------------------------------------------------------------------------------- /rdiffweb/core/tests/rdiff-backup-data/file_statistics.2014-11-05T16:05:07-05:00.data: -------------------------------------------------------------------------------- 1 | # Format of each line in file statistics file: 2 | # Filename Changed SourceSize MirrorSize IncrementSize 3 | . 0 0 0 NA 4 | (@vec) {càraçt#èrë} $épêcial 0 286 143 NA 5 | Char ;090 to quote 0 0 0 NA 6 | Char ;090 to quote/Data 0 21 21 NA 7 | Char ;090 to quote/Untitled Testcase.doc 0 14848 14848 NA 8 | DIR� 0 0 0 NA 9 | DIR�/Data 0 10 10 NA 10 | Fichier @ 0 13 13 NA 11 | Fichier avec non asci char �velyne M�re.txt 0 18 18 NA 12 | Revisions 1 0 0 NA 13 | Revisions/Data 1 9 9 72 14 | Répertoire (@vec) {càraçt#èrë} $épêcial 0 0 0 NA 15 | Répertoire (@vec) {càraçt#èrë} $épêcial/Untitled Testcase.doc 0 14848 14848 NA 16 | Répertoire Existant 0 0 0 NA 17 | Répertoire Existant/Untitled Empty Text File 0 0 0 NA 18 | Répertoire Existant/Untitled Empty Text File 2 0 0 0 NA 19 | test\\test 0 0 0 NA 20 | test\\test/some data 0 226 226 NA 21 | 이루마 YIRUMA - River Flows in You.mp3 0 3636731 3636731 NA 22 | -------------------------------------------------------------------------------- /rdiffweb/__init__.py: -------------------------------------------------------------------------------- 1 | # rdiffweb, A web interface to rdiff-backup repositories 2 | # Copyright (C) 2012-2025 rdiffweb contributors 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | from importlib.metadata import distribution 17 | 18 | try: 19 | __version__ = distribution("rdiffweb").version 20 | except Exception: 21 | __version__ = 'DEV' 22 | -------------------------------------------------------------------------------- /rdiffweb/tests/test_app.py: -------------------------------------------------------------------------------- 1 | # rdiffweb, A web interface to rdiff-backup repositories 2 | # Copyright (C) 2012-2025 rdiffweb contributors 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | 18 | import rdiffweb.test 19 | 20 | 21 | class AppTest(rdiffweb.test.WebCase): 22 | def test_version(self): 23 | """Verify return value of version.""" 24 | self.assertIsNotNone(self.app.version) 25 | -------------------------------------------------------------------------------- /rdiffweb/core/tests/test_authorized_keys: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDFqrQ0aYuT6l40kerv1W2OlxXxgsYB60IuWZTtMptu77yYMy6iDew38z/hL8kc+unp4q4qvqyMVEy/evp0HYOxl5VJZfuJgTjFQLeZE46iZ5iNOMDiwQGN4GHlTLE1LVTMjk+Dc1w7EagWtc3wizNsMOVn/vefT4LkM78GEv040ze3LItSt2PSwPZ7f8jpEWRpAMmDlYGX2+UfmgOQEMdSPgP3kkBufmQcR+4pcSo41GpXc5oQ0zxIRfmDQFlObOWTuuQ/U1G4Tiz6i+/4zZgLKuve7LIXbdYjs/uFQOy2zYyTg47aEpgu3JZKQ3UGKUfJz5lsuF1I30vw8qNcrWkh root@thymara 2 | command="mycommand arg" ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDfQmZfSBr/gF1WVFD4Je/JSSnnUrtzeZ0Lz2/XoPasEfUjFAGl+2HaSRNFuxjw9kNjfApMrfPinYKGfFYpbn08yDbPjuFkQ4V2szrVaf4KKfwtPQxSPyT8hIQQ9pylFqG/wRr18KmIHYyVCUu+rvLiF+epSp7uCkLjRdy2VNaZMBn3TiHdpLMQms0x2N8G76UbO/zGhrpx/BLvHLp2r9t8bL0H0cT2j7LH4OaeVtU/U7/1S9Km/1afMqz6yooLfjDAISWX8IVTAjdEkNBXeiZf9AEGUscqGTgrfDs8GlC7Uy9WIxPhdy81MM8OSntZ+4kDQyzLlVqFo/BrZvMCSTlX root@mercor 3 | 4 | 5 | no-user-rc ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAUGK root@kalo 6 | command="mycommand arg" ssh-rsa AAAAB3NzaC1yc2EAAAADAQSTlX root@ranculos 7 | tunnel="n",no-X11-forwarding ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAUGK 8 | -------------------------------------------------------------------------------- /rdiffweb/templates/mfa.html: -------------------------------------------------------------------------------- 1 | {% extends 'login.html' %} 2 | {% block content %} 3 |
4 |
5 | 21 |
22 |
23 | {% endblock %} 24 | -------------------------------------------------------------------------------- /rdiffweb/templates/include/panel.html: -------------------------------------------------------------------------------- 1 | {% macro panel(title, description=None, show_submit_area=False) -%} 2 |

{{ title }}

3 | {% if description %}

{{ description }}

{% endif %} 4 |
5 |
{{ caller() }}
6 | {% if show_submit_area %} 7 | 22 | {% endif %} 23 |
24 | {%- endmacro %} 25 | -------------------------------------------------------------------------------- /rdiffweb/templates/prefs_general.html: -------------------------------------------------------------------------------- 1 | {% extends 'prefs.html' %} 2 | {% from 'include/panel.html' import panel %} 3 | {% set active_panelid='general' %} 4 | {% block panel %} 5 | {% include 'message.html' %} 6 | {# Panel to set user info. #} 7 | {% call panel(title=_("Account settings"), description=_("General information about your account.")) %} 8 |
9 |
10 | {{ profile_form }} 11 |
12 |
13 | {% endcall %} 14 | {# Panel to change password. #} 15 | {% call panel(title=_("Password"), description=_("Change your current password")) %} 16 |
17 |
18 | {{ password_form }} 19 |
20 |
21 | {% endcall %} 22 | {# Panel to refresh repository list. #} 23 | {% call panel(title=_("Refresh")) %} 24 |
25 |
26 | {{ refresh_form }} 27 |
28 |
29 | {% endcall %} 30 | {% endblock %} 31 | -------------------------------------------------------------------------------- /rdiffweb/core/model/__init__.py: -------------------------------------------------------------------------------- 1 | # rdiffweb, A web interface to rdiff-backup repositories 2 | # Copyright (C) 2012-2025 rdiffweb contributors 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | from ._message import Message # noqa 18 | from ._repo import RepoObject # noqa 19 | from ._session import DbSession, SessionObject # noqa 20 | from ._sshkey import SshKey, sshkey_fingerprint_index # noqa 21 | from ._token import Token # noqa 22 | from ._user import UserObject, user_username_index # noqa 23 | -------------------------------------------------------------------------------- /rdiffweb/tools/errors.py: -------------------------------------------------------------------------------- 1 | # rdiffweb, A web interface to rdiff-backup repositories 2 | # Copyright (C) 2012-2025 rdiffweb contributors 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | import sys 17 | 18 | import cherrypy 19 | 20 | 21 | def handle_exception(error_table): 22 | t = sys.exc_info()[0] 23 | code = error_table.get(t, 500) 24 | cherrypy.serving.request.error_response = cherrypy.HTTPError(code).set_response 25 | 26 | 27 | cherrypy.tools.errors = cherrypy.Tool('before_error_response', handle_exception, priority=90) 28 | -------------------------------------------------------------------------------- /rdiffweb/controller/tests/test_page_admin_sysinfo.py: -------------------------------------------------------------------------------- 1 | # rdiffweb, A web interface to rdiff-backup repositories 2 | # Copyright (C) 2012-2025 rdiffweb contributors 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | 18 | import rdiffweb.test 19 | 20 | 21 | class AdminSysinfoTest(rdiffweb.test.WebCase): 22 | login = True 23 | 24 | def test_sysinfo(self): 25 | self.getPage("/admin/sysinfo/") 26 | self.assertStatus(200) 27 | self.assertInBody("Operating System Info") 28 | self.assertInBody("Python Info") 29 | -------------------------------------------------------------------------------- /rdiffweb/templates/admin_sysinfo.html: -------------------------------------------------------------------------------- 1 | {% extends 'admin.html' %} 2 | {% block title %} 3 | {% trans %}System Info{% endtrans %} 4 | {% endblock %} 5 | {% set admin_nav_active="sysinfo" %} 6 | {% block content %} 7 | {% set section_items = [ 8 | (_('Application Version'), [(_('Core Version'), version)] + plugins|d([]) ), 9 | (_('Application Config'), cfg.items()), 10 | (_('System usage'), hwinfo), 11 | (_('Operating System Info'), osinfo), 12 | (_('Dependencies Version'), ldapinfo), 13 | (_('Python Info'), pyinfo),]%} 14 | {% for section_name, items in section_items %} 15 |

{{ section_name }}

16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | {% for k, v in items %} 26 | 27 | 28 | 29 | 30 | {% endfor %} 31 | 32 |
DescriptionValue
{{k}}{{v}}
33 |
34 | {% endfor %} 35 | {% endblock %} 36 | -------------------------------------------------------------------------------- /extras/nginx/rdiffweb.conf: -------------------------------------------------------------------------------- 1 | # rdiffweb, A web interface to rdiff-backup repositories 2 | # Copyright (C) 2012-2025 rdiffweb contributors 3 | # 4 | # Nginx reverse proxy configuration. 5 | # This configuration allow you to serve rdiffweb as http://example.com/rdiffweb/ 6 | # 7 | 8 | location /rdiffweb { 9 | rewrite ^([^\?#]*/)([^\?#\./]+)([\?#].*)?$ $1$2/$3 permanent; 10 | # substitute with rdiffweb url 11 | proxy_pass http://127.0.0.1:18080/; 12 | # for https 13 | #proxy_redirect http://example.com https://example.com/rdiffweb; 14 | proxy_set_header Host $host; 15 | proxy_set_header X-Real-IP $remote_addr; 16 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 17 | proxy_set_header X-Forwarded-Proto $scheme; 18 | proxy_set_header Accept-Encoding ""; 19 | # rewrite internal links 20 | sub_filter 'href="/' 'href="/rdiffweb/'; 21 | sub_filter 'src="/' 'src="/rdiffweb/'; 22 | sub_filter 'action="/' 'action="/rdiffweb/'; 23 | sub_filter "url(\'/static" "url(\'/rdiffweb/static"; 24 | sub_filter 'd3.csv("/' 'd3.csv("/rdiffweb/'; 25 | sub_filter_types "*"; 26 | sub_filter_once off; 27 | } 28 | -------------------------------------------------------------------------------- /rdiffweb/templates/include/timerange.html: -------------------------------------------------------------------------------- 1 | {% macro timerange(ranges=[1,7,30], limit=0, qs='limit') -%} 2 | 24 | {% endmacro %} 25 | -------------------------------------------------------------------------------- /rdiffweb/templates/prefs.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | {% from 'components/nav.html' import nav_list %} 3 | {% set active_page='settings' %} 4 | {% block title %} 5 | {% trans %}User profile{% endtrans %} 6 | {% endblock %} 7 | {% block body %} 8 |
9 |

{% trans %}User profile{% endtrans %}

10 | 11 |
12 |
13 | {% set nav_items = [ 14 | (_('General'), url_for('prefs','general'), active_panelid=='general'), 15 | (_('Notifications'), url_for('prefs', 'notification'), active_panelid=='notification'), 16 | (_('SSH Keys'), url_for('prefs','sshkeys'), active_panelid=='sshkeys'), 17 | (_('Access Tokens'), url_for('prefs','tokens'), active_panelid=='tokens'), 18 | (_('Two-Factor Authentication'), url_for('prefs','mfa'), active_panelid=='mfa'), 19 | (_('Active Sessions'), url_for('prefs','session'), active_panelid=='session'), 20 | ] %} 21 | {{ nav_list(nav_items) }} 22 |
23 |
24 | 25 | {% block panel %}{% endblock %} 26 |
27 |
28 |
29 | {% endblock %} 30 | -------------------------------------------------------------------------------- /rdiffweb/templates/admin.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% from 'include/table.html' import table %} 3 | {% set active_page='admin' %} 4 | {% block title %} 5 | {% trans %}Admin area{% endtrans %} 6 | {% endblock title %} 7 | {% block body %} 8 |
9 | {# Navigation bar for Administration #} 10 | {% set admin_nav_items = [ 11 | ('', _('Overview'), not admin_nav_active), 12 | ('users', _('Users'), admin_nav_active=='users'), 13 | ('repos', _('Repositories'), admin_nav_active=='repos'), 14 | ('session', _('User Sessions'), admin_nav_active=='session'), 15 | ('activity', _('Activity'), admin_nav_active=='activity'), 16 | ('logs', _('System Logs'), admin_nav_active=='logs'), 17 | ('sysinfo', _('System Info'), admin_nav_active=='sysinfo')] %} 18 | 26 | {% include "message.html" %} 27 | {% block content %} 28 | {% endblock content %} 29 |
30 | {% endblock body %} 31 | -------------------------------------------------------------------------------- /debian/rdiffweb.postinst: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | case "${1}" in 6 | configure) 7 | # Encforce permissions on /etc/rdiffweb 8 | mkdir -p --mode 750 /etc/rdiffweb 9 | 10 | # Create default session directory 11 | mkdir -p /var/lib/rdiffweb/session 12 | 13 | # Create symbolic link to Chart.js 14 | if [ -f /usr/share/javascript/chart.js/Chart.bundle.min.js ] 15 | then 16 | ln -sf /usr/share/javascript/chart.js/Chart.bundle.min.js /usr/lib/python3/dist-packages/rdiffweb/static/js/chart.min.js 17 | else 18 | ln -sf /usr/share/javascript/chart.js/Chart.min.js /usr/lib/python3/dist-packages/rdiffweb/static/js/chart.min.js 19 | fi 20 | 21 | # Create symbolic link to chartkick.js 22 | if [ -f /usr/share/javascript/chartkick/chartkick.min.js ] 23 | then 24 | ln -sf /usr/share/javascript/chartkick/chartkick.min.js /usr/lib/python3/dist-packages/rdiffweb/static/js/chartkick.min.js 25 | else 26 | ln -sf /usr/share/libjs-chartkick.js/chartkick.js /usr/lib/python3/dist-packages/rdiffweb/static/js/chartkick.min.js 27 | fi 28 | ;; 29 | 30 | abort-upgrade|abort-remove|abort-deconfigure) 31 | 32 | ;; 33 | 34 | *) 35 | echo "postinst called with unknown argument \`${1}'" >&2 36 | exit 1 37 | ;; 38 | esac 39 | 40 | #DEBHELPER# 41 | 42 | exit 0 43 | -------------------------------------------------------------------------------- /doc/access_tokens.md: -------------------------------------------------------------------------------- 1 | # Access Tokens 2 | 3 | Introduce in Rdiffweb 2.5.0, access tokens are an alternative to username and password to authenticate with Rdiffweb's API. When Two-Factor Authentication is enabled, Access Tokens are the only available authentication mechanisms available for API access. 4 | 5 | * Access tokens could be used to authenticate with the Rdiffweb API `/api` 6 | * Access tokens are required when two-factor authentication (2FA) is enabled. 7 | 8 | ## Create a personal access token 9 | 10 | You can create as many access tokens as required for your needs. 11 | 12 | 1. Go to **Edit profile > Access Tokens** 13 | 2. Enter a *Name* to uniquely identify your token usage. This can be anything you like as long as it is unique. 14 | 3. Optionally, enter an expiration date. 15 | 4. Click **Create access token** 16 | 5. If successful, a new token will be generated. Make sure to save this token somewhere safe. 17 | 18 | ## Revoke an access token 19 | 20 | You may need to revoke unused access tokens at any time. Any application using the token to authenticate with Rdiffweb API will stop working. 21 | 22 | 1. Go to **Edit profile > Access Tokens** 23 | 2. In the **Active access tokens** area, next to the token, click **Revoke**. 24 | 3. Then confirm revoke operation. 25 | -------------------------------------------------------------------------------- /doc/development.md: -------------------------------------------------------------------------------- 1 | # Development 2 | 3 | This section provide details for those who want to contributes to the development. 4 | 5 | ## Translation 6 | 7 | Reference 8 | 9 | rdiffweb may be translated using `.po` files. This section describe briefly 10 | how to translate rdiffweb. It's not a complete instruction set, it's merely a reminder. 11 | 12 | Extract the strings to be translated: 13 | 14 | tox -e babel_extract 15 | 16 | Create a new translation: 17 | 18 | tox -e babel_init -- --local fr 19 | 20 | Update an existing translation: 21 | 22 | tox -e babel_update -- --local fr 23 | 24 | Compile all existing translation: 25 | 26 | tox -e babel_compile 27 | 28 | ## Running tests 29 | 30 | Rdiffweb is provided with unit tests. To run them, execute a command similar to the following: 31 | 32 | tox -e py3 33 | 34 | ## Generage favicon.ico with imagemagik 35 | 36 | convert -density 256x256 -background transparent favicon.svg -define icon:auto-resize -colors 256 favicon.ico 37 | 38 | ## Documentation 39 | 40 | To generate documentation run `tox -e doc`. 41 | 42 | It generates HTML documentation in folder `dist/html` 43 | 44 | Ref.: 45 | -------------------------------------------------------------------------------- /rdiffweb/controller/page_admin_repos.py: -------------------------------------------------------------------------------- 1 | # rdiffweb, A web interface to rdiff-backup repositories 2 | # Copyright (C) 2012-2025 rdiffweb contributors 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | import cherrypy 18 | 19 | from rdiffweb.controller import Controller 20 | from rdiffweb.core.model import RepoObject 21 | 22 | 23 | @cherrypy.tools.is_admin() 24 | class AdminReposPage(Controller): 25 | @cherrypy.expose 26 | def index(self): 27 | """ 28 | Show all user repositories 29 | """ 30 | params = { 31 | "repos": RepoObject.query.all(), 32 | } 33 | return self._compile_template("admin_repos.html", **params) 34 | -------------------------------------------------------------------------------- /rdiffweb/templates/prefs_notification.html: -------------------------------------------------------------------------------- 1 | {% extends 'prefs.html' %} 2 | {% from 'include/panel.html' import panel %} 3 | {% set active_panelid='notification' %} 4 | {% block panel %} 5 | {% include 'message.html' %} 6 | {% call panel(title=_("Notification settings")) %} 7 |
8 | {% trans %}Notification sent to{% endtrans %} 9 | 14 | {% trans %}To receive notification, configure your email address in your profile.{% endtrans %} 15 |
16 | {% endcall %} 17 | {# Report config #} 18 | {% call panel(title=_("Report"), description=_("Configure how often you want to receive a backup report in your mailbox.")) %} 19 |
20 |
21 | {{ report_form }} 22 |
23 |
24 | {% endcall %} 25 | {# Notification config #} 26 | {% call panel(title=_("Notification"), description=_("Configure when you will be notified if your backup is inactive.")) %} 27 |
28 |
29 | {{ notification_form }} 30 |
31 |
32 | {% endcall %} 33 | {% endblock %} 34 | -------------------------------------------------------------------------------- /rdiffweb/tools/poppath.py: -------------------------------------------------------------------------------- 1 | # rdiffweb, A web interface to rdiff-backup repositories 2 | # Copyright (C) 2012-2025 rdiffweb contributors 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | import cherrypy 18 | 19 | from rdiffweb.core.rdw_helpers import unquote_url 20 | 21 | 22 | def convert_path(): 23 | """ 24 | A tool to convert vpath to path once the handler was found. 25 | 26 | Used to merge the segment of URI into a single parameter denoting the 27 | repository path. 28 | """ 29 | handler = cherrypy.serving.request.handler 30 | if hasattr(handler, 'kwargs'): 31 | handler.kwargs = {'path': b"/".join([unquote_url(segment) for segment in handler.args])} 32 | handler.args = [] 33 | 34 | 35 | cherrypy.tools.poppath = cherrypy.Tool('on_start_resource', convert_path, priority=15) 36 | -------------------------------------------------------------------------------- /rdiffweb/templates/layout_repo.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | {% from 'components/nav.html' import nav_tabs %} 3 | {% block body %} 4 |
5 | 6 |

{{ repo.display_name }}

7 | 8 | {% set nav_items = [ 9 | (_('Files'), url_for('browse', repo), active_repo_page=='browse', 'fa fa-files-o'), 10 | (_('Settings'), url_for('settings', repo), active_repo_page=='settings', 'fa fa-sliders'), 11 | (_('Graphs'), url_for('graphs', 'activities', repo), active_repo_page=='graphs', 'fa fa-area-chart'), 12 | (_('Snapshot Changes'), url_for('stats', repo), active_repo_page=='stats', 'fa fa-list-alt'), 13 | (_('Logs'), url_for('logs', repo), active_repo_page=='logs', 'fa fa-file-text-o'), 14 | ] -%} 15 | {{ nav_tabs(nav_items) }} 16 |
17 |
18 | 19 | {% if repo.status[0] != 'ok' %} 20 | 27 | {% endif %} 28 | {% include 'message.html' %} 29 | {% block content %}{% endblock %} 30 |
31 | {% endblock %} 32 | -------------------------------------------------------------------------------- /rdiffweb/templates/include/session.html: -------------------------------------------------------------------------------- 1 | {# https://developer.mozilla.org/en-US/docs/Web/HTTP/Browser_detection_using_the_user_agent #} 2 | {% macro browser(user_agent) %} 3 | {% if not user_agent %} 4 | {% trans %}Unknown{% endtrans %} 5 | {% elif 'Firefox/' in user_agent and 'Seamonkey/' not in user_agent %} 6 | Firefox 7 | {% elif 'Seamonkey/' in user_agent %} 8 | Seamonkey 9 | {% elif 'Chrome/' in user_agent and 'Chromium/' not in user_agent %} 10 | Chrome 11 | {% elif 'Chromium/' in user_agent %} 12 | Chromium 13 | {% elif 'Safari/' in user_agent and 'Chrome/' not in user_agent and 'Chromium/' not in user_agent %} 14 | Safari 15 | {% elif 'OPR/' in user_agent or 'Opera/' in user_agent %} 16 | Opera 17 | {% elif '; MSIE xyz;' in user_agent %} 18 | Internet Explorer 19 | {% elif 'Trident/7.0;' in user_agent %} 20 | Internet Explorer 21 | {% elif 'minarca/' in user_agent %} 22 | Minarca 23 | {% else %} 24 | {% trans %}Unknown{% endtrans %} 25 | {% endif %} 26 | {% endmacro %} 27 | {% macro os(user_agent) %} 28 | {% if not user_agent %} 29 | {% trans %}Unknown{% endtrans %} 30 | {% elif 'Linux' in user_agent %} 31 | Linux 32 | {% elif 'X11' in user_agent %} 33 | Unix 34 | {% elif 'Mac' in user_agent or 'Darwin' in user_agent %} 35 | MacOS 36 | {% elif 'Windows' in user_agent %} 37 | Windows 38 | {% else %} 39 | {% trans %}Unknown{% endtrans %} 40 | {% endif %} 41 | {% endmacro %} 42 | -------------------------------------------------------------------------------- /rdiffweb/templates/include/chartkick.html: -------------------------------------------------------------------------------- 1 | {% set ns = namespace(chart_id=0) %} 2 | {% macro chartkick(name, data, height, options) %} 3 | {% if ns.chart_id == 0 %} 4 | 5 | 6 | {% endif %} 7 | {% set ns.chart_id = ns.chart_id + 1 %} 8 |
10 | {% trans %}Loading...{% endtrans %} 11 |
12 | 21 | {% endmacro %} 22 | {% macro line_chart(data, height="300px") %} 23 | {{ chartkick('LineChart', data, height, kwargs)}} 24 | {% endmacro %} 25 | {% macro bar_chart(data, height="300px") %} 26 | {{ chartkick('BarChart', data, height, kwargs)}} 27 | {% endmacro %} 28 | {% macro column_chart(data, height="300px") %} 29 | {{ chartkick('ColumnChart', data, height, kwargs)}} 30 | {% endmacro %} 31 | {% macro pie_chart(data, height="300px") %} 32 | {{ chartkick('PieChart', data, height, kwargs)}} 33 | {% endmacro %} 34 | -------------------------------------------------------------------------------- /rdiffweb/templates/graphs.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout_repo.html' %} 2 | {% from 'include/timerange.html' import timerange %} 3 | {% set active_page='repo' %} 4 | {% set active_repo_page='graphs' %} 5 | {% block title %} 6 | {% trans %}Graphs{% endtrans %} 7 | {% endblock title %} 8 | {% block content %} 9 |
10 | {% include 'message.html' %} 11 | 12 | {% set graph_nav_bar = [ 13 | ('activities', _('Activities'), url_for('graphs', 'activities', repo, limit=limit)), 14 | ('files', _('File count'), url_for('graphs', 'files', repo, limit=limit)), 15 | ('sizes', _('Size'), url_for('graphs', 'sizes', repo, limit=limit)), 16 | ('times', _('Elapsed Time'), url_for('graphs', 'times', repo, limit=limit)), 17 | ('errors', _('Errors'), url_for('graphs', 'errors', repo, limit=limit)), 18 | ] -%} 19 |
20 |
    21 | {% for item in graph_nav_bar %} 22 | {{ item[1] }} 23 | {% endfor %} 24 |
25 |
26 | 27 |
28 |
29 |
{{ timerange(ranges=[1, 7, 14, 30, 60, 90], limit=limit) }}
30 | {% block graph_body %} 31 | {% endblock graph_body %} 32 |
33 |
34 |
35 | {% endblock content %} 36 | -------------------------------------------------------------------------------- /rdiffweb/controller/filter_authorization.py: -------------------------------------------------------------------------------- 1 | # rdiffweb, A web interface to rdiff-backup repositories 2 | # Copyright (C) 2012-2025 rdiffweb contributors 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | import cherrypy 17 | 18 | 19 | def is_admin(): 20 | # Validate the permissions. 21 | if not cherrypy.serving.request.currentuser or not cherrypy.serving.request.currentuser.is_admin: 22 | raise cherrypy.HTTPError(404) 23 | 24 | 25 | def is_maintainer(): 26 | # Validate the permissions. 27 | if not cherrypy.serving.request.currentuser or not cherrypy.serving.request.currentuser.is_maintainer: 28 | raise cherrypy.HTTPError(404) 29 | 30 | 31 | # Make sure it's running after authentication (priority = 72) 32 | cherrypy.tools.is_admin = cherrypy.Tool('before_handler', is_admin, priority=80) 33 | cherrypy.tools.is_maintainer = cherrypy.Tool('before_handler', is_maintainer, priority=80) 34 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | # rdiffweb, A web interface to rdiff-backup repositories 2 | # Copyright (C) 2012-2025 rdiffweb contributors 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | include LICENSE 18 | include README.md 19 | include MANIFEST.in 20 | include rdw.conf 21 | include babel.cfg 22 | 23 | # Include templates 24 | recursive-include rdiffweb/templates * 25 | 26 | # Include static Web files 27 | recursive-include extras * 28 | 29 | # Include static Web files 30 | recursive-include rdiffweb/static * 31 | 32 | # Include translation file 33 | recursive-include rdiffweb/locales *.po *.pot *.mo 34 | 35 | # Exclude gitignore 36 | exclude .gitignore 37 | 38 | # Exclude gitlab CICD from source 39 | exclude .gitlab-ci.yml 40 | 41 | # Exclude "debian" folder 42 | prune debian 43 | 44 | # Exclude Dockerfile 45 | exclude Dockerfile .dockerignore 46 | 47 | # Exclude sonarqube CICD config 48 | exclude sonar-project.properties 49 | -------------------------------------------------------------------------------- /rdiffweb/templates/admin_overview.html: -------------------------------------------------------------------------------- 1 | {% extends "admin.html" %} 2 | {% from 'include/table.html' import table %} 3 | {% set active_page='admin' %} 4 | {% block title %} 5 | {% trans %}Admin area{% endtrans %} 6 | {% endblock title %} 7 | {% block content %} 8 | {% set card_items = [ 9 | (_('Users'), user_count, url_for('/admin/users/'), 'primary'), 10 | (_('Repositories'), repo_count, url_for('/admin/repos/'), 'success'), 11 | (_('Active Sessions (last hour)'), session_count, url_for('/admin/session/'), 'warning'), 12 | ]%} 13 |
14 | {% for label, count, url, class in card_items %} 15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | {{ label }} 23 |
24 |
{{ count }}
25 |
26 |
27 | 28 |
29 |
30 |
31 |
32 |
33 |
34 | {% endfor %} 35 |
36 | {% endblock content %} 37 | -------------------------------------------------------------------------------- /rdiffweb/tools/required_scope.py: -------------------------------------------------------------------------------- 1 | # Required scope tools for cherrypy 2 | # Copyright (C) 2023 IKUS Software 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | import cherrypy 17 | 18 | 19 | def required_scope(scope): 20 | """ 21 | Check the current authentication has the required scope to access the resource. 22 | """ 23 | # Convert single scope or scope list to array. 24 | if isinstance(scope, str): 25 | scope = scope.split(',') 26 | # Get the current user scope 27 | current_scope = getattr(cherrypy.serving.request, 'scope', []) 28 | # Check if our current_scope match any of the required scope. 29 | if current_scope: 30 | for s in scope: 31 | if s in current_scope: 32 | return True 33 | raise cherrypy.HTTPError(403) 34 | 35 | 36 | # Make sure it's running after authentication (priority = 72) 37 | cherrypy.tools.required_scope = cherrypy.Tool('before_handler', required_scope, priority=85) 38 | -------------------------------------------------------------------------------- /rdiffweb/controller/tests/test_page_admin.py: -------------------------------------------------------------------------------- 1 | # rdiffweb, A web interface to rdiff-backup repositories 2 | # Copyright (C) 2012-2025 rdiffweb contributors 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | from parameterized import parameterized 17 | 18 | import rdiffweb.test 19 | from rdiffweb.core.model import UserObject 20 | 21 | 22 | class AdminPagesAsUser(rdiffweb.test.WebCase): 23 | def setUp(self): 24 | super().setUp() 25 | # Add test user 26 | userobj = UserObject.add_user('test', 'test123') 27 | userobj.commit() 28 | self._login('test', 'test123') 29 | 30 | @parameterized.expand( 31 | [ 32 | "/admin/", 33 | "/admin/users/", 34 | "/admin/repos/", 35 | "/admin/session/", 36 | "/admin/sysinfo/", 37 | "/admin/logs/", 38 | ] 39 | ) 40 | def test_forbidden_access(self, value): 41 | self.getPage(value) 42 | self.assertStatus(404) 43 | -------------------------------------------------------------------------------- /rdiffweb/templates/email_notification.html: -------------------------------------------------------------------------------- 1 | {% extends 'email_layout.html' %} 2 | {% block title %} 3 | {% if repos %} 4 | {% trans %}Backup inactive{% endtrans %} 5 | {% else %} 6 | {% trans %}Notification{% endtrans %} 7 | {% endif %} 8 | {% endblock title %} 9 | {% block body %} 10 |

11 | {% trans username=(user.fullname or user.username) %}Hey {{ username }},{% endtrans %} 12 |

13 |

{% trans %}You are receiving this email to notify you about your backups.{% endtrans %}

14 | {# Quota Usage #} 15 | {{ storage_usage(disk_usage, disk_quota) }} 16 | {# Inactive repo #} 17 | {% if repos %} 18 |

19 | {% trans %}The following repositories are inactive for some time. We invite you to have a look at your last backup schedule.{% endtrans %} 20 |

21 |
    22 | {% for repo in repos %} 23 |
  • 24 | {{ repo.display_name }} 25 |
    26 | 27 | {% if repo.last_backup_date %} 28 | {% trans %}Last backup {% endtrans %} 29 | 31 | {% endif %} 32 | {% if repo.status[0] != 'ok' %}{{ repo.status[1] }}{% endif %} 33 | 34 |
  • 35 | {% endfor %} 36 |
37 |

{% trans %}If you don't want to be notify about this. You need to review your user preferences.{% endtrans %}

38 | {% endif %} 39 | {% endblock body %} 40 | -------------------------------------------------------------------------------- /rdiffweb/core/model/_callbacks.py: -------------------------------------------------------------------------------- 1 | # rdiffweb, A web interface to rdiff-backup repositories 2 | # Copyright (C) 2012-2025 rdiffweb contributors 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | import cherrypy 17 | from sqlalchemy import event 18 | 19 | Session = cherrypy.db.get_session() 20 | 21 | 22 | def add_post_commit_tasks(session, *args): 23 | # Add task 24 | session.info.setdefault("_after_commit_tasks", []).append(args) 25 | 26 | 27 | @event.listens_for(Session, 'after_rollback') 28 | def reset_tasks(session): 29 | # Clear previous tasks. 30 | session.info.pop('_after_commit_tasks', None) 31 | 32 | 33 | @event.listens_for(Session, 'after_transaction_end') 34 | def run_tasks(session, transaction): 35 | if transaction.parent: 36 | return 37 | # Execute after commit tasks. 38 | if '_after_commit_tasks' in session.info: 39 | tasks = session.info.pop('_after_commit_tasks', None) 40 | for args in tasks: 41 | cherrypy.engine.publish(*args) 42 | -------------------------------------------------------------------------------- /rdiffweb/tools/enrich_session.py: -------------------------------------------------------------------------------- 1 | # rdiffweb, A web interface to rdiff-backup repositories 2 | # Copyright (C) 2012-2025 rdiffweb contributors 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | import datetime 17 | 18 | import cherrypy 19 | 20 | 21 | def enrich_session(): 22 | """ 23 | Store ephemeral information into user's session. e.g.: last IP address, user-agent 24 | """ 25 | # When session is not enable, simply validate credentials 26 | if not hasattr(cherrypy.serving, 'session'): 27 | return 28 | # Get information related to the current request 29 | request = cherrypy.serving.request 30 | ip_address = request.remote.ip 31 | session = cherrypy.serving.session 32 | session['ip_address'] = ip_address 33 | session['user_agent'] = request.headers.get('User-Agent', None) 34 | session['access_time'] = datetime.datetime.now(tz=datetime.timezone.utc) 35 | 36 | 37 | cherrypy.tools.enrich_session = cherrypy.Tool('before_handler', enrich_session, priority=60) 38 | -------------------------------------------------------------------------------- /rdiffweb/controller/page_locations.py: -------------------------------------------------------------------------------- 1 | # rdiffweb, A web interface to rdiff-backup repositories 2 | # Copyright (C) 2012-2025 rdiffweb contributors 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | import logging 18 | 19 | import cherrypy 20 | 21 | from rdiffweb.controller import Controller 22 | 23 | # Define the logger 24 | logger = logging.getLogger(__name__) 25 | 26 | 27 | class LocationsPage(Controller): 28 | @cherrypy.expose 29 | def index(self): 30 | """ 31 | Shows repositories of current user 32 | """ 33 | # Get page params 34 | currentuser = cherrypy.serving.request.currentuser 35 | if currentuser.refresh_repos(): 36 | currentuser.commit() 37 | params = { 38 | "repos": currentuser.repo_objs, 39 | "disk_usage": currentuser.disk_usage, 40 | "disk_quota": currentuser.disk_quota, 41 | } 42 | # Render the page. 43 | return self._compile_template("locations.html", **params) 44 | -------------------------------------------------------------------------------- /debian/source/options: -------------------------------------------------------------------------------- 1 | # Ignore excluded files in d/copyright 2 | extend-diff-ignore = "extras" 3 | extend-diff-ignore = "rdiffweb/static/css/bootstrap.min.css" 4 | extend-diff-ignore = "rdiffweb/static/css/bootstrap.min.css.map" 5 | extend-diff-ignore = "rdiffweb/static/css/font-awesome.css.map" 6 | extend-diff-ignore = "rdiffweb/static/css/font-awesome.min.css" 7 | extend-diff-ignore = "rdiffweb/static/css/jquery.dataTables.min.css" 8 | extend-diff-ignore = "rdiffweb/static/css/responsive.dataTables.min.css" 9 | extend-diff-ignore = "rdiffweb/static/fonts/fontawesome-webfont.eot" 10 | extend-diff-ignore = "rdiffweb/static/fonts/fontawesome-webfont.svg" 11 | extend-diff-ignore = "rdiffweb/static/fonts/fontawesome-webfont.woff" 12 | extend-diff-ignore = "rdiffweb/static/fonts/fontawesome-webfont.woff2" 13 | extend-diff-ignore = "rdiffweb/static/images/sort_asc.png" 14 | extend-diff-ignore = "rdiffweb/static/images/sort_asc_disabled.png" 15 | extend-diff-ignore = "rdiffweb/static/images/sort_both.png" 16 | extend-diff-ignore = "rdiffweb/static/images/sort_desc.png" 17 | extend-diff-ignore = "rdiffweb/static/images/sort_desc_disabled.png" 18 | extend-diff-ignore = "rdiffweb/static/js/bootstrap.bundle.js.map" 19 | extend-diff-ignore = "rdiffweb/static/js/bootstrap.bundle.min.js" 20 | extend-diff-ignore = "rdiffweb/static/js/chartkick.min.js" 21 | extend-diff-ignore = "rdiffweb/static/js/chart.min.js" 22 | extend-diff-ignore = "rdiffweb/static/js/dataTables.buttons.min.js" 23 | extend-diff-ignore = "rdiffweb/static/js/dataTables.responsive.min.js" 24 | extend-diff-ignore = "rdiffweb/static/js/jquery.dataTables.min.js" 25 | extend-diff-ignore = "rdiffweb/static/js/jquery.min.js" 26 | -------------------------------------------------------------------------------- /rdiffweb/core/tests/test_rdw_helpers.py: -------------------------------------------------------------------------------- 1 | # rdiffweb, A web interface to rdiff-backup repositories 2 | # Copyright (C) 2012-2025 rdiffweb contributors 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | import unittest 18 | 19 | from rdiffweb.core.rdw_helpers import quote_url, unquote_url 20 | 21 | 22 | class Test(unittest.TestCase): 23 | def test_quote_url(self): 24 | self.assertEqual('this%20is%20some%20path', quote_url('this is some path')) 25 | self.assertEqual('this%20is%20some%20path', quote_url(b'this is some path')) 26 | self.assertEqual( 27 | 'R%C3%A9pertoire%20%28%40vec%29%20%7Bc%C3%A0ra%C3%A7t%23%C3%A8r%C3%AB%7D%20%24%C3%A9p%C3%AAcial', 28 | quote_url(b'R\xc3\xa9pertoire (@vec) {c\xc3\xa0ra\xc3\xa7t#\xc3\xa8r\xc3\xab} $\xc3\xa9p\xc3\xaacial'), 29 | ) 30 | 31 | def test_unquote_url(self): 32 | self.assertEqual(b'this is some path', unquote_url('this%20is%20some%20path')) 33 | self.assertEqual(b'this is some path', unquote_url(b'this%20is%20some%20path')) 34 | -------------------------------------------------------------------------------- /doc/two_factor_authentication.md: -------------------------------------------------------------------------------- 1 | # Two-Factor Authentication 2 | 3 | Two-factor authentication (2FA) provides an additional level of security to your Rdiffweb account. In order for others to access your account, they must have your username and password, as well as access to your second factor of authentication. 4 | 5 | As of version 2.5.0, Rdiffweb supports email verification as a second authentication factor. 6 | 7 | When enabled, users must log in with a username and password. Then a verification code is emailed to the user. To successfully authenticate, the user must provide this verification code. 8 | 9 | ## Enable 2FA as Administrator 10 | 11 | For 2FA to work properly, [SMTP must be configured properly](configuration.md#configure-email-notifications). 12 | 13 | In the administration view, an administrator can enable 2FA for a specific user. By doing so, the next time this user tries to connect to Rdiffweb, he will be prompted to enter a verification code that will be sent to his email. 14 | 15 | 1. Go to **Admin Area > Users** 16 | 2. **Edit** a user 17 | 3. Change the value of *Two-factor authentication* to *Enabled* 18 | 19 | ## Enabled 2FA as User 20 | 21 | For 2FA to work properly, [SMTP must be configured properly](configuration.md#configure-email-notifications). 22 | 23 | A user may enabled 2FA for is own account from it's user's profile. To enabled 2FA, the user must provide the verification code that get sent to him by email. 24 | 25 | 1. Go to **Edit profile > Two-Factor Authentication** 26 | 2. Click **Enable Two-Factor Authentication** 27 | 3. A verification code should be sent to your email address 28 | 4. Enter this verification code 29 | -------------------------------------------------------------------------------- /rdiffweb/templates/include/storage_usage.html: -------------------------------------------------------------------------------- 1 | {% macro storage_usage(disk_usage, disk_quota) %} 2 | {% if disk_usage and disk_quota %} 3 |
4 |
5 | {% set used_pct = disk_usage / disk_quota * 100 %} 6 | {% set used_str = disk_usage | filesize %} 7 | {% set size_str = disk_quota | filesize %} 8 | {% trans %}Storage Usage{% endtrans %} 9 |
10 |
16 |
17 |
18 |
19 |
20 | {% trans %}Used{% endtrans %} 21 |
{{ used_pct|int }}%
22 | {{ disk_usage | filesize }} 23 |
24 |
25 | {% trans %}Free{% endtrans %} 26 |
{{ [0, 100 - used_pct]| max |int }}%
27 | {{ [0, disk_quota - disk_usage] | max | filesize }} 28 |
29 |
30 | {% trans %}Total{% endtrans %} 31 |
{{ disk_quota | filesize }}
32 |
33 |
34 |
35 |
36 | {% endif %} 37 | {% endmacro %} 38 | -------------------------------------------------------------------------------- /rdiffweb/templates/admin_activity.html: -------------------------------------------------------------------------------- 1 | {% extends 'admin.html' %} 2 | {% from 'include/table.html' import table %} 3 | {% block title %} 4 | {% trans %}System Logs{% endtrans %} 5 | {% endblock title %} 6 | {% set admin_nav_active="activity" %} 7 | {% block content %} 8 | {# Lazy loaded Table #} 9 | {% set user_model = {'text': _('Show User Activities'), 'extend': 'filter', 'column': 'model_name:name', 'search':'user'} %} 10 | {% set repo_model = {'text': _('Show Repository Activities'), 'extend': 'filter', 'column': 'model_name:name', 'search':'repo'} %} 11 | {% set buttons = [user_model, repo_model, {'text': _('Reset'), 'extend': 'clear'}] %} 12 | {% set columns = [ 13 | {'name':'date', 'title':_('Date'), 'render':'datetime'}, 14 | {'name':'author', 'title':_('Actor'), 'orderable':True}, 15 | {'name':'model_id', 'visible':False}, 16 | {'name':'model_name', 'title':_('Model Type'), 'orderable':True, 'render':'choices', 'render_arg': [('user',_('User')), ('repo', _('Repository'))] }, 17 | {'name':'model_summary', 'title':_('Target'), 'orderable':True}, 18 | {'name':'type', 'title':_('Action'), 'render':'choices', 'render_arg': [ ['new',_('Create')], ['dirty',_('Modify')], ['deleted', _('Delete')], ['comment', _('Comment')], ['event', _('Event')] ] }, 19 | {'name':'body', 'visible':False}, 20 | {'name':'changes', 'title':_('Details'), 'render':'message_body' }, 21 | ] %} 22 | {{ table(url_for('/admin/activity/data.json'), 23 | columns=columns, 24 | buttons=buttons, 25 | order=[[ 0, 'desc' ]], 26 | state_save=False, 27 | searching=True, 28 | empty_message=_('No Activity'), 29 | info_message=_('Displaying _START_-_END_ of _TOTAL_ most recent activities'), 30 | paging=True, 31 | page_length=20, 32 | server_side=True) }} 33 | {% endblock %} 34 | -------------------------------------------------------------------------------- /rdiffweb/core/tests/test_passwd.py: -------------------------------------------------------------------------------- 1 | # rdiffweb, A web interface to rdiff-backup repositories 2 | # Copyright (C) 2012-2025 rdiffweb contributors 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | import unittest 18 | 19 | from parameterized import parameterized 20 | 21 | from rdiffweb.core.passwd import check_password, hash_password 22 | 23 | 24 | class Test(unittest.TestCase): 25 | @parameterized.expand( 26 | [ 27 | ('admin123', 'f865b53623b121fd34ee5426c792e5c33af8c227'), 28 | ('admin123', '{SSHA}/LAr7zGT/Rv/CEsbrEndyh27h+4fLb9h'), 29 | ('admin123', '$argon2id$v=19$m=102400,t=2,p=8$/mDhOg8wyZeMTUjcbIC7mg$3pxRSfYgUXmKEKNtasP1Og'), 30 | ] 31 | ) 32 | def test_check_password(self, password, challenge): 33 | self.assertTrue(check_password(password, challenge)) 34 | self.assertTrue(check_password(password, hash_password(password))) 35 | self.assertFalse(check_password('invalid', challenge)) 36 | self.assertFalse(check_password(password, 'invalid')) 37 | 38 | def test_hash_password(self): 39 | self.assertTrue(hash_password('admin12').startswith('$argon2id$')) 40 | -------------------------------------------------------------------------------- /rdiffweb/templates/include/datatables.html: -------------------------------------------------------------------------------- 1 | {% set table_id = 1 %} 2 | {% macro datatables(label, buttons=[]) %} 3 | {% set id = 'table' + table_id|string %} 4 | {% set table_id = table_id + 1 %} 5 | 9 | {{ caller() }} 10 |
11 | 47 | {% endmacro %} 48 | -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | 3 | include /usr/share/dpkg/pkg-info.mk 4 | 5 | export SETUPTOOLS_SCM_PRETEND_VERSION_FOR_RDIFFWEB := $(DEB_VERSION_UPSTREAM) 6 | 7 | # Force use of pyproject.toml 8 | export PYBUILD_SYSTEM=pyproject 9 | 10 | # For testing, we need a timezone 11 | export TZ=UTC 12 | 13 | %: 14 | dh $@ --buildsystem=pybuild --test-tox 15 | 16 | execute_before_dh_auto_build: 17 | @echo "Checking for Ubuntu Jammy..." 18 | @if grep -q "Ubuntu 22.04" /etc/os-release; then \ 19 | echo "Running on Ubuntu Jammy: Installing setuptools>=66,<=68"; \ 20 | pip install "setuptools>=66,<=68"; \ 21 | fi 22 | 23 | execute_after_dh_auto_install: 24 | # Test files are not required for runtime 25 | rm -rf debian/rdiffweb/usr/lib/python*/dist-packages/rdiffweb/controller/tests 26 | rm -rf debian/rdiffweb/usr/lib/python*/dist-packages/rdiffweb/core/tests 27 | rm -rf debian/rdiffweb/usr/lib/python*/dist-packages/rdiffweb/tests 28 | rm -f debian/rdiffweb/usr/lib/python*/dist-packages/rdiffweb/test.py 29 | 30 | override_dh_installman: 31 | mkdir -p debian/rdiffweb/usr/share/man/man1 32 | PATH=debian/rdiffweb/usr/bin:$(PATH) \ 33 | PYTHONPATH=debian/rdiffweb/usr/lib/python{version}/dist-packages:$(PYTHONPATH) \ 34 | help2man --version-string "$(DEB_VERSION_UPSTREAM)" \ 35 | --no-info \ 36 | --name "A web interface to rdiff-backup repositories" \ 37 | --no-discard-stderr \ 38 | rdiffweb > \ 39 | debian/rdiffweb/usr/share/man/man1/rdiffweb.1 40 | 41 | execute_before_dh_auto_clean: 42 | rm -rf *.egg-info 43 | 44 | # Generate orig.tar.gz 45 | gentarball: SOURCE=rdiffweb 46 | gentarball: 47 | git archive --format=tar HEAD --prefix=$(SOURCE)-$(DEB_VERSION_UPSTREAM)/ | gzip -9 > ../$(SOURCE)_$(DEB_VERSION_UPSTREAM).orig.tar.gz 48 | mk-origtargz --compression gzip ../$(SOURCE)_$(DEB_VERSION_UPSTREAM).orig.tar.gz 49 | -------------------------------------------------------------------------------- /doc/quickstart.md: -------------------------------------------------------------------------------- 1 | # Quick Start 2 | 3 | What to do to get started using Rdiffweb ? 4 | 5 | 1. [Install Rdiffweb](installation.md) 6 | 7 | * Rdiffweb can be installed on a Linux-based operating system. 8 | 9 | 2. [Configure the database](configuration.md#configure-database) 10 | 11 | * Rdiffweb can use either SQLite or PostgreSQL as the backend database. 12 | * To use SQLite, no additional configuration is needed. To use PostgreSQL, update the `database-uri` setting in the `/etc/rdiffweb/rdw.conf` file with the appropriate database connection details. 13 | 14 | 3. [Change the administrator password](configuration.md#configure-administrator-username-and-password) 15 | 16 | * You can change the administrator password either through the admin-password configuration option in the `/etc/rdiffweb/rdw.conf` file or through the web interface. 17 | * To change the password via the configuration file, update the `admin-password` setting with a new, secure password. 18 | * To change the password via the web interface, log in as the administrator (username: **admin** password: **admin123**) click on the "admin" in top-right corner, and select "Edit profile". 19 | 20 | 4. [Create a user account and adjust the user root location](settings.md#users-repositories) 21 | 22 | * To create a new user, log in as the administrator (username: **admin** password: **admin123**) click on the "Admin area" tab, and select "Users". 23 | * Click "Add user" and follow the prompts to create a new user account with a username and password. 24 | * After creating the user account, you can adjust the user's root location by going to the "Users" tab in the web interface, selecting the user, and clicking on "Edit". From there, you can specify the user's root location defining the location of the backup for that particular user.. 25 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: rdiffweb 2 | Section: web 3 | Priority: optional 4 | Maintainer: Patrik Dufresne 5 | Build-Depends: 6 | chromium-driver, 7 | debhelper-compat (= 13), 8 | dh-python, 9 | dh-sequence-python3, 10 | help2man, 11 | lsb-release, 12 | pybuild-plugin-pyproject, 13 | python3, 14 | python3-all-dev, 15 | python3-apscheduler, 16 | python3-argon2, 17 | python3-babel, 18 | python3-cached-property, 19 | python3-cherrypy3, 20 | python3-configargparse, 21 | python3-distro, 22 | python3-html5lib, 23 | python3-humanfriendly, 24 | python3-jinja2, 25 | python3-ldap3, 26 | python3-oauthlib, 27 | python3-parameterized, 28 | python3-pip, 29 | python3-psutil, 30 | python3-pytest, 31 | python3-requests, 32 | python3-requests-oauthlib, 33 | python3-responses, 34 | python3-selenium, 35 | python3-setuptools, 36 | python3-setuptools-scm, 37 | python3-sqlalchemy, 38 | python3-tz, 39 | python3-wtforms, 40 | python3-zxcvbn | python3-python-zxcvbn-rs-py, 41 | rdiff-backup, 42 | Rules-Requires-Root: no 43 | Standards-Version: 4.7.0 44 | Homepage: https://rdiffweb.org 45 | Vcs-Git: https://gitlab.com/ikus-soft/rdiffweb.git 46 | Vcs-Browser: https://gitlab.com/ikus-soft/rdiffweb 47 | 48 | Package: rdiffweb 49 | Architecture: all 50 | Depends: 51 | fonts-font-awesome, 52 | libjs-bootstrap4, 53 | libjs-chart.js, 54 | libjs-chartkick.js, 55 | libjs-jquery, 56 | libjs-jquery-datatables, 57 | libjs-jquery-datatables-extensions, 58 | rdiff-backup, 59 | ${python3:Depends}, 60 | ${misc:Depends}, 61 | Suggests: 62 | python3-psycopg2, 63 | Description: web interface to rdiff-backup repositories 64 | Rdiffweb is a web application that allows you to view repositories generated 65 | by rdiff-backup. The purpose of this application is to ease the management 66 | of backups and quickly restore your data with a rich and powerful web 67 | interface. 68 | -------------------------------------------------------------------------------- /rdiffweb/controller/page_history.py: -------------------------------------------------------------------------------- 1 | # rdiffweb, A web interface to rdiff-backup repositories 2 | # Copyright (C) 2012-2025 rdiffweb contributors 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | 18 | import cherrypy 19 | 20 | from rdiffweb.controller import Controller, validate_int 21 | from rdiffweb.core.librdiff import AccessDeniedError, DoesNotExistError 22 | from rdiffweb.core.model import RepoObject 23 | 24 | 25 | @cherrypy.tools.poppath() 26 | class HistoryPage(Controller): 27 | @cherrypy.expose 28 | @cherrypy.tools.errors( 29 | error_table={ 30 | DoesNotExistError: 404, 31 | AccessDeniedError: 403, 32 | } 33 | ) 34 | def default(self, path, limit='10', **kwargs): 35 | """ 36 | Show repository, file or folder history 37 | """ 38 | limit = validate_int(limit) 39 | 40 | repo, path = RepoObject.get_repo_path(path) 41 | 42 | # Set up warning about in-progress backups, if necessary 43 | path_obj = repo.fstat(path) 44 | parms = { 45 | "limit": limit, 46 | "repo": repo, 47 | "path": path, 48 | "path_obj": path_obj, 49 | } 50 | 51 | return self._compile_template("history.html", **parms) 52 | -------------------------------------------------------------------------------- /rdiffweb/templates/settings.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout_repo.html' %} 2 | {% from 'include/panel.html' import panel %} 3 | {% from 'include/messages.html' import messages %} 4 | {% from 'include/modal_dialog.html' import modal_dialog, button_confirm, modal_confirm %} 5 | {% set active_page='repo' %} 6 | {% set active_repo_page='settings' %} 7 | {% block title %} 8 | {% trans %}Settings{% endtrans %} 9 | {% endblock %} 10 | {% block content %} 11 |
12 | {# Delete repo button #} 13 | {% if not is_maintainer %} 14 | {% trans %}Ask your administrator if you want to delete this repository.{% endtrans %} 15 | {% endif %} 16 | {{ button_confirm(label=_('Delete repository'), target="#delete-repo-modal", class="btn-danger float-right", disabled=not is_maintainer, redirect=url_for('/'), url=url_for('delete', repo)) }} 17 | {# Delete Repo Modal #} 18 | {{ modal_confirm( 19 | id="delete-repo-modal", 20 | title=_('Confirmation required'), 21 | message=_("You are about to permanently delete this repository. Deleted repository CANNOT be restored! Are you ABSOLUTELY sure?"), 22 | fields=['action'], 23 | submit=_('Delete'), 24 | confirm_value=repo.display_name) }} 25 | {# Repo settings #} 26 |
27 | {% call panel( 28 | title=_("Repository Settings"), 29 | description=_('You can modify the backup repository settings to suit your requirements. These settings allow you to adjust the notification period, retention period, and display format (encoding) of the repository to align with your preferred frequency, data storage duration, and localization needs.'), 30 | show_submit_area=True) %} 31 | {{ form }} 32 | {% endcall %} 33 |
34 | {# Message Thread #} 35 | {{ messages(data_url=url_for('audit', repo)) }} 36 |
37 | {% endblock %} 38 | -------------------------------------------------------------------------------- /rdiffweb/templates/components/nav.html: -------------------------------------------------------------------------------- 1 | {% macro nav_tabs(nav_items) -%} 2 | 17 | {% endmacro %} 18 | {% macro nav_pills(nav_items) -%} 19 | 34 | {% endmacro %} 35 | {% macro nav_list(nav_items) -%} 36 |
37 | {% for item in nav_items %} 38 | {% if item|length == 3 %} 39 | {% set label, url, active = item %} 40 | {% elif item|length == 4 %} 41 | {% set label, url, active, icon = item %} 42 | {% endif %} 43 | 46 | {% if icon %}{% endif %} 47 | {{ label }} 48 | 49 | {% endfor %} 50 |
51 | {% endmacro %} 52 | -------------------------------------------------------------------------------- /rdiffweb/controller/page_prefs.py: -------------------------------------------------------------------------------- 1 | # rdiffweb, A web interface to rdiff-backup repositories 2 | # Copyright (C) 2012-2025 rdiffweb contributors 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | import logging 18 | 19 | import cherrypy 20 | 21 | from rdiffweb.controller import Controller 22 | from rdiffweb.controller.page_pref_general import PagePrefsGeneral 23 | from rdiffweb.controller.page_pref_mfa import PagePrefMfa 24 | from rdiffweb.controller.page_pref_notification import PagePrefNotification 25 | from rdiffweb.controller.page_pref_session import PagePrefSession 26 | from rdiffweb.controller.page_pref_sshkeys import PagePrefSshKeys 27 | from rdiffweb.controller.page_pref_tokens import PagePrefTokens 28 | from rdiffweb.core.rdw_templating import url_for 29 | 30 | # Define the logger 31 | logger = logging.getLogger(__name__) 32 | 33 | 34 | class PreferencesPage(Controller): 35 | general = PagePrefsGeneral() 36 | mfa = PagePrefMfa() 37 | notification = PagePrefNotification() 38 | session = PagePrefSession() 39 | sshkeys = PagePrefSshKeys() 40 | tokens = PagePrefTokens() 41 | 42 | @cherrypy.expose 43 | def index(self, panelid=None, **kwargs): 44 | """ 45 | Redirect user to general settings 46 | """ 47 | raise cherrypy.HTTPRedirect(url_for('/prefs/general')) 48 | -------------------------------------------------------------------------------- /rdiffweb/core/rdw_helpers.py: -------------------------------------------------------------------------------- 1 | # rdiffweb, A web interface to rdiff-backup repositories 2 | # Copyright (C) 2012-2025 rdiffweb contributors 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | from urllib.parse import quote, unquote 18 | 19 | 20 | # TODO: Move this into page_main 21 | def quote_url(url, safe='/'): 22 | """ 23 | Receive either str or bytes. Always return str. 24 | """ 25 | # If URL is None, return None 26 | if not url: 27 | return '' 28 | # Convert everything to bytes 29 | if not isinstance(url, bytes): 30 | url = url.encode(encoding='latin1') 31 | if not isinstance(safe, bytes): 32 | safe = safe.encode(encoding='latin1') 33 | 34 | # URL encode 35 | val = quote(url, safe) 36 | if isinstance(val, bytes): 37 | val = val.decode(encoding='latin1') 38 | return val 39 | 40 | 41 | # TODO: Move this into page_main 42 | def unquote_url(url): 43 | """ 44 | Receive either str or bytes. Always return bytes 45 | """ 46 | if not url: 47 | return url 48 | # Convert everything to str 49 | if isinstance(url, bytes): 50 | url = url.decode(encoding='latin1') 51 | # Unquote 52 | val = unquote(url) 53 | # Make sure to return bytes. 54 | if isinstance(val, str): 55 | val = val.encode(encoding='latin1') 56 | return val 57 | -------------------------------------------------------------------------------- /rdiffweb/tests/test_main.py: -------------------------------------------------------------------------------- 1 | # rdiffweb, A web interface to rdiff-backup repositories 2 | # Copyright (C) 2012-2025 rdiffweb contributors 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | 18 | import contextlib 19 | import importlib.resources 20 | import io 21 | import unittest 22 | from unittest.mock import patch 23 | 24 | from rdiffweb.main import main 25 | 26 | 27 | @patch('cherrypy.quickstart') 28 | class Test(unittest.TestCase): 29 | def test_main_with_config(self, *args): 30 | config = str(importlib.resources.files(__package__) / 'rdw.conf') 31 | main(['-f', config]) 32 | 33 | def test_main_without_config(self, *args): 34 | f = io.StringIO() 35 | with contextlib.redirect_stdout(f): 36 | main([]) 37 | 38 | def test_main_help(self, *args): 39 | f = io.StringIO() 40 | with contextlib.redirect_stdout(f): 41 | with self.assertRaises(SystemExit): 42 | main(['--help']) 43 | self.assertTrue(f.getvalue().startswith('usage: rdiffweb'), msg='%s is not a help message' % f.getvalue()) 44 | 45 | def test_main_version(self, *args): 46 | f = io.StringIO() 47 | with contextlib.redirect_stdout(f): 48 | with self.assertRaises(SystemExit): 49 | main(['--version']) 50 | self.assertRegex(f.getvalue(), r'rdiffweb (DEV|[0-9].*)') 51 | -------------------------------------------------------------------------------- /rdiffweb/controller/page_browse.py: -------------------------------------------------------------------------------- 1 | # rdiffweb, A web interface to rdiff-backup repositories 2 | # Copyright (C) 2012-2025 rdiffweb contributors 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | import logging 18 | 19 | import cherrypy 20 | 21 | import rdiffweb.tools.errors # noqa 22 | from rdiffweb.controller import Controller 23 | from rdiffweb.core.librdiff import AccessDeniedError, DoesNotExistError 24 | from rdiffweb.core.model import RepoObject 25 | 26 | # Define the logger 27 | logger = logging.getLogger(__name__) 28 | 29 | 30 | @cherrypy.tools.poppath() 31 | class BrowsePage(Controller): 32 | @cherrypy.expose 33 | @cherrypy.tools.errors( 34 | error_table={ 35 | DoesNotExistError: 404, 36 | AccessDeniedError: 403, 37 | } 38 | ) 39 | def default(self, path): 40 | """ 41 | Browser view displaying files and folders in user's repository 42 | """ 43 | # Check user access to the given repo & path 44 | repo, path = RepoObject.get_repo_path(path, refresh=True) 45 | 46 | # Get list of actual directory entries 47 | if repo.status[0] == 'failed': 48 | dir_entries = [] 49 | else: 50 | dir_entries = repo.listdir(path) 51 | parms = {"repo": repo, "path": path, "dir_entries": dir_entries} 52 | return self._compile_template("browse.html", **parms) 53 | -------------------------------------------------------------------------------- /doc/configuration-latest.md: -------------------------------------------------------------------------------- 1 | # Rdiffweb Version Check 2 | 3 | The Rdiffweb Version Check feature is designed to automatically check for 4 | the latest version of Rdiffweb and notify the administrator via email if 5 | a new version is available. This feature is enabled by default and can be 6 | customized according to your needs. 7 | 8 | ## How it works 9 | 10 | The Version Check feature works by comparing the version number of the 11 | Rdiffweb installation against the latest available version number, 12 | which is obtained from the URL specified in the "latest-version-url" 13 | configuration option. If the version number of the Rdiffweb installation 14 | is lower than the latest available version number, an email notification 15 | is sent to the administrator. 16 | 17 | By default, the "latest-version-url" configuration option is set to 18 | "https://latest.ikus-soft.com/rdiffweb/latest_version". This URL contains 19 | the latest version number of Rdiffweb in a plain text format. 20 | 21 | However, administrators can customize this option by editing the "rdw.conf" 22 | configuration file and setting the "latest-version-url" option to a 23 | different URL if desired. 24 | 25 | ## Disabling the feature 26 | 27 | If you wish to disable the Version Check feature, you can do so by setting 28 | the "latest-version-url" configuration option to an empty value. 29 | This can be done by editing the "rdw.conf" configuration file and 30 | setting the option like this: 31 | 32 | ```ini 33 | latest-version-url = 34 | ``` 35 | 36 | By setting the "latest-version-url" option to an empty value, Rdiffweb will 37 | no longer check for the latest version of the software and will not send 38 | email notifications to the administrator if a new version is available. 39 | 40 | It's important to note that disabling the Version Check feature means 41 | that you will need to manually check for updates and upgrade Rdiffweb when a 42 | new version is available. Therefore, it's recommended to keep this feature 43 | enabled to ensure that you are running the latest version of Rdiffweb. 44 | -------------------------------------------------------------------------------- /doc/fail2ban.md: -------------------------------------------------------------------------------- 1 | # Installing and Configuring Fail2Ban for Secure SSH Server 2 | 3 | Fail2Ban is a powerful open-source intrusion prevention tool that helps protect your SSH server by automatically blocking IP addresses that exhibit suspicious behavior, such as repeated failed login attempts. Follow these steps to install and configure Fail2Ban to secure your SSH server: 4 | 5 | ## Step 1: Install Fail2Ban 6 | 7 | 1. Update your package manager's repository: 8 | 9 | ```sh 10 | sudo apt update 11 | ``` 12 | 13 | 2. Install Fail2Ban: 14 | 15 | ```sh 16 | sudo apt install fail2ban 17 | ``` 18 | 19 | ## Step 2: Configure Fail2Ban 20 | 21 | 1. Create a new configuration file: 22 | 23 | ```sh 24 | sudo touch /etc/fail2ban/jail.local 25 | ``` 26 | 27 | 2. Open the `jail.local` file using a text editor: 28 | 29 | ```sh 30 | sudo nano /etc/fail2ban/jail.local 31 | ``` 32 | 33 | 3. Configure the `[sshd]` section to secure SSH server: 34 | 35 | ```ini 36 | [sshd] 37 | enabled = true 38 | ``` 39 | 40 | 4. Save the changes and exit the text editor (press Ctrl + X, then Y, and finally Enter). 41 | 42 | ## Step 3: Start and Enable Fail2Ban 43 | 44 | 1. Start the Fail2Ban service: 45 | 46 | ```sh 47 | sudo systemctl start fail2ban 48 | ``` 49 | 50 | 2. Enable Fail2Ban to start on system boot: 51 | 52 | ```sh 53 | sudo systemctl enable fail2ban 54 | ``` 55 | 56 | ## Customize Fail2Ban Rules 57 | 58 | If you want to customize Fail2Ban rules or create specific filters for your SSH server, you can edit the `jail.local` file. 59 | Refer to the Fail2Ban documentation for more information on rule customization. You may configure additional settings like: 60 | 61 | * Set `port = ` to specify the SSH port you are using (default is 22). 62 | * Set `maxretry = ` to define the number of failed login attempts before an IP gets banned (recommended value: 3-5). 63 | * Set `bantime = ` to specify the duration an IP remains banned (recommended value: 1 hour or more). 64 | -------------------------------------------------------------------------------- /rdiffweb/templates/admin_user_edit.html: -------------------------------------------------------------------------------- 1 | {% extends 'admin.html' %} 2 | {% from 'include/panel.html' import panel %} 3 | {% from 'include/messages.html' import messages %} 4 | {% from 'include/storage_usage.html' import storage_usage %} 5 | {% from 'include/modal_dialog.html' import button_confirm, modal_confirm %} 6 | {% set admin_nav_active="users" %} 7 | {% block title %} 8 | {% trans %}Edit user{% endtrans %} 9 | {% endblock %} 10 | {% block content %} 11 |
12 | {# Delete button #} 13 | {{ button_confirm(label=_('Delete User'), class="btn-danger float-right", target="#delete-user-modal", url=url_for('admin', 'users', 'delete'), disabled=(form.username.data == username)) }} 14 | {# User Form #} 15 |
16 | {% call panel( 17 | title=_("Edit user"), 18 | show_submit_area=True) %} 19 | {{ form }} 20 | {{ storage_usage(form.disk_usage.data, form.disk_quota.data) }} 21 | {% endcall %} 22 |
23 | {# Message thread #} 24 | {{ messages(data_url=url_for('admin','users', 'messages', form.id.data|string)) }} 25 |
26 | {# Delete User Modal #} 27 | {% call modal_confirm( 28 | id="delete-user-modal", 29 | title=_('Confirm User Deletion'), 30 | message=_("Deleting this user will remove their account and access. You can also choose to delete all backup data associated with this user. Are you sure you want to delete this User?"), 31 | fields=[], 32 | submit=_('Delete User'), 33 | confirm_value=form.username.data) %} 34 |
35 | 39 | 42 |
43 | 44 | {% endcall %} 45 | {% endblock content %} 46 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=66", "setuptools_scm[toml]>=6.2", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "rdiffweb" 7 | authors = [ 8 | {name = "Patrik Dufresne", email = "patrik@ikus-soft.com"}, 9 | ] 10 | description = "A web interface to rdiff-backup repositories." 11 | readme = "README.md" 12 | requires-python = ">=3.8, <4" 13 | license = { file = "LICENSE" } 14 | classifiers = [ 15 | "Development Status :: 5 - Production/Stable", 16 | "Intended Audience :: System Administrators", 17 | "Programming Language :: Python :: 3", 18 | "Programming Language :: Python :: 3.6", 19 | "Programming Language :: Python :: 3.7", 20 | "Programming Language :: Python :: 3.8", 21 | "Programming Language :: Python :: 3.9", 22 | "Programming Language :: Python :: 3.10", 23 | "Framework :: CherryPy" 24 | ] 25 | dependencies = [ 26 | "apscheduler", 27 | "argon2-cffi>=18.3.0", 28 | "babel>=0.9.6", 29 | "cached-property", 30 | "CherryPy>=18", 31 | "cheroot<11", 32 | "configargparse", 33 | "distro", 34 | "humanfriendly", 35 | "Jinja2>=2.10", 36 | "ldap3", 37 | "MarkupSafe<3", 38 | "packaging", 39 | "psutil>=2.1.1", 40 | "pytz", 41 | "requests", 42 | "requests_oauthlib", 43 | "sqlalchemy>=1.4,<3", 44 | "WTForms>=2.2,<4", 45 | "zxcvbn>=4.4.27" 46 | ] 47 | dynamic = ["version"] 48 | 49 | [tool.setuptools.packages.find] 50 | where = ["."] 51 | include = ["rdiffweb*"] 52 | 53 | [project.optional-dependencies] 54 | test = [ 55 | "html5lib", 56 | "pytest", 57 | "parameterized", 58 | "responses", 59 | "selenium" 60 | ] 61 | 62 | [project.scripts] 63 | rdiffweb = "rdiffweb.main:main" 64 | 65 | [project.urls] 66 | Homepage = "https://rdiffweb.org" 67 | documentation = "https://www.ikus-soft.com/archive/rdiffweb/doc/latest/html/" 68 | source = "https://gitlab.com/ikus-soft/rdiffweb" 69 | bug_tracker = "https://gitlab.com/ikus-soft/rdiffweb/-/issues" 70 | 71 | [tool.black] 72 | line-length = 120 73 | skip-string-normalization = "True" 74 | 75 | [tool.djlint] 76 | indent=2 77 | 78 | [tool.setuptools_scm] -------------------------------------------------------------------------------- /rdiffweb/templates/login.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | {% set username = None %} 3 | {% block title %} 4 | {% trans %}Login{% endtrans %} 5 | {% endblock %} 6 | {% block body %} 7 |
8 |
9 |
10 |

11 | {{ header_name }} 15 |

16 |

17 | {% if welcome_msg %} 18 | {% autoescape false %} 19 | {{ welcome_msg }} 20 | {% endautoescape %} 21 | {% else %} 22 | {% trans %}A simplified backup management software for quick access to your archives through an 23 | efficient web interface.{% endtrans %} 24 |
25 |
26 | {% trans %}website{% endtrans %} • 27 | {% trans %}community{% endtrans %} 28 | {% endif %} 29 |

30 |
31 |
32 |
33 | {% block content %} 34 |
35 |
36 |
37 |

{% trans %}Welcome back{% endtrans %}

38 |
39 | 51 |
52 |
53 | {% endblock content %} 54 | {% endblock body %} 55 | -------------------------------------------------------------------------------- /rdiffweb/templates/admin_logs.html: -------------------------------------------------------------------------------- 1 | {% extends 'admin.html' %} 2 | {% from 'include/empty.html' import empty %} 3 | {% from 'components/log.html' import pre_code %} 4 | {% block title %} 5 | {% trans %}System Logs{% endtrans %} 6 | {% endblock title %} 7 | {% set admin_nav_active="logs" %} 8 | {% block content %} 9 |
10 | {% if log_files %} 11 |
12 | 19 |
20 |
21 | {% if data %} 22 | {# Show log file data #} 23 |
24 | {% trans %}Notice: To prevent performance issues, only the last {{ limit }} lines of the log files are displayed.{% endtrans %} 25 | {% trans %}Show all logs{% endtrans %} 26 |
27 | {{ pre_code(data)}} 28 | {% elif name or date %} 29 | {# Log file is empty. #} 30 | {% call empty('icon-file', _('Log file empty')) %} 31 |

{% trans %}This log file is empty. Select another log file to show it's contents.{% endtrans %}

32 | {% endcall %} 33 | {% else %} 34 | {# Log file not selected #} 35 | {% call empty('icon-file', _('No log file selected')) %} 36 |

{% trans %}Select a log file to show it's contents.{% endtrans %}

37 | {% endcall %} 38 | {% endif %} 39 |
40 | {% else %} 41 | {# Log file not selected #} 42 |
43 | {% call empty('icon-file', _('No log file available')) %} 44 |

{% trans %}The server is not configured to log in any file.{% endtrans %}

45 | {% endcall %} 46 |
47 | {% endif %} 48 |
49 | 50 | {% endblock %} 51 | -------------------------------------------------------------------------------- /rdiffweb/controller/dispatch.py: -------------------------------------------------------------------------------- 1 | # rdiffweb, A web interface to rdiff-backup repositories 2 | # Copyright (C) 2012-2025 rdiffweb contributors 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | """ 18 | Default page handler 19 | 20 | @author: Patrik Dufresne 21 | """ 22 | 23 | import cherrypy 24 | 25 | import rdiffweb.tools.auth # noqa 26 | import rdiffweb.tools.auth_mfa # noqa 27 | import rdiffweb.tools.ratelimit # noqa 28 | 29 | 30 | def staticdir(path, doc=''): 31 | """ 32 | Create a page handler to serve static directory. 33 | """ 34 | 35 | @cherrypy.expose 36 | @cherrypy.tools.auth(on=False) 37 | @cherrypy.tools.auth_mfa(on=False) 38 | @cherrypy.tools.ratelimit(on=False) 39 | @cherrypy.tools.sessions(on=False) 40 | @cherrypy.tools.secure_headers(on=False) 41 | @cherrypy.tools.staticdir(section="", dir=str(path)) 42 | def handler(*args, **kwargs): 43 | raise cherrypy.HTTPError(400) 44 | 45 | if doc: 46 | handler.__doc__ = doc 47 | return handler 48 | 49 | 50 | def staticfile(path, doc=''): 51 | """ 52 | Create a page handler to serve static file. 53 | """ 54 | 55 | @cherrypy.expose 56 | @cherrypy.tools.auth(on=False) 57 | @cherrypy.tools.auth_mfa(on=False) 58 | @cherrypy.tools.ratelimit(on=False) 59 | @cherrypy.tools.sessions(on=False) 60 | @cherrypy.tools.secure_headers(on=False) 61 | @cherrypy.tools.staticfile(filename=str(path)) 62 | def handler(*args, **kwargs): 63 | raise cherrypy.HTTPError(400) 64 | 65 | if doc: 66 | handler.__doc__ = doc 67 | return handler 68 | -------------------------------------------------------------------------------- /debian/copyright: -------------------------------------------------------------------------------- 1 | Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 2 | Upstream-Name: rdiffweb 3 | Upstream-Contact: Patrik Dufresne 4 | Source: https://gitlab.com/ikus-soft/rdiffweb/-/tags 5 | Files-Excluded: extras 6 | rdiffweb/static/css/bootstrap.min.css 7 | rdiffweb/static/css/bootstrap.min.css.map 8 | rdiffweb/static/css/font-awesome.css.map 9 | rdiffweb/static/css/font-awesome.min.css 10 | rdiffweb/static/css/jquery.dataTables.min.css 11 | rdiffweb/static/css/responsive.dataTables.min.css 12 | rdiffweb/static/fonts/fontawesome-webfont.eot 13 | rdiffweb/static/fonts/fontawesome-webfont.svg 14 | rdiffweb/static/fonts/fontawesome-webfont.woff 15 | rdiffweb/static/fonts/fontawesome-webfont.woff2 16 | rdiffweb/static/images/sort_asc.png 17 | rdiffweb/static/images/sort_asc_disabled.png 18 | rdiffweb/static/images/sort_both.png 19 | rdiffweb/static/images/sort_desc.png 20 | rdiffweb/static/images/sort_desc_disabled.png 21 | rdiffweb/static/js/bootstrap.bundle.js.map 22 | rdiffweb/static/js/bootstrap.bundle.min.js 23 | rdiffweb/static/js/chartkick.min.js 24 | rdiffweb/static/js/chart.min.js 25 | rdiffweb/static/js/dataTables.buttons.min.js 26 | rdiffweb/static/js/dataTables.responsive.min.js 27 | rdiffweb/static/js/jquery.dataTables.min.js 28 | rdiffweb/static/js/jquery.min.js 29 | 30 | Files: * 31 | Copyright: 2019-2025 Patrik Dufresne 32 | License: GPL-3+ 33 | 34 | License: GPL-3+ 35 | This program is free software: you can redistribute it and/or modify 36 | it under the terms of the GNU General Public License as published by 37 | the Free Software Foundation, either version 3 of the License, or 38 | (at your option) any later version. 39 | . 40 | This program is distributed in the hope that it will be useful, 41 | but WITHOUT ANY WARRANTY; without even the implied warranty of 42 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 43 | GNU General Public License for more details. 44 | . 45 | You should have received a copy of the GNU General Public License 46 | along with this program. If not, see . 47 | . 48 | The full text of the GNU General Public License version 3 49 | can be found in the file /usr/share/common-licenses/GPL-3. 50 | -------------------------------------------------------------------------------- /rdiffweb/controller/tests/test_page_admin_activity.py: -------------------------------------------------------------------------------- 1 | # rdiffweb, A web interface to rdiff-backup repositories 2 | # Copyright (C) 2012-2025 rdiffweb contributors 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | import rdiffweb.test 18 | 19 | 20 | class AdminActivityTest(rdiffweb.test.WebCase): 21 | login = True 22 | 23 | def test_get_activity(self): 24 | # Given an application with log file 25 | # When getting the system log page 26 | self.getPage("/admin/activity/") 27 | # Then the page return without error 28 | self.assertStatus(200) 29 | # Then an ajax table is displayed 30 | self.assertInBody('data-ajax="http://127.0.0.1:%s/admin/activity/data.json"' % self.PORT) 31 | 32 | def test_get_activity_selenium(self): 33 | # Given a user browsing the system logs. 34 | with self.selenium() as driver: 35 | # When getting web page. 36 | driver.get(self.baseurl + '/admin/activity/') 37 | # Then the web page contain a datatable 38 | driver.find_element('css selector', 'table[data-ajax]') 39 | # Then the web page is loaded without error. 40 | self.assertFalse(driver.get_log('browser')) 41 | # Then page contains system activity 42 | driver.implicitly_wait(10) 43 | driver.find_element('xpath', "//*[contains(text(), 'Created')]") 44 | 45 | def test_data_json(self): 46 | # Given a database with system activity 47 | # When getting data.json 48 | data = self.getJson("/admin/activity/data.json") 49 | # Then it contains activity data. 50 | self.assertTrue(len(data['data']) > 1) 51 | -------------------------------------------------------------------------------- /rdiffweb/core/model/_timestamp.py: -------------------------------------------------------------------------------- 1 | # rdiffweb, A web interface to rdiff-backup repositories 2 | # Copyright (C) 2023-2025 rdiffweb contributors 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | from datetime import datetime, timezone 17 | 18 | from sqlalchemy import TypeDecorator 19 | from sqlalchemy.ext.compiler import compiles 20 | from sqlalchemy.sql.functions import GenericFunction 21 | from sqlalchemy.sql.sqltypes import DateTime 22 | 23 | 24 | class epoch(GenericFunction): 25 | name = "epoch" 26 | inherit_cache = True 27 | 28 | 29 | @compiles(epoch, "postgresql") 30 | def _render_to_tsvector_of_pg(element, compiler, **kw): 31 | """ 32 | On Postgresql, use extract(epoch FROM ...) 33 | """ 34 | return "extract(epoch FROM %s)" % compiler.process(element.clauses, **kw) 35 | 36 | 37 | @compiles(epoch, 'sqlite') 38 | def _render_to_tsvector_of_sqlite(element, compiler, **kw): 39 | """ 40 | On SQLite, use STRFTIME('%s', ...) function. 41 | """ 42 | return "STRFTIME('%%s', %s)" % compiler.process(element.clauses, **kw) 43 | 44 | 45 | class Timestamp(TypeDecorator): 46 | cache_ok = True 47 | impl = DateTime 48 | 49 | def process_bind_param(self, value: datetime, dialect): 50 | if value is None: 51 | return None 52 | if value.tzinfo is None: 53 | raise ValueError('datetime without tzinfo: %s' % value) 54 | return value.astimezone(timezone.utc) 55 | 56 | def process_result_value(self, value, dialect): 57 | if value is None: 58 | return None 59 | if value.tzinfo is None: 60 | return value.replace(tzinfo=timezone.utc) 61 | return value.astimezone(timezone.utc) 62 | -------------------------------------------------------------------------------- /rdiffweb/controller/page_admin.py: -------------------------------------------------------------------------------- 1 | # rdiffweb, A web interface to rdiff-backup repositories 2 | # Copyright (C) 2012-2025 rdiffweb contributors 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | import datetime 18 | 19 | import cherrypy 20 | 21 | from rdiffweb.controller import Controller 22 | from rdiffweb.controller.page_admin_activity import AdminActivityPage 23 | from rdiffweb.controller.page_admin_logs import AdminLogsPage 24 | from rdiffweb.controller.page_admin_repos import AdminReposPage 25 | from rdiffweb.controller.page_admin_session import AdminSessionPage 26 | from rdiffweb.controller.page_admin_sysinfo import AdminSysinfoPage 27 | from rdiffweb.controller.page_admin_users import AdminUsersPage 28 | from rdiffweb.core.model import RepoObject, SessionObject, UserObject 29 | 30 | 31 | @cherrypy.tools.is_admin() 32 | class AdminPage(Controller): 33 | """ 34 | Administration pages. Allow to manage users database. 35 | """ 36 | 37 | logs = AdminLogsPage() 38 | repos = AdminReposPage() 39 | session = AdminSessionPage() 40 | sysinfo = AdminSysinfoPage() 41 | users = AdminUsersPage() 42 | activity = AdminActivityPage() 43 | 44 | @cherrypy.expose 45 | def index(self): 46 | """ 47 | Admin dashboard 48 | """ 49 | last_hour = datetime.datetime.now(tz=datetime.timezone.utc) - datetime.timedelta(hours=1) 50 | params = { 51 | "user_count": UserObject.query.count(), 52 | "repo_count": RepoObject.query.count(), 53 | "session_count": SessionObject.query.filter(SessionObject.access_time > last_hour).count(), 54 | } 55 | return self._compile_template("admin_overview.html", **params) 56 | -------------------------------------------------------------------------------- /rdiffweb/templates/restore.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | {% set active_page='repo' %} 3 | {% set active_repo_page='browse' %} 4 | {% block title %} 5 | {% trans %}Downloading{% endtrans %} 6 | {% endblock %} 7 | {% block head %} 8 | {{ super() }} 9 | {% if download_url %} 10 | {# Let use meta refresh to start download. #} 11 | 12 | {% endif %} 13 | {% endblock %} 14 | {% block body %} 15 |
16 |
17 | {% if download_url %} 18 |

19 | 20 | {% trans %}Your download will start shortly...{% endtrans %} 21 |

22 |

23 | 24 | {% trans %}Your download has started.{% endtrans %} 25 |

26 |

{% trans %}If the download doesn't start automatically, please click the link below:{% endtrans %}

27 | {% trans %}Download Now{% endtrans %} 28 | 40 | {% else %} 41 |

42 | 43 | {% trans %}Download is not possible in the current state of your repository:{% endtrans %} 44 |
45 |
46 | {{ repo.status[1] }} 47 |

48 | {% trans %}Go Back{% endtrans %} 49 | {% endif %} 50 |
51 |
52 | {% endblock %} 53 | -------------------------------------------------------------------------------- /rdiffweb/core/passwd.py: -------------------------------------------------------------------------------- 1 | # rdiffweb, A web interface to rdiff-backup repositories 2 | # Copyright (C) 2012-2025 rdiffweb contributors 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | import hashlib 18 | import os 19 | from base64 import b64decode, b64encode 20 | 21 | try: 22 | import argon2 23 | 24 | _argon = argon2.PasswordHasher() 25 | 26 | def hash_password(password): 27 | assert password and isinstance(password, str) 28 | return _argon.hash(password) 29 | 30 | except ImportError: 31 | _argon = None 32 | 33 | def hash_password(password): 34 | assert password and isinstance(password, str) 35 | password = password.encode(encoding='utf8') 36 | salt = os.urandom(4) 37 | h = hashlib.sha1(password) 38 | h.update(salt) 39 | return "{SSHA}" + b64encode(h.digest() + salt).decode('latin1') 40 | 41 | 42 | def check_password(password, challenge): 43 | """ 44 | Check if the password matches the challenge. 45 | The challenge is an encrypted password. 46 | """ 47 | if not password or not challenge: 48 | return False 49 | assert isinstance(password, str) 50 | assert isinstance(challenge, str) 51 | if _argon and challenge.startswith('$argon2'): 52 | try: 53 | return _argon.verify(challenge, password) 54 | except Exception: 55 | return False 56 | elif challenge.startswith('{SSHA}'): 57 | digest_salt = b64decode(challenge[6:]) 58 | digest = digest_salt[:20] 59 | sha = hashlib.sha1(password.encode(encoding='utf8')) 60 | sha.update(digest_salt[20:]) 61 | return digest == sha.digest() 62 | else: 63 | # Fallback to previous SHA 64 | sha = hashlib.sha1(password.encode('utf8')) 65 | return challenge == sha.hexdigest() 66 | -------------------------------------------------------------------------------- /rdiffweb/templates/prefs_sshkeys.html: -------------------------------------------------------------------------------- 1 | {% extends 'prefs.html' %} 2 | {% set active_panelid='sshkeys' %} 3 | {% block panel %} 4 | {% include 'message.html' %} 5 | {% from 'include/modal_dialog.html' import modal_dialog, button_confirm, modal_confirm %} 6 | {% if disable_ssh_keys %} 7 | {% trans %}SSH Keys management is disabled by your administrator.{% endtrans %} 8 | {% else %} 9 |
10 |

{% trans %}SSH keys{% endtrans %}

11 | 17 |
18 |

19 | {% trans %}SSH keys allow you to establish a secure connection between your computer and this backup system. This is a list of SSH keys associated with your account. Remove any keys that you do not recognize.{% endtrans %} 20 |

21 | {% if not is_maintainer %} 22 |

{% trans %}Ask your administrator to delete SSH Keys.{% endtrans %}

23 | {% endif %} 24 | 25 |
    26 | {% for key in sshkeys %} 27 |
  • 28 |
    29 | {{ button_confirm(label=_('Delete'), target="#delete-sshkey-modal", action="delete", fingerprint=key.fingerprint, disabled=not is_maintainer) }} 30 |
    31 | {{ key.title }} 32 |

    {{ key.fingerprint }}

    33 |
  • 34 | {% else %} 35 |
  • 36 | {% trans %}There are no SSH keys associated with your account.{% endtrans %} 37 |
  • 38 | {% endfor %} 39 |
40 | {# djlint:off #} 41 | {# Dialog to create SSH key. #} 42 | {% call modal_dialog('add-sshkey-modal',_('Add SSH key'), _('Add SSH key')) %} 43 | 44 | {{ form }} 45 | {% endcall %} 46 | {# djlint:on #} 47 | 48 | {{ modal_confirm( 49 | id='delete-sshkey-modal', 50 | title=_('Delete SSH key'), 51 | message=_("Are you sure you want to delete this SSH Key?"), 52 | fields=['action', 'fingerprint'], 53 | submit=_('Delete')) }} 54 | {% endif %} 55 | {% endblock %} 56 | -------------------------------------------------------------------------------- /rdiffweb/controller/formdb.py: -------------------------------------------------------------------------------- 1 | # rdiffweb, A web interface to rdiff-backup repositories 2 | # Copyright (C) 2012-2025 rdiffweb contributors 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | import logging 18 | 19 | import cherrypy 20 | from sqlalchemy.exc import IntegrityError 21 | from wtforms.validators import ValidationError 22 | 23 | from rdiffweb.tools.i18n import gettext_lazy as _ 24 | 25 | from .form import CherryForm 26 | 27 | logger = logging.getLogger(__name__) 28 | 29 | Session = cherrypy.db.get_session() 30 | 31 | 32 | class DbForm(CherryForm): 33 | """ 34 | Special form to handle saving form to database. 35 | """ 36 | 37 | def save_to_db(self, obj): 38 | 39 | form_errors = self.form_errors if hasattr(self, 'form_errors') else self.errors.setdefault(None, []) 40 | try: 41 | self.populate_obj(obj) 42 | Session.commit() 43 | return True 44 | except (ValueError, ValidationError) as e: 45 | Session.rollback() 46 | form_errors.append(str(e)) 47 | return False 48 | except IntegrityError as e: 49 | Session.rollback() 50 | if e.constraint and e.constraint.info and 'error_message' in e.constraint.info: 51 | error_message = e.constraint.info['error_message'] 52 | form_errors.append(error_message) 53 | else: 54 | form_errors.append(_("A database error occurred. Please check your input.")) 55 | logger.error("database error occurred", exc_info=1) 56 | return False 57 | except Exception: 58 | Session.rollback() 59 | logger.exception("unexpected error occurred while saving data", exc_info=1) 60 | form_errors.append(_("An unexpected error occurred while saving your data.")) 61 | return False 62 | -------------------------------------------------------------------------------- /rdiffweb/static/css/responsive.dataTables.min.css: -------------------------------------------------------------------------------- 1 | table.dataTable.dtr-inline.collapsed>tbody>tr>td:first-child,table.dataTable.dtr-inline.collapsed>tbody>tr>th:first-child{position:relative;padding-left:30px;cursor:pointer}table.dataTable.dtr-inline.collapsed>tbody>tr>td:first-child:before,table.dataTable.dtr-inline.collapsed>tbody>tr>th:first-child:before{top:8px;left:4px;height:16px;width:16px;display:block;position:absolute;color:white;border:2px solid white;border-radius:16px;text-align:center;line-height:14px;box-shadow:0 0 3px #444;box-sizing:content-box;content:'+';background-color:#31b131}table.dataTable.dtr-inline.collapsed>tbody>tr>td:first-child.dataTables_empty:before,table.dataTable.dtr-inline.collapsed>tbody>tr>th:first-child.dataTables_empty:before{display:none}table.dataTable.dtr-inline.collapsed>tbody>tr.parent>td:first-child:before,table.dataTable.dtr-inline.collapsed>tbody>tr.parent>th:first-child:before{content:'-';background-color:#d33333}table.dataTable.dtr-inline.collapsed>tbody>tr.child td:before{display:none}table.dataTable.dtr-inline.collapsed.compact>tbody>tr>td:first-child,table.dataTable.dtr-inline.collapsed.compact>tbody>tr>th:first-child{padding-left:27px}table.dataTable.dtr-inline.collapsed.compact>tbody>tr>td:first-child:before,table.dataTable.dtr-inline.collapsed.compact>tbody>tr>th:first-child:before{top:5px;left:4px;height:14px;width:14px;border-radius:14px;line-height:12px}table.dataTable.dtr-column>tbody>tr>td.control,table.dataTable.dtr-column>tbody>tr>th.control{position:relative;cursor:pointer}table.dataTable.dtr-column>tbody>tr>td.control:before,table.dataTable.dtr-column>tbody>tr>th.control:before{top:50%;left:50%;height:16px;width:16px;margin-top:-10px;margin-left:-10px;display:block;position:absolute;color:white;border:2px solid white;border-radius:16px;text-align:center;line-height:14px;box-shadow:0 0 3px #444;box-sizing:content-box;content:'+';background-color:#31b131}table.dataTable.dtr-column>tbody>tr.parent td.control:before,table.dataTable.dtr-column>tbody>tr.parent th.control:before{content:'-';background-color:#d33333}table.dataTable>tbody>tr.child{padding:0.5em 1em}table.dataTable>tbody>tr.child:hover{background:transparent !important}table.dataTable>tbody>tr.child ul{display:inline-block;list-style-type:none;margin:0;padding:0}table.dataTable>tbody>tr.child ul li{border-bottom:1px solid #efefef;padding:0.5em 0}table.dataTable>tbody>tr.child ul li:first-child{padding-top:0}table.dataTable>tbody>tr.child ul li:last-child{border-bottom:none}table.dataTable>tbody>tr.child span.dtr-title{display:inline-block;min-width:75px;font-weight:bold} 2 | -------------------------------------------------------------------------------- /rdiffweb/templates/prefs_session.html: -------------------------------------------------------------------------------- 1 | {% extends 'prefs.html' %} 2 | {% from 'include/modal_dialog.html' import modal_dialog, button_confirm, modal_confirm %} 3 | {% from 'include/session.html' import browser, os %} 4 | {% set active_panelid='session' %} 5 | {% block panel %} 6 | {% include 'message.html' %} 7 |
8 |

{% trans %}Active Sessions{% endtrans %}

9 |
10 |

11 | {% trans %}This is a list of devices that are logged into your account. You may revoke any sessions that you do not recognize except your current session.{% endtrans %} 12 |

13 | 14 |
    15 | {% for session in active_sessions %} 16 |
  • 17 |
    18 | {% if not session.current %} 19 | {{ button_confirm(label=_('Revoke'), target="#delete-session-modal", action="delete", number=session.number) }} 20 | {% endif %} 21 |
    22 |

    23 | {{ browser(session.user_agent) }} 24 | {% trans %}running on{% endtrans %} 25 | {{ os(session.user_agent)}} 26 | {% if session.current %} 27 | {% trans %}current session{% endtrans %} 28 | {% endif %} 29 | {% if session.login_persistent %} 30 | {% trans %}persistent{% endtrans %} 31 | {% endif %} 32 |

    33 |

    34 | {% trans %}Last accessed{% endtrans %} 35 | 36 | {% trans %}from{% endtrans %} 37 | {{ session.ip_address }} 38 |
    39 | {% trans %}Signed in{% endtrans %} 40 | 41 |
    42 | {% trans %}Expired on{% endtrans %} 43 | 44 |

    45 |
  • 46 | {% endfor %} 47 |
48 | {{ modal_confirm( 49 | id='delete-session-modal', 50 | title=_('Revoke Session'), 51 | message=_("Are you sure? The device will be signed out from the application."), 52 | fields=['action', 'number'], 53 | submit=_('Revoke')) }} 54 | {% endblock %} 55 | -------------------------------------------------------------------------------- /rdiffweb/templates/logs.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout_repo.html' %} 2 | {% from 'include/empty.html' import empty %} 3 | {% from 'components/log.html' import pre_code %} 4 | {% set active_page='repo' %} 5 | {% set active_repo_page='logs' %} 6 | {% block title %} 7 | {% trans %}Repository Logs{% endtrans %} 8 | {% endblock %} 9 | {% block content %} 10 |
11 | 34 |
35 | {% if data %} 36 |
37 | {% trans %}Notice: To prevent performance issues, only the last 2000 lines of each log files are displayed.{% endtrans %} 38 | {% trans %}Show all logs{% endtrans %} 39 |
40 | {{ pre_code(data)}} 41 | {% elif file or date %} 42 | {% call empty('icon-file', _('Log file empty')) %} 43 |

{% trans %}This log file is empty. Select another log file to show it's contents.{% endtrans %}

44 | {% endcall %} 45 | {% else %} 46 | {% call empty('icon-file', _('No log file selected')) %} 47 |

{% trans %}Select a log file to show it's contents.{% endtrans %}

48 | {% endcall %} 49 | {% endif %} 50 |
51 |
52 | 53 | {% endblock %} 54 | -------------------------------------------------------------------------------- /rdiffweb/core/remove_older.py: -------------------------------------------------------------------------------- 1 | # rdiffweb, A web interface to rdiff-backup repositories 2 | # Copyright (C) 2012-2025 rdiffweb contributors 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | import logging 18 | 19 | import cherrypy 20 | from cherrypy.process.plugins import SimplePlugin 21 | 22 | from rdiffweb.core import librdiff 23 | from rdiffweb.core.model import RepoObject 24 | 25 | _logger = logging.getLogger(__name__) 26 | 27 | 28 | class RemoveOlder(SimplePlugin): 29 | execution_time = '23:00' 30 | 31 | def start(self): 32 | self.bus.log('Start RemoveOlder plugin') 33 | self.bus.publish( 34 | 'scheduler:add_job_daily', self.execution_time, f'{self.__module__}:cherrypy.remove_older.remove_older_job' 35 | ) 36 | 37 | def stop(self): 38 | self.bus.log('Stop RemoveOlder plugin') 39 | self.bus.publish('scheduler:remove_job', f'{self.__module__}:cherrypy.remove_older.remove_older_job') 40 | 41 | def graceful(self): 42 | """Reload of subscribers.""" 43 | self.stop() 44 | self.start() 45 | 46 | def remove_older_job(self): 47 | # Create a generator to loop on repositories. 48 | # Loop on each repos. 49 | for repo in RepoObject.query.filter(RepoObject.keepdays > 0).all(): 50 | try: 51 | # Check history date. 52 | if not repo.last_backup_date: 53 | _logger.info("no backup dates for [%r]", repo.full_path) 54 | continue 55 | d = librdiff.RdiffTime() - repo.last_backup_date 56 | d = d.days + repo.keepdays 57 | repo.remove_older(d) 58 | except Exception: 59 | _logger.exception("fail to remove older for user [%r] repo [%r]", repo.owner, repo) 60 | 61 | 62 | cherrypy.remove_older = RemoveOlder(cherrypy.engine) 63 | cherrypy.remove_older.subscribe() 64 | 65 | cherrypy.config.namespaces['remove_older'] = lambda key, value: setattr(cherrypy.remove_older, key, value) 66 | -------------------------------------------------------------------------------- /debian/rdiffweb.links: -------------------------------------------------------------------------------- 1 | /usr/share/fonts-font-awesome/css/font-awesome.css.map /usr/lib/python3/dist-packages/rdiffweb/static/css/font-awesome.css.map 2 | /usr/share/fonts-font-awesome/css/font-awesome.min.css /usr/lib/python3/dist-packages/rdiffweb/static/css/font-awesome.min.css 3 | /usr/share/fonts-font-awesome/fonts/fontawesome-webfont.eot /usr/lib/python3/dist-packages/rdiffweb/static/fonts/fontawesome-webfont.eot 4 | /usr/share/fonts-font-awesome/fonts/fontawesome-webfont.svg /usr/lib/python3/dist-packages/rdiffweb/static/fonts/fontawesome-webfont.svg 5 | /usr/share/fonts-font-awesome/fonts/fontawesome-webfont.woff2 /usr/lib/python3/dist-packages/rdiffweb/static/fonts/fontawesome-webfont.woff2 6 | /usr/share/fonts-font-awesome/fonts/fontawesome-webfont.woff /usr/lib/python3/dist-packages/rdiffweb/static/fonts/fontawesome-webfont.woff 7 | /usr/share/javascript/bootstrap4/css/bootstrap.min.css.map /usr/lib/python3/dist-packages/rdiffweb/static/css/bootstrap.min.css.map 8 | /usr/share/javascript/bootstrap4/css/bootstrap.min.css /usr/lib/python3/dist-packages/rdiffweb/static/css/bootstrap.min.css 9 | /usr/share/javascript/bootstrap4/js/bootstrap.bundle.js.map /usr/lib/python3/dist-packages/rdiffweb/static/js/bootstrap.bundle.js.map 10 | /usr/share/javascript/bootstrap4/js/bootstrap.bundle.min.js /usr/lib/python3/dist-packages/rdiffweb/static/js/bootstrap.bundle.min.js 11 | /usr/share/javascript/jquery-datatables/css/jquery.dataTables.min.css /usr/lib/python3/dist-packages/rdiffweb/static/css/jquery.dataTables.min.css 12 | /usr/share/javascript/jquery-datatables-extensions/Buttons/js/dataTables.buttons.min.js /usr/lib/python3/dist-packages/rdiffweb/static/js/dataTables.buttons.min.js 13 | /usr/share/javascript/jquery-datatables-extensions/Responsive/css/responsive.dataTables.min.css /usr/lib/python3/dist-packages/rdiffweb/static/css/responsive.dataTables.min.css 14 | /usr/share/javascript/jquery-datatables-extensions/Responsive/js/dataTables.responsive.min.js /usr/lib/python3/dist-packages/rdiffweb/static/js/dataTables.responsive.min.js 15 | /usr/share/javascript/jquery-datatables/images/sort_asc.png /usr/lib/python3/dist-packages/rdiffweb/static/images/sort_asc.png 16 | /usr/share/javascript/jquery-datatables/images/sort_both.png /usr/lib/python3/dist-packages/rdiffweb/static/images/sort_both.png 17 | /usr/share/javascript/jquery-datatables/images/sort_desc.png /usr/lib/python3/dist-packages/rdiffweb/static/images/sort_desc.png 18 | /usr/share/javascript/jquery-datatables/jquery.dataTables.min.js /usr/lib/python3/dist-packages/rdiffweb/static/js/jquery.dataTables.min.js 19 | /usr/share/javascript/jquery/jquery.min.js /usr/lib/python3/dist-packages/rdiffweb/static/js/jquery.min.js 20 | -------------------------------------------------------------------------------- /rdiffweb/controller/tests/test_page_error.py: -------------------------------------------------------------------------------- 1 | # rdiffweb, A web interface to rdiff-backup repositories 2 | # Copyright (C) 2012-2025 rdiffweb contributors 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | from parameterized import parameterized_class 18 | 19 | import rdiffweb.test 20 | 21 | 22 | @parameterized_class( 23 | [ 24 | {"default_config": {'environment': 'production'}, "expect_stacktrace": False}, 25 | {"default_config": {'environment': 'development'}, "expect_stacktrace": True}, 26 | {"default_config": {'debug': False}, "expect_stacktrace": False}, 27 | {"default_config": {'debug': True}, "expect_stacktrace": True}, 28 | ] 29 | ) 30 | class ErrorPageTest(rdiffweb.test.WebCase): 31 | login = True 32 | 33 | def test_error_page_html(self): 34 | # When browsing the invalid URL 35 | self.getPage('/invalid/') 36 | # Then a 404 error page is return using jinja2 template 37 | self.assertStatus("404 Not Found") 38 | self.assertInBody("Oops!") 39 | if self.expect_stacktrace: 40 | self.assertInBody('Traceback (most recent call last):') 41 | else: 42 | self.assertNotInBody('Traceback (most recent call last):') 43 | 44 | def test_not_found(self): 45 | # When user browser an invalid path. 46 | self.getPage( 47 | '/This%20website%20has%20been%20hacked%20and%20the%20confidential%20data%20of%20all%20users%20have%20been%20compromised%20and%20leaked%20to%20public' 48 | ) 49 | # Then an error page is return 50 | self.assertStatus("404 Not Found") 51 | # Then page doesn't make reference to the path. 52 | if self.expect_stacktrace: 53 | self.assertInBody( 54 | 'This website has been hacked and the confidential data of all users have been compromised and leaked to public' 55 | ) 56 | else: 57 | self.assertNotInBody( 58 | 'This website has been hacked and the confidential data of all users have been compromised and leaked to public' 59 | ) 60 | -------------------------------------------------------------------------------- /doc/api.md: -------------------------------------------------------------------------------- 1 | # RESTful API 2 | 3 | ## Overview 4 | 5 | Rdiffweb provides a RESTful API that allows users to interact with the application programmatically. The API is accessible via the `/api` endpoint, and different endpoints provide access to various functionalities, including retrieving application information, managing user settings, working with access tokens, SSH keys, and repository settings. 6 | 7 | ## Authentication 8 | 9 | The REST API supports two modes of authentication: 10 | 11 | 1. **Username and Password:** The same credentials used for authenticating via the web interface. 12 | 2. **Username and Access Token:** When Multi-Factor Authentication (MFA) is enabled, this mode is supported. Access tokens act as passwords, and their scope may limit access to specific API endpoints. 13 | 14 | ## Input Payloads 15 | 16 | The Rdiffweb RESTful API supports input payloads in two commonly used formats: `application/json` and `application/x-www-form-urlencoded`. This flexibility allows users to choose the payload format that best suits their needs when interacting with the API. 17 | 18 | - **`application/json`**: Use this format for JSON-encoded data. The payload should be a valid JSON object sent in the request body. 19 | 20 | - **`application/x-www-form-urlencoded`**: This format is suitable for URL-encoded data, typically used in HTML forms. Key-value pairs are sent in the request body with a `Content-Type` header set to `application/x-www-form-urlencoded`. 21 | 22 | Please ensure that the appropriate `Content-Type` header is set in your API requests to match the payload format being used. 23 | 24 | ### Example using cURL 25 | 26 | Here's an example of using cURL to make a request to the Rdiffweb API with a JSON payload: 27 | 28 | ```bash 29 | # Example using application/json payload 30 | curl -u admin:admin123 -X POST -H "Content-Type: application/json" -d '{"fullname": "John Doe", "email": "john@example.com", "lang": "en", "report_time_range": "30"}' https://example.com/api/currentuser 31 | ``` 32 | 33 | And for a request with `application/x-www-form-urlencoded` payload: 34 | 35 | ```bash 36 | # Example using application/x-www-form-urlencoded payload 37 | curl -u admin:admin123 -X POST -H "Content-Type: application/x-www-form-urlencoded" -d 'fullname=John%20Doe&email=john@example.com&lang=en&report_time_range=30' https://example.com/api/currentuser 38 | ``` 39 | 40 | Adjust the payload data and endpoint URL accordingly based on your specific use case. 41 | 42 | --- 43 | 44 | All available endpoints are documented using the OpenAPI (Swagger) specification, accessible from your Rdiffweb server at https://example.com/api/openapi.json. Access to the URL requires authentication. 45 | 46 | ```{toctree} 47 | --- 48 | maxdepth: 2 49 | --- 50 | endpoints 51 | ``` 52 | -------------------------------------------------------------------------------- /rdiffweb/controller/tests/test_page_admin_repos.py: -------------------------------------------------------------------------------- 1 | # rdiffweb, A web interface to rdiff-backup repositories 2 | # Copyright (C) 2012-2025 rdiffweb contributors 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | 18 | import rdiffweb.test 19 | from rdiffweb.core.model import RepoObject, UserObject 20 | 21 | 22 | class AdminReposTest(rdiffweb.test.WebCase): 23 | login = True 24 | 25 | def test_repos(self): 26 | # Given an admin user with repos 27 | repos = ( 28 | RepoObject.query.join(UserObject, RepoObject.userid == UserObject.id) 29 | .filter(UserObject.username == self.USERNAME) 30 | .all() 31 | ) 32 | # When querying the repository page 33 | self.getPage("/admin/repos/") 34 | self.assertStatus(200) 35 | # Then the page contains our repos. 36 | for repo in repos: 37 | self.assertInBody(repo.name) 38 | 39 | def test_repos_with_maxage(self): 40 | # Given an admin user with repos 41 | repos = ( 42 | RepoObject.query.join(UserObject, RepoObject.userid == UserObject.id) 43 | .filter(UserObject.username == self.USERNAME) 44 | .all() 45 | ) 46 | # Given a repo with maxage 47 | repos[0].maxage = 3 48 | repos[0].commit() 49 | # When querying the repository page 50 | self.getPage("/admin/repos/") 51 | self.assertStatus(200) 52 | # Then the page contains "3 days". 53 | self.assertInBody("3 days") 54 | 55 | def test_repos_with_keepdays(self): 56 | # Given an admin user with repos 57 | repos = ( 58 | RepoObject.query.join(UserObject, RepoObject.userid == UserObject.id) 59 | .filter(UserObject.username == self.USERNAME) 60 | .all() 61 | ) 62 | # Given a repo with maxage 63 | repos[0].keepdays = 6 64 | repos[0].commit() 65 | # When querying the repository page 66 | self.getPage("/admin/repos/") 67 | self.assertStatus(200) 68 | # Then the page contains "3 days". 69 | self.assertInBody("6 days") 70 | -------------------------------------------------------------------------------- /rdiffweb/templates/components/form.html: -------------------------------------------------------------------------------- 1 | {% set bootstrap_class_table = { 2 | 'CheckboxInput': 'form-check-input', 3 | 'EmailInput': 'form-control', 4 | 'PasswordInput': 'form-control', 5 | 'Select': 'form-control', 6 | 'SubmitInput': 'btn', 7 | 'TextArea': 'form-control', 8 | 'TextInput': 'form-control', 9 | } %} 10 | {% for id, field in form._fields.items() %} 11 | {% if field.widget['input_type'] == 'hidden' %} 12 | {{ field(id=False) }} 13 | {% else %} 14 | {% set extra_label_class = field.errors and ' is-invalid' or '' %} 15 | {% set field_class = bootstrap_class_table.get(field.widget.__class__.__name__) %} 16 | {% if field.render_kw and field.render_kw.get('class') %} 17 | {% set field_class = field_class + ' ' + field.render_kw.get('class') %} 18 | {% endif %} 19 | {% if field.widget.__class__.__name__ == 'SubmitInput' %} 20 |
21 | {{ field(id=False, class=field_class + (' btn-primary' if 'btn-' not in field_class else '')) }} 22 | {% if field.description %}
{{ field.description }}
{% endif %} 23 | {% for error in field.errors %}
{{ error }}
{% endfor %} 24 |
25 | {% elif field.widget.__class__.__name__ == 'CheckboxInput' %} 26 |
27 | {{ field(class=field_class) }} 28 | {{ field.label(class="font-weight-bold" + extra_label_class) }} 29 | {% if field.description %}
{{ field.description }}
{% endif %} 30 | {% for error in field.errors %}
{{ error }}
{% endfor %} 31 |
32 | {% elif field.option_widget.__class__.__name__ == 'RadioInput' %} 33 |
34 | {{ field.label(class="font-weight-bold" + extra_label_class) }} 35 | {% for subfield in field %} 36 |
37 | {{ subfield(class="form-check-input") }} 38 | {{ subfield.label(class="form-check-label") }} 39 |
40 | {% endfor %} 41 | {% if field.description %}
{{ field.description }}
{% endif %} 42 | {% for error in field.errors %}
{{ error }}
{% endfor %} 43 |
44 | {% else %} 45 |
46 | {{ field.label(class="font-weight-bold" + extra_label_class) }} 47 | {{ field(id=False, class=field_class) }} 48 | {% if field.description %}
{{ field.description }}
{% endif %} 49 | {% for error in field.errors %}
{{ error }}
{% endfor %} 50 |
51 | {% endif %} 52 | {% endif %} 53 | {% endfor %} 54 | -------------------------------------------------------------------------------- /doc/hardening.md: -------------------------------------------------------------------------------- 1 | # Server Hardening 2 | 3 | Server hardening involves implementing various security measures to protect your server from unauthorized access, attacks, and vulnerabilities. By following these steps, you can enhance the security posture of your Rdiffweb server. 4 | 5 | ## Configure a Reverse Proxy 6 | 7 | Setting up a reverse proxy can provide an additional layer of security and improve the performance and scalability of your web applications. 8 | 9 | Read more: 10 | 11 | ```{toctree} 12 | --- 13 | titlesonly: true 14 | --- 15 | networking 16 | ``` 17 | 18 | ## Encrypt Network Traffic (SSL) 19 | 20 | Configure you server to make use of secure protocols like SSL/TLS to encrypt network traffic. 21 | Obtain and install valid SSL certificates from trusted certificate authorities. 22 | 23 | [How to configure letencrypt](https://wiki.debian.org/LetsEncrypt) 24 | 25 | ## Configure Firewall 26 | 27 | Set up a firewall to control incoming and outgoing network traffic. Only allow necessary ports and protocols. 28 | You should consider to expose only ports 80 (http), 433 (https) and 22 (ssh). 29 | Make sure you are not exposing default port 8080 used by default by Rdiffweb for unsecure communication. 30 | Close all unused ports and services to minimize potential attack vectors. 31 | Implement a default deny policy, only permitting essential services. 32 | 33 | ## Secure remote SSH access 34 | 35 | The SSH (Secure Shell) server is a critical component of remote server access. Implementing proper security measures for SSH helps protect against unauthorized access and potential attacks. 36 | 37 | 1. Disable SSH protocol versions 1 and use only SSH protocol version 2, which offers stronger security. 38 | 2. Change the default SSH port (typically 22) to a non-standard port to reduce visibility to potential attackers. Choose a port outside the well-known port range. 39 | 3. Configure SSH to only allow specific users or groups to connect, limiting access to authorized individuals. 40 | 4. Implement key-based authentication instead of relying solely on passwords. Generate SSH key pairs for each user and disable password-based authentication. 41 | 5. Enforce strong password policies for SSH users, including the use of complex, unique passwords. 42 | 6. Restrict SSH access to specific IP addresses or IP ranges using firewall rules or TCP wrappers. This helps limit access to trusted networks or specific machines. 43 | 7. Implement SSH brute-force attack protection mechanisms, such as [fail2ban](fail2ban.md), to automatically block IP addresses that exhibit suspicious behavior. 44 | 45 | Read more: 46 | 47 | ```{toctree} 48 | --- 49 | titlesonly: true 50 | --- 51 | fail2ban 52 | ``` 53 | 54 | ## Update and Patch Management 55 | 56 | Regularly update and patch your server's operating system, software, and applications to ensure you have the latest security fixes and bug patches. 57 | Enable automatic updates or establish a systematic update process. 58 | Rdiffweb is continiously updated with secutiry enhancements. 59 | 60 | -------------------------------------------------------------------------------- /rdiffweb/plugins/restapi.py: -------------------------------------------------------------------------------- 1 | # RestAPI plugin for cherrypy 2 | # Copyright (C) 2024-2025 IKUS Software 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | import cherrypy 18 | 19 | 20 | class Dispatcher(cherrypy.dispatch.Dispatcher): 21 | """ 22 | Dispatcher using HTTP method to find the proper function to be called. 23 | 24 | e.g.: 25 | GET /api/users -> list() 26 | GET /api/users/3 -> get(3) 27 | GET /api/users/my/path -> get('my/path') 28 | DELETE /api/users/3 -> delete(3) 29 | DELETE /api/users/my/path -> delete('my/path') 30 | 31 | POST /api/users -> post(data) 32 | POST /api/users/3 -> post(3, data) 33 | PUT /api/users -> post(data) 34 | PUT /api/users/3 -> post(3, data) 35 | 36 | """ 37 | 38 | supported_method = ['get', 'delete', 'post', 'put'] 39 | 40 | def __call__(self, path): 41 | request = cherrypy.serving.request 42 | # To simplify implementation reuse default dispatcher function. 43 | # But strip the last segment 44 | resource, vpath = self.find_handler(path) 45 | 46 | if not resource: 47 | request.handler = cherrypy.NotFound() 48 | return 49 | 50 | # Two scenario possible. Either we have found our GET handler. 51 | # Or we need to look into the resource for it. 52 | meth = request.method.lower() 53 | if meth not in self.supported_method: 54 | request.handler = cherrypy.HTTPError(405) 55 | if meth == 'get' and not hasattr(resource, 'list') and not hasattr(resource, 'get'): 56 | request.handler = cherrypy.HTTPError(405) 57 | return 58 | 59 | # Call "list()" instead of "get()" when path doesn't have an id or name. 60 | if meth == 'get' and not vpath and hasattr(resource, 'list'): 61 | meth = 'list' 62 | # Find the subhandler 63 | func = getattr(resource, meth, None) 64 | if func: 65 | # Grab any _cp_config on the subhandler. 66 | if hasattr(func, '_cp_config'): 67 | request.config.update(func._cp_config) 68 | 69 | # Decode any leftover %2F in the virtual_path atoms. 70 | if vpath: 71 | vpath = ['/'.join([x.replace('%2F', '/') for x in vpath])] 72 | request.handler = cherrypy.dispatch.LateParamPageHandler(func, *vpath) 73 | else: 74 | request.handler = cherrypy.HTTPError(405) 75 | -------------------------------------------------------------------------------- /rdiffweb/templates/stats.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout_repo.html' %} 2 | {% from 'include/empty.html' import empty %} 3 | {% from 'include/table.html' import table %} 4 | {% set active_page='repo' %} 5 | {% set active_repo_page='stats' %} 6 | {% block title %} 7 | {% trans %}Snapshot Changes{% endtrans %} 8 | {% endblock %} 9 | {% block content %} 10 |
11 |
12 | 24 |
25 |
26 |

{% trans %}Snapshot Changes{% endtrans %}

27 |

28 | {% trans %}The snapshot changes view presents a comprehensive list of changes in the backup, including new, deleted and changed files, allowing users to easily track and manage modifications to their data.{% endtrans %} 29 |

30 | {% if date %} 31 | {% set buttons = [ 32 | {'text': _('All'), 'extend': 'clear'}, 33 | {'text': _('New'), 'extend': 'filter', 'column': 'state:name', 'search': 'new'}, 34 | {'text': _('Deleted'), 'extend': 'filter', 'column': 'state:name', 'search': 'deleted'}, 35 | {'text': _('Changed'), 'extend': 'filter', 'column': 'state:name', 'search': '^changed', 'regex':True}, 36 | {'text': _('Unchanged'), 'extend': 'filter', 'column': 'state:name', 'search': '^unchanged'}, 37 | ] %} 38 | {% set columns = [ 39 | {'name':'path', 'title': _('Path'), 'orderable': True, 'render':'text' }, 40 | {'name':'state', 'title':_('State'), 'orderable': True, 'render':'choices', 'render_arg': [ ['new',_('New')], ['deleted',_('Deleted')], ['changed',_('Changed')], ['unchanged',_('Unchanged')]] }, 41 | {'name':'size', 'title':_('Size'), 'orderable': True, 'render':'filesize', 'type':'num' }, 42 | {'name':'increment_size', 'title':_('Increment Size'), 'orderable': True, 'render':'filesize', 'type':'num' }, 43 | ] %} 44 | {{ table(url_for('stats', 'data.json', repo, date=date), 45 | columns=columns, 46 | buttons=buttons, 47 | order=[[ 0, 'desc' ]], 48 | searching=True, 49 | empty_message=_('Changes are not available'), 50 | info_message=_('Displaying _START_-_END_ of _TOTAL_ changes'), 51 | paging=True, 52 | page_length=20) }} 53 | {% else %} 54 | {% call empty('fa fa-list-alt', _('No snapshot selected')) %} 55 |

{% trans %}Select a backup date on the left to display detailed changes for that date.{% endtrans %}

56 | {% endcall %} 57 | {% endif %} 58 |
59 |
60 | 61 | {% endblock %} 62 | -------------------------------------------------------------------------------- /rdiffweb/controller/tests/test_check_links.py: -------------------------------------------------------------------------------- 1 | # rdiffweb, A web interface to rdiff-backup repositories 2 | # Copyright (C) 2012-2025 rdiffweb contributors 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | import re 18 | from collections import OrderedDict 19 | 20 | import rdiffweb.test 21 | 22 | 23 | class CheckLinkTest(rdiffweb.test.WebCase): 24 | login = True 25 | 26 | start_url = [ 27 | "/", 28 | ] 29 | 30 | ignore_url = [ 31 | '.*/logout', 32 | '.*/restore/admin/testcases/BrokenSymlink.*', 33 | '.*/browse/admin/testcases/BrokenSymlink.*', 34 | '.*/history/admin/testcases/BrokenSymlink.*', 35 | 'https://www.ikus-soft.com/.*', 36 | 'https://rdiffweb.org/.*', 37 | '.*js', 38 | # Bug in rdiff-backup >=2.2.x with test\test - see https://github.com/rdiff-backup/rdiff-backup/issues/1040 39 | '.*/restore/admin/testcases/test.*test', 40 | ] 41 | 42 | def test_links(self): 43 | """ 44 | Crawl all the pages to find broken links or relative links. 45 | """ 46 | done = set(['#']) 47 | todo = OrderedDict() 48 | for url in self.start_url: 49 | todo[url] = 'start' 50 | # Store the original cookie since it get replace during execution. 51 | self.assertIsNotNone(self.cookies) 52 | cookies = self.cookies 53 | while todo: 54 | page, ref = todo.popitem(last=False) 55 | # Query page 56 | self.cookies = cookies 57 | self.getPage(page) 58 | # Check status 59 | if int(self.status[:3]) in [301, 303]: 60 | newpage = self.assertHeader('Location') 61 | todo[newpage] = page 62 | else: 63 | self.assertStatus('200 OK', "can't access page [%s] referenced by [%s]" % (page, ref)) 64 | 65 | # Check if valid HTML 66 | if dict(self.headers).get('Content-Type').startswith('text/html'): 67 | self.assertValidHTML() 68 | 69 | done.add(page) 70 | 71 | # Collect all link in the page. 72 | for unused, newpage in re.findall("(href|src|data-ajax)=\"([^\"]+)\"", self.body.decode('utf8', 'replace')): 73 | newpage = newpage.replace("&", "&") 74 | if newpage.startswith("?"): 75 | newpage = re.sub("\\?.*", "", page) + newpage 76 | if newpage not in done and not any(re.match(i, newpage) for i in self.ignore_url): 77 | todo[newpage] = page 78 | -------------------------------------------------------------------------------- /rdiffweb/controller/page_delete.py: -------------------------------------------------------------------------------- 1 | # rdiffweb, A web interface to rdiff-backup repositories 2 | # Copyright (C) 2012-2025 rdiffweb contributors 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | # Define the logger 18 | 19 | import os 20 | 21 | import cherrypy 22 | from wtforms.fields import StringField 23 | from wtforms.validators import DataRequired, ValidationError 24 | 25 | from rdiffweb.controller import Controller 26 | from rdiffweb.controller.filter_authorization import is_maintainer 27 | from rdiffweb.controller.formdb import DbForm 28 | from rdiffweb.core.librdiff import AccessDeniedError, DoesNotExistError 29 | from rdiffweb.core.model import RepoObject 30 | from rdiffweb.core.rdw_templating import url_for 31 | from rdiffweb.tools.i18n import gettext_lazy as _ 32 | 33 | 34 | class DeleteRepoForm(DbForm): 35 | confirm = StringField(_('Confirmation'), validators=[DataRequired()]) 36 | 37 | def validate_confirm(self, field): 38 | if self.confirm.data != self.expected_confirm: 39 | raise ValidationError(_('Invalid value, must be: %s') % self.expected_confirm) 40 | 41 | 42 | @cherrypy.tools.poppath() 43 | class DeletePage(Controller): 44 | @cherrypy.expose 45 | @cherrypy.tools.errors( 46 | error_table={ 47 | DoesNotExistError: 404, 48 | AccessDeniedError: 403, 49 | } 50 | ) 51 | @cherrypy.tools.allow(methods=['POST']) 52 | def default(self, path, **kwargs): 53 | """ 54 | Delete a repo, a file or folder history 55 | """ 56 | # Check permissions on path/repo 57 | repo, path = RepoObject.get_repo_path(path) 58 | # Check if path exists with fstats 59 | path_obj = repo.fstat(path) 60 | # Check user's permissions 61 | is_maintainer() 62 | 63 | # validate form 64 | form = DeleteRepoForm() 65 | form.expected_confirm = repo.display_name if path_obj.isroot else path_obj.display_name 66 | if form.validate_on_submit(): 67 | if path_obj.isroot: 68 | repo.schedule_delete_repo() 69 | # Redirect to main page 70 | raise cherrypy.HTTPRedirect(url_for('/')) 71 | else: 72 | repo.schedule_delete_path(path) 73 | # Redirect to parent folder. 74 | parent_path = repo.fstat(os.path.dirname(path_obj.path)) 75 | raise cherrypy.HTTPRedirect(url_for('browse', repo, parent_path)) 76 | if form.error_message: 77 | raise cherrypy.HTTPError(400, form.error_message) 78 | -------------------------------------------------------------------------------- /rdiffweb/controller/page_admin_logs.py: -------------------------------------------------------------------------------- 1 | # rdiffweb, A web interface to rdiff-backup repositories 2 | # Copyright (C) 2012-2025 rdiffweb contributors 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | import os 18 | import subprocess 19 | 20 | import cherrypy 21 | from cherrypy.lib.static import serve_file 22 | 23 | from rdiffweb.controller import Controller, validate_int 24 | 25 | 26 | @cherrypy.tools.is_admin() 27 | class AdminLogsPage(Controller): 28 | """ 29 | Controller responsible to re-format the logs into usable Json format. 30 | """ 31 | 32 | def _get_log_files(self): 33 | cfg = cherrypy.tree.apps[''].cfg 34 | return_value = {} 35 | if cfg.log_file: 36 | return_value['log_file'] = os.path.abspath(cfg.log_file) 37 | if cfg.log_access_file: 38 | return_value['log_access_file'] = os.path.abspath(cfg.log_access_file) 39 | return return_value 40 | 41 | def _tail(self, filename, limit, encoding='utf-8'): 42 | """ 43 | Return a list of log files to be shown in admin area. 44 | """ 45 | return subprocess.check_output( 46 | ['tail', '-n', str(limit), filename], stderr=subprocess.STDOUT, encoding=encoding, errors='replace' 47 | ) 48 | 49 | @cherrypy.expose 50 | def index(self, name=None, limit='2000'): 51 | """ 52 | Show server logs. 53 | """ 54 | limit = validate_int(limit, min=1, max=20000) 55 | 56 | # Validate filename 57 | log_files = self._get_log_files() 58 | if name is not None and name not in log_files: 59 | raise cherrypy.NotFound() 60 | 61 | # Read file 62 | data = None 63 | if name: 64 | filename = log_files[name] 65 | try: 66 | data = self._tail(filename, limit) 67 | except subprocess.CalledProcessError: 68 | data = '' 69 | 70 | return self._compile_template("admin_logs.html", log_files=log_files, name=name, limit=limit, data=data) 71 | 72 | @cherrypy.expose 73 | @cherrypy.tools.response_headers(headers=[('Content-Type', 'text/plain')]) 74 | def raw(self, name=None): 75 | """ 76 | Download full server logs. 77 | """ 78 | # Validate filename 79 | log_files = self._get_log_files() 80 | if name not in log_files: 81 | raise cherrypy.NotFound() 82 | filename = log_files[name] 83 | 84 | # Release session lock 85 | cherrypy.session.release_lock() 86 | 87 | # Return log file 88 | return serve_file(filename, content_type="text/plain", disposition=True) 89 | -------------------------------------------------------------------------------- /doc/introduction.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | ## What is Rdiffweb? 4 | 5 | Rdiffweb is a web interface that can be used to display and browse Rdiff-backup repositories. It's specially optimized to manage access to your backup in a centralized deployment. If you have a centralized backup server containing multiple repositories for various users, you can use it to control user access to those repositories. 6 | 7 | Once a user has access to a repository, they may browse its content and restore previous revisions directly from the web interface without using a command line. 8 | 9 | ## Architecture 10 | 11 | Rdiffweb is a standalone application that can be used to display and restore data from Rdiff-backup repositories. Backup data will be accessible from the application, either with files being accessible locally on the same physical server or using mount points like NFS. 12 | 13 | This architecture provides deployment flexibility giving you a choice regarding the storage and the backup procedure. 14 | 15 | While it can be run on a centralized backup server, Rdiff-backup cannot be used to manage the backup procedure itself. If you are looking for a completely managed backup solution, please look further into [Minarca](https://minarca.org/). 16 | 17 | ## Main features 18 | 19 | * Web interface: browse and restore Rdiff-backup repositories without command lines. 20 | * User management: provides user access control list for repositories. 21 | * User authentication: username and password validation are done using a database or your LDAP server. 22 | * User permission: allows you to control which users can run deletion operations. 23 | * Email notification: emails can be sent to keep you informed when backup fails 24 | * Statistics visualization: web interface to view backup statistics provided by Rdiff-backup. 25 | * Open source: no secrets. Rdiffweb is a free open-source software. The source code is licensed under GPL v3. 26 | * Support: business support will be available through [Ikus Software](https://ikus-soft.com). 27 | * Rdiff-backup: used as the main backup software you benefit from its stability, cross-platform. And you can still use it the way you are used to with the command line. 28 | 29 | ## Software stack 30 | 31 | Rdiffweb software consists of a single component: the web server, which provides a RESTful API and a user interface. 32 | 33 | Aside from the web interface, which uses HTML and JavaScript everything else is written in Python programming language. 34 | 35 | The Rdiffweb application relies on rdiff-backup and must be installed on the same server. 36 | 37 | ## Getting help 38 | 39 | ### Mailing list 40 | 41 | Rdiffweb is open-source, and contributions are welcome. Here is the main communication channel to get help from other users. 42 | 43 | [Rdiffweb Google Group](https://groups.google.com/forum/#!forum/rdiffweb) 44 | 45 | ### Bug tracker 46 | 47 | If you encounter a problem, you should start by asking to be added to the mailing list. Next, you may open a ticket in our issue tracking system. 48 | 49 | [Gitlab Issues](https://gitlab.com/ikus-soft/rdiffweb/-/issues) 50 | 51 | ### Professional support 52 | 53 | If you need professional support or custom development, you should contact Ikus Software directly. 54 | 55 | [Support Form](https://rdiffweb.org/contactus) 56 | -------------------------------------------------------------------------------- /rdiffweb/templates/status.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | {% import 'include/chartkick.html' as chartkick with context %} 3 | {% from 'include/timerange.html' import timerange %} 4 | {% set active_page='status' %} 5 | {% block title %} 6 | {% trans %}Dashboard{% endtrans %} 7 | {% endblock %} 8 | {% block body %} 9 |
10 | {% include 'message.html' %} 11 | {# Title #} 12 |
13 |

14 | {% trans %}Dashboard{% endtrans %} 15 | {% if username != path %} 16 | {% trans %}for user {{ path }}{% endtrans %} 17 | {% endif %} 18 |

19 | {{ timerange(ranges=[1, 7, 14, 30, 60], limit=days, qs='days') }} 20 |
21 |
22 | {# Backup per day #} 23 |
24 |
25 |
{% trans %}Backup per days{% endtrans %}
26 |
27 | {{ chartkick.column_chart(url_for('status', path, 'per-days.json', days=days), legend='bottom', stacked=True, colors=['#109618', '#ff9900', '#dc3912']) }} 28 |
29 |
30 |
31 | {# Backup by age #} 32 |
33 |
34 |
{% trans %}Oldest backup{% endtrans %}
35 |
36 | {{ chartkick.bar_chart(url_for('status', path, 'age.json', count=count), legend='bottom', colors=['#3b3eac']) }} 37 |
38 |
39 |
40 | {# Backup disk usage #} 41 |
42 |
43 |
{% trans %}Storage usage{% endtrans %}
44 |
45 | {{ chartkick.pie_chart(url_for('status', path, 'disk-usage.json'), donut=True, preffix="~", suffix=" MiB", legend=False) }} 46 |
47 |
48 |
49 | {# Backup duration #} 50 |
51 |
52 |
{% trans %}Highest average duration{% endtrans %}
53 |
54 | {{ chartkick.bar_chart(url_for('status', path, 'elapsetime.json', count=count), legend='bottom', colors=['#0099c6']) }} 55 |
56 |
57 |
58 | {# Least Activities #} 59 |
60 |
61 |
{% trans %}Least active{% endtrans %}
62 |
63 | {{ chartkick.bar_chart(url_for('status', path, 'activities.json', count=count, days=days, sort=1), legend='bottom', stacked=True) }} 64 |
65 |
66 |
67 | {# Most Activities #} 68 |
69 |
70 |
{% trans %}Most active{% endtrans %}
71 |
72 | {{ chartkick.bar_chart(url_for('status', path, 'activities.json', count=count, days=days, sort=-1), legend='bottom', stacked=True) }} 73 |
74 |
75 |
76 |
77 |
78 | {% endblock %} 79 | -------------------------------------------------------------------------------- /rdiffweb/templates/include/table.html: -------------------------------------------------------------------------------- 1 | {% macro table(data, columns=[], order=[], empty_message=None, info_message=None, searching=True, buttons=[], paging=False, page_length=5, layout="default", state_save=True, server_side=False) %} 2 | {% set language = { 3 | "aria": { 4 | "sortAscending": _('activate to sort column ascending'), 5 | "sortDescending": _('activate to sort column descending') 6 | }, 7 | "info": info_message or (_('Showing from _START_ to _END_ of _TOTAL_ total records') if paging else _('Showing total of _TOTAL_ records')), 8 | "infoFiltered": _('(filtered from _MAX_ total records)'), 9 | "infoEmpty": _('No records available'), 10 | "processing": _('Loading...'), 11 | "rdiffweb": { 12 | "null": _('undefined'), 13 | "value": { 14 | "mfa": { 15 | "0": _('Disabled'), 16 | "1": _('Enabled'), 17 | }, 18 | "report_time_range": { 19 | "0": _('Never'), 20 | "1": _('Daily'), 21 | "7": _('Weekly'), 22 | "30": _('Monthly'), 23 | }, 24 | "role": { 25 | "0": _("Admin"), 26 | "5": _("Maintainer"), 27 | "10": _("User"), 28 | }, 29 | "type": { 30 | "new": _('Created by'), 31 | "deleted": _('Deleted by'), 32 | "dirty": _('Modified by'), 33 | "comment": _('Comment by'), 34 | "event": _('Event logged by'), 35 | }, 36 | }, 37 | "field": { 38 | "_encoding_name": _('Display Encoding'), 39 | "_ignore_weekday": _('Excluded Days of the Week'), 40 | "_keepdays": _('Data Retention Duration'), 41 | "authorizedkeys": _('SSH Keys'), 42 | "email": _('Email'), 43 | "fullname": _('Fullname'), 44 | "hash_password": _('Password'), 45 | "lang": _('Preferred Language'), 46 | "maxage": _('Inactivity Notification Period'), 47 | "mfa": _('Two-Factor Authentication'), 48 | "notes": _('Notes'), 49 | "repo_objs": _('Repositories'), 50 | "repopath": _('Display Name'), 51 | "report_time_range": _('Send Backup report'), 52 | "role": _('User Role'), 53 | "tokens": _('Access Token'), 54 | "user": _('Owner'), 55 | "user_root": _('Root directory'), 56 | "username": _('Username'), 57 | } 58 | }, 59 | "search": _('Filter: '), 60 | "zeroRecords": empty_message or _('List is empty'), 61 | } %} 62 | {% set buttons_cfg = { 63 | "dom": { 64 | "button": { 65 | "className": 'btn btn-sm btn-primary ml-1 mb-1', 66 | "active": 'active', 67 | } 68 | }, 69 | "buttons": buttons 70 | } %} 71 | {# Pick the right layout #} 72 | {% set dom_cfg = { 73 | "default": "<'d-sm-flex align-items-center'<'mb-1 flex-grow-1'i><'mb-1'f>><'row'<'col-sm-12'rt>><'row'<'col-sm-12 col-md-7'p>>", 74 | "audit" : "<'row'<'col-sm-12'rt>><'row'<'col-sm-12 col-md-7'p>>"}.get(layout) 75 | %} 76 | 90 |
91 | {% endmacro %} 92 | -------------------------------------------------------------------------------- /rdiffweb/controller/api.py: -------------------------------------------------------------------------------- 1 | # rdiffweb, A web interface to rdiff-backup repositories 2 | # Copyright (C) 2012-2025 rdiffweb contributors 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | import logging 18 | 19 | import cherrypy 20 | 21 | from rdiffweb.controller import Controller 22 | from rdiffweb.controller.api_currentuser import ApiCurrentUser 23 | from rdiffweb.controller.api_openapi import OpenAPI 24 | from rdiffweb.controller.page_admin_users import AdminApiUsers 25 | from rdiffweb.core.model import UserObject 26 | 27 | logger = logging.getLogger(__name__) 28 | 29 | 30 | def _checkpassword(realm, username, password): 31 | """ 32 | Check basic authentication. 33 | """ 34 | # Validate username 35 | userobj = UserObject.get_user(username) 36 | if userobj is not None: 37 | # Verify if the password matches a token. 38 | access_token = userobj.validate_access_token(password) 39 | if access_token: 40 | access_token.accessed() 41 | access_token.commit() 42 | cherrypy.serving.request.scope = access_token.scope 43 | return True 44 | # Disable password authentication for MFA 45 | if userobj.mfa == UserObject.ENABLED_MFA: 46 | cherrypy.tools.ratelimit.increase_hit() 47 | return False 48 | # Otherwise validate username password 49 | valid = cherrypy.tools.auth.login_with_credentials(username, password) 50 | if valid: 51 | # Store scope 52 | cherrypy.serving.request.scope = ['all'] 53 | return True 54 | # When invalid, we need to increase the rate limit. 55 | cherrypy.tools.ratelimit.increase_hit() 56 | return False 57 | 58 | 59 | @cherrypy.expose 60 | @cherrypy.tools.allow(on=False) 61 | @cherrypy.tools.json_out(on=True) 62 | @cherrypy.tools.json_in(on=True, force=False) 63 | @cherrypy.tools.auth_basic(realm='rdiffweb', checkpassword=_checkpassword, priority=70) 64 | @cherrypy.tools.auth(on=True, redirect=False) 65 | @cherrypy.tools.auth_mfa(on=False) 66 | @cherrypy.tools.i18n(on=False) 67 | @cherrypy.tools.ratelimit(scope='rdiffweb-api', hit=0, priority=69) 68 | @cherrypy.tools.sessions(on=False) 69 | class ApiPage(Controller): 70 | """ 71 | This class provide a restful API to access some of the rdiffweb resources. 72 | """ 73 | 74 | currentuser = ApiCurrentUser() 75 | openapi_json = OpenAPI() 76 | users = AdminApiUsers() 77 | 78 | def get(self): 79 | """ 80 | Returns the current application version in JSON format. 81 | 82 | **Example Response** 83 | 84 | ```json 85 | { 86 | "version": "1.2.8" 87 | } 88 | ``` 89 | 90 | """ 91 | return { 92 | "version": self.app.version, 93 | } 94 | -------------------------------------------------------------------------------- /rdiffweb/core/tests/test_remove_older.py: -------------------------------------------------------------------------------- 1 | # rdiffweb, A web interface to rdiff-backup repositories 2 | # Copyright (C) 2012-2025 rdiffweb contributors 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | from unittest.mock import MagicMock, patch 18 | 19 | import cherrypy 20 | 21 | import rdiffweb.core.remove_older 22 | import rdiffweb.test 23 | from rdiffweb.core.librdiff import RdiffTime 24 | from rdiffweb.core.model import RepoObject, UserObject 25 | 26 | 27 | class RemoveOlderTest(rdiffweb.test.WebCase): 28 | def test_check_schedule(self): 29 | # Given the application is started 30 | # Then remove_older job should be schedule 31 | self.assertEqual( 32 | 1, len([job for job in cherrypy.scheduler.get_jobs() if job.name.endswith('remove_older_job')]) 33 | ) 34 | 35 | @patch("rdiffweb.core.model.RepoObject.query") 36 | def test_remove_older_job_without_last_backup_date(self, mock_query): 37 | # Given a store with repos with last_backup_date undefined 38 | repo = MagicMock() 39 | repo.keepdays = 0 40 | repo.last_backup_date = None 41 | mock_query.filter.return_value.all.return_value = [repo] 42 | # When the job is running. 43 | cherrypy.remove_older.remove_older_job() 44 | # Then remove_older function is not called. 45 | mock_query.filter.return_value.all.assert_called() 46 | repo.remove_older.assert_not_called() 47 | 48 | @patch("rdiffweb.core.model.RepoObject.query") 49 | def test_remove_older_job_with_keepdays(self, mock_query): 50 | # Given a store with repos with keepdays equals to 30 51 | repo = MagicMock() 52 | repo.keepdays = 30 53 | repo.last_backup_date = RdiffTime('2014-11-02T17:23:41-05:00') 54 | mock_query.filter.return_value.all.return_value = [repo] 55 | # When the job is running. 56 | cherrypy.remove_older.remove_older_job() 57 | # Then remove_older function get called on the repo. 58 | mock_query.filter.return_value.all.assert_called() 59 | repo.remove_older.assert_called() 60 | 61 | def test_remove_older_without_mock(self): 62 | # Given two repo with keepdays 63 | userobj = UserObject.get_user(self.USERNAME) 64 | repo = RepoObject.get_repo('admin/testcases', userobj) 65 | repo.keepdays = 1 66 | repo.commit() 67 | self.assertEqual(2, RepoObject.query.count()) 68 | self.assertEqual(1, RepoObject.query.filter(RepoObject.keepdays > 0).count()) 69 | # When the job is running. 70 | cherrypy.remove_older.remove_older_job() 71 | # Then history get deleted 72 | repo = RepoObject.get_repo('admin/testcases', userobj) 73 | self.assertEqual(1, len(repo.backup_dates)) 74 | -------------------------------------------------------------------------------- /rdiffweb/controller/page_logs.py: -------------------------------------------------------------------------------- 1 | # rdiffweb, A web interface to rdiff-backup repositories 2 | # Copyright (C) 2012-2025 rdiffweb contributors 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | import logging 18 | 19 | import cherrypy 20 | from cherrypy.lib.static import serve_fileobj 21 | 22 | from rdiffweb.controller import Controller, validate_date, validate_int 23 | from rdiffweb.core.librdiff import AccessDeniedError, DoesNotExistError 24 | from rdiffweb.core.model import RepoObject 25 | from rdiffweb.tools.i18n import ugettext as _ 26 | 27 | # Define the logger 28 | logger = logging.getLogger(__name__) 29 | 30 | 31 | @cherrypy.tools.poppath() 32 | class LogsPage(Controller): 33 | @cherrypy.expose 34 | @cherrypy.tools.errors( 35 | error_table={ 36 | DoesNotExistError: 404, 37 | AccessDeniedError: 403, 38 | } 39 | ) 40 | def default(self, path, limit='10', date=None, file=None, raw=0): 41 | """ 42 | Show repository backup and restore logs 43 | """ 44 | limit = validate_int(limit) 45 | if date is not None: 46 | date = validate_date(date) 47 | raw = validate_int(raw) 48 | 49 | repo_obj = RepoObject.get_repo(path) 50 | if repo_obj.status[0] == 'failed': 51 | params = {'repo': repo_obj, 'limit': limit, 'date': date, 'file': file, 'data': '', 'error_logs': []} 52 | return self._compile_template("logs.html", **params) 53 | 54 | # Read log file data 55 | if date: 56 | try: 57 | entry = repo_obj.error_log[date] 58 | except KeyError: 59 | raise cherrypy.HTTPError(404, _('Invalid date.')) 60 | elif file is None or file == 'backup.log': 61 | entry = repo_obj.backup_log 62 | elif file == 'restore.log': 63 | entry = repo_obj.restore_log 64 | else: 65 | raise cherrypy.HTTPError(404, _('Invalid file')) 66 | 67 | try: 68 | data = None 69 | if raw: 70 | return serve_fileobj(entry._open(), content_type="text/plain") 71 | elif entry: 72 | # Limit to 2000 lines in html page. 73 | data = entry.tail() 74 | except FileNotFoundError: 75 | # If the file doesn't exists, swallow the error. 76 | pass 77 | 78 | # Get error log list 79 | if limit < len(repo_obj.error_log): 80 | error_logs = repo_obj.error_log[: -limit - 1 : -1] 81 | else: 82 | error_logs = repo_obj.error_log[::-1] 83 | 84 | params = {'repo': repo_obj, 'limit': limit, 'date': date, 'file': file, 'data': data, 'error_logs': error_logs} 85 | return self._compile_template("logs.html", **params) 86 | -------------------------------------------------------------------------------- /rdiffweb/controller/page_admin_session.py: -------------------------------------------------------------------------------- 1 | # rdiffweb, A web interface to rdiff-backup repositories 2 | # Copyright (C) 2012-2025 rdiffweb contributors 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | 18 | import cherrypy 19 | from wtforms import validators 20 | from wtforms.fields import IntegerField, StringField 21 | 22 | from rdiffweb.controller import Controller, flash 23 | from rdiffweb.controller.formdb import DbForm 24 | from rdiffweb.core.model import SessionObject 25 | from rdiffweb.tools.i18n import ugettext as _ 26 | from rdiffweb.tools.sessions_timeout import SESSION_PERSISTENT, SESSION_START_TIME 27 | 28 | 29 | class RevokeSessionForm(DbForm): 30 | action = StringField(validators=[validators.regexp('delete')]) 31 | number = IntegerField(validators=[validators.data_required()]) 32 | 33 | 34 | @cherrypy.tools.is_admin() 35 | class AdminSessionPage(Controller): 36 | @cherrypy.expose 37 | @cherrypy.tools.allow(methods=['GET', 'POST']) 38 | @cherrypy.tools.ratelimit(method=['POST']) 39 | def index(self, **kwargs): 40 | """ 41 | Show or remove user sessions 42 | """ 43 | # Delete session on form submit 44 | current_session_id = cherrypy.session.id 45 | form = RevokeSessionForm() 46 | if form.validate_on_submit(): 47 | session = SessionObject.query.filter(SessionObject.number == form.number.data).first() 48 | if not session: 49 | flash(_('The given session cannot be removed because it cannot be found.'), level='warning') 50 | elif session.id == current_session_id: 51 | flash(_('You cannot revoke your current session.'), level='warning') 52 | else: 53 | session.delete() 54 | session.commit() 55 | flash(_('The session was successfully revoked.'), level='success') 56 | if form.error_message: 57 | flash(form.error_message, level='error') 58 | # Get list of current user's session 59 | obj_list = SessionObject.query.filter().all() 60 | active_sessions = [ 61 | { 62 | 'number': obj.number, 63 | 'access_time': obj.data.get('access_time', None), 64 | 'current': current_session_id == obj.id, 65 | 'expiration_time': obj.expiration_time, 66 | 'ip_address': obj.data.get('ip_address', None), 67 | 'start_time': obj.data.get(SESSION_START_TIME, None), 68 | 'user_agent': obj.data.get('user_agent', None), 69 | 'login_persistent': obj.data.get(SESSION_PERSISTENT, None), 70 | 'username': obj.username, 71 | } 72 | for obj in obj_list 73 | ] 74 | return self._compile_template("admin_session.html", active_sessions=active_sessions) 75 | -------------------------------------------------------------------------------- /rdiffweb/controller/page_pref_session.py: -------------------------------------------------------------------------------- 1 | # rdiffweb, A web interface to rdiff-backup repositories 2 | # Copyright (C) 2012-2025 rdiffweb contributors 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | 18 | import cherrypy 19 | from wtforms import validators 20 | from wtforms.fields import IntegerField, StringField 21 | 22 | from rdiffweb.controller import Controller, flash 23 | from rdiffweb.controller.formdb import DbForm 24 | from rdiffweb.core.model import SessionObject 25 | from rdiffweb.tools.i18n import ugettext as _ 26 | from rdiffweb.tools.sessions_timeout import SESSION_PERSISTENT, SESSION_START_TIME 27 | 28 | 29 | class RevokeSessionForm(DbForm): 30 | action = StringField(validators=[validators.regexp('delete')]) 31 | number = IntegerField(validators=[validators.data_required()]) 32 | 33 | 34 | class PagePrefSession(Controller): 35 | @cherrypy.expose 36 | @cherrypy.tools.allow(methods=['GET', 'POST']) 37 | def default(self, **kwargs): 38 | """ 39 | Show user sessions 40 | """ 41 | currentuser = cherrypy.serving.request.currentuser 42 | # Delete session on form submit 43 | form = RevokeSessionForm() 44 | if form.validate_on_submit(): 45 | session = SessionObject.query.filter( 46 | SessionObject.username == currentuser.username, SessionObject.number == form.number.data 47 | ).first() 48 | if not session: 49 | flash(_('The given session cannot be removed because it cannot be found.'), level='warning') 50 | elif session.id == cherrypy.serving.session.id: 51 | flash(_('You cannot revoke your current session.'), level='warning') 52 | else: 53 | session.delete() 54 | session.commit() 55 | flash(_('The session was successfully revoked.'), level='success') 56 | if form.error_message: 57 | flash(form.error_message, level='error') 58 | # Get list of current user's session 59 | obj_list = SessionObject.query.filter(SessionObject.username == currentuser.username).all() 60 | current_session_id = cherrypy.serving.session.id 61 | active_sessions = [ 62 | { 63 | 'number': obj.number, 64 | 'access_time': obj.data.get('access_time', None), 65 | 'current': current_session_id == obj.id, 66 | 'expiration_time': obj.expiration_time, 67 | 'ip_address': obj.data.get('ip_address', None), 68 | 'start_time': obj.data.get(SESSION_START_TIME, None), 69 | 'user_agent': obj.data.get('user_agent', None), 70 | 'login_persistent': obj.data.get(SESSION_PERSISTENT, None), 71 | 'username': obj.username, 72 | } 73 | for obj in obj_list 74 | ] 75 | return self._compile_template("prefs_session.html", active_sessions=active_sessions) 76 | --------------------------------------------------------------------------------