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 |
{% 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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
{% 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 |
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 |
34 | {# Message Thread #}
35 | {{ messages(data_url=url_for('audit', repo)) }}
36 |
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 |
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 |
13 | {% set active_name = name %}
14 | {% for name, filename in log_files.items() %}
15 | {{ filename.split('/') | last }}
17 | {% endfor %}
18 |
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 %}
{% 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 %}
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 %}
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 |
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 |
{% 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 %}
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 |
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 |
{% 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 |
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 |
--------------------------------------------------------------------------------