settings.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501
  1. __package__ = 'archivebox.core'
  2. import os
  3. import sys
  4. import re
  5. import logging
  6. import tempfile
  7. from pathlib import Path
  8. from django.utils.crypto import get_random_string
  9. from ..config import (
  10. CONFIG,
  11. DEBUG,
  12. SECRET_KEY,
  13. ALLOWED_HOSTS,
  14. PACKAGE_DIR,
  15. TEMPLATES_DIR_NAME,
  16. CUSTOM_TEMPLATES_DIR,
  17. SQL_INDEX_FILENAME,
  18. OUTPUT_DIR,
  19. ARCHIVE_DIR,
  20. LOGS_DIR,
  21. CACHE_DIR,
  22. TIMEZONE,
  23. LDAP,
  24. LDAP_SERVER_URI,
  25. LDAP_BIND_DN,
  26. LDAP_BIND_PASSWORD,
  27. LDAP_USER_BASE,
  28. LDAP_USER_FILTER,
  29. LDAP_USERNAME_ATTR,
  30. LDAP_FIRSTNAME_ATTR,
  31. LDAP_LASTNAME_ATTR,
  32. LDAP_EMAIL_ATTR,
  33. )
  34. IS_MIGRATING = 'makemigrations' in sys.argv[:3] or 'migrate' in sys.argv[:3]
  35. IS_TESTING = 'test' in sys.argv[:3] or 'PYTEST_CURRENT_TEST' in os.environ
  36. IS_SHELL = 'shell' in sys.argv[:3] or 'shell_plus' in sys.argv[:3]
  37. ################################################################################
  38. ### Django Core Settings
  39. ################################################################################
  40. WSGI_APPLICATION = 'core.wsgi.application'
  41. ROOT_URLCONF = 'core.urls'
  42. LOGIN_URL = '/accounts/login/'
  43. LOGOUT_REDIRECT_URL = os.environ.get('LOGOUT_REDIRECT_URL', '/')
  44. PASSWORD_RESET_URL = '/accounts/password_reset/'
  45. APPEND_SLASH = True
  46. DEBUG = DEBUG or ('--debug' in sys.argv)
  47. # add plugins folders to system path, and load plugins in installed_apps
  48. BUILTIN_PLUGINS_DIR = PACKAGE_DIR / 'plugins'
  49. USER_PLUGINS_DIR = OUTPUT_DIR / 'plugins'
  50. sys.path.insert(0, str(BUILTIN_PLUGINS_DIR))
  51. sys.path.insert(0, str(USER_PLUGINS_DIR))
  52. def find_plugins(plugins_dir):
  53. return {
  54. # plugin_entrypoint.parent.name: import_module(plugin_entrypoint.parent.name).METADATA
  55. plugin_entrypoint.parent.name: plugin_entrypoint.parent
  56. for plugin_entrypoint in plugins_dir.glob('*/apps.py')
  57. }
  58. INSTALLED_PLUGINS = {
  59. **find_plugins(BUILTIN_PLUGINS_DIR),
  60. **find_plugins(USER_PLUGINS_DIR),
  61. }
  62. INSTALLED_APPS = [
  63. 'django.contrib.auth',
  64. 'django.contrib.contenttypes',
  65. 'django.contrib.sessions',
  66. 'django.contrib.messages',
  67. 'django.contrib.staticfiles',
  68. 'django.contrib.admin',
  69. 'django_jsonform',
  70. 'signal_webhooks',
  71. 'abid_utils',
  72. 'plugantic',
  73. 'core',
  74. 'api',
  75. *INSTALLED_PLUGINS.keys(),
  76. 'admin_data_views',
  77. 'django_extensions',
  78. ]
  79. # For usage with https://www.jetadmin.io/integrations/django
  80. # INSTALLED_APPS += ['jet_django']
  81. # JET_PROJECT = 'archivebox'
  82. # JET_TOKEN = 'some-api-token-here'
  83. MIDDLEWARE = [
  84. 'core.middleware.TimezoneMiddleware',
  85. 'django.middleware.security.SecurityMiddleware',
  86. 'django.contrib.sessions.middleware.SessionMiddleware',
  87. 'django.middleware.common.CommonMiddleware',
  88. 'django.middleware.csrf.CsrfViewMiddleware',
  89. 'django.contrib.auth.middleware.AuthenticationMiddleware',
  90. 'core.middleware.ReverseProxyAuthMiddleware',
  91. 'django.contrib.messages.middleware.MessageMiddleware',
  92. 'core.middleware.CacheControlMiddleware',
  93. ]
  94. ################################################################################
  95. ### Authentication Settings
  96. ################################################################################
  97. AUTHENTICATION_BACKENDS = [
  98. 'django.contrib.auth.backends.RemoteUserBackend',
  99. 'django.contrib.auth.backends.ModelBackend',
  100. ]
  101. if LDAP:
  102. try:
  103. import ldap
  104. from django_auth_ldap.config import LDAPSearch
  105. global AUTH_LDAP_SERVER_URI
  106. global AUTH_LDAP_BIND_DN
  107. global AUTH_LDAP_BIND_PASSWORD
  108. global AUTH_LDAP_USER_SEARCH
  109. global AUTH_LDAP_USER_ATTR_MAP
  110. AUTH_LDAP_SERVER_URI = LDAP_SERVER_URI
  111. AUTH_LDAP_BIND_DN = LDAP_BIND_DN
  112. AUTH_LDAP_BIND_PASSWORD = LDAP_BIND_PASSWORD
  113. assert AUTH_LDAP_SERVER_URI and LDAP_USERNAME_ATTR and LDAP_USER_FILTER, 'LDAP_* config options must all be set if LDAP=True'
  114. AUTH_LDAP_USER_SEARCH = LDAPSearch(
  115. LDAP_USER_BASE,
  116. ldap.SCOPE_SUBTREE,
  117. '(&(' + LDAP_USERNAME_ATTR + '=%(user)s)' + LDAP_USER_FILTER + ')',
  118. )
  119. AUTH_LDAP_USER_ATTR_MAP = {
  120. 'username': LDAP_USERNAME_ATTR,
  121. 'first_name': LDAP_FIRSTNAME_ATTR,
  122. 'last_name': LDAP_LASTNAME_ATTR,
  123. 'email': LDAP_EMAIL_ATTR,
  124. }
  125. AUTHENTICATION_BACKENDS = [
  126. 'django.contrib.auth.backends.ModelBackend',
  127. 'django_auth_ldap.backend.LDAPBackend',
  128. ]
  129. except ModuleNotFoundError:
  130. sys.stderr.write('[X] Error: Found LDAP=True config but LDAP packages not installed. You may need to run: pip install archivebox[ldap]\n\n')
  131. # dont hard exit here. in case the user is just running "archivebox version" or "archivebox help", we still want those to work despite broken ldap
  132. # sys.exit(1)
  133. ################################################################################
  134. ### Debug Settings
  135. ################################################################################
  136. # only enable debug toolbar when in DEBUG mode with --nothreading (it doesnt work in multithreaded mode)
  137. DEBUG_TOOLBAR = DEBUG and ('--nothreading' in sys.argv) and ('--reload' not in sys.argv)
  138. if DEBUG_TOOLBAR:
  139. try:
  140. import debug_toolbar # noqa
  141. DEBUG_TOOLBAR = True
  142. except ImportError:
  143. DEBUG_TOOLBAR = False
  144. if DEBUG_TOOLBAR:
  145. INSTALLED_APPS = [*INSTALLED_APPS, 'debug_toolbar']
  146. INTERNAL_IPS = ['0.0.0.0', '127.0.0.1', '*']
  147. DEBUG_TOOLBAR_CONFIG = {
  148. "SHOW_TOOLBAR_CALLBACK": lambda request: True,
  149. "RENDER_PANELS": True,
  150. }
  151. DEBUG_TOOLBAR_PANELS = [
  152. 'debug_toolbar.panels.history.HistoryPanel',
  153. 'debug_toolbar.panels.versions.VersionsPanel',
  154. 'debug_toolbar.panels.timer.TimerPanel',
  155. 'debug_toolbar.panels.settings.SettingsPanel',
  156. 'debug_toolbar.panels.headers.HeadersPanel',
  157. 'debug_toolbar.panels.request.RequestPanel',
  158. 'debug_toolbar.panels.sql.SQLPanel',
  159. 'debug_toolbar.panels.staticfiles.StaticFilesPanel',
  160. # 'debug_toolbar.panels.templates.TemplatesPanel',
  161. 'debug_toolbar.panels.cache.CachePanel',
  162. 'debug_toolbar.panels.signals.SignalsPanel',
  163. 'debug_toolbar.panels.logging.LoggingPanel',
  164. 'debug_toolbar.panels.redirects.RedirectsPanel',
  165. 'debug_toolbar.panels.profiling.ProfilingPanel',
  166. 'djdt_flamegraph.FlamegraphPanel',
  167. ]
  168. MIDDLEWARE = [*MIDDLEWARE, 'debug_toolbar.middleware.DebugToolbarMiddleware']
  169. # https://github.com/bensi94/Django-Requests-Tracker (improved version of django-debug-toolbar)
  170. # Must delete archivebox/templates/admin to use because it relies on some things we override
  171. # visit /__requests_tracker__/ to access
  172. DEBUG_REQUESTS_TRACKER = False
  173. if DEBUG_REQUESTS_TRACKER:
  174. INSTALLED_APPS += ["requests_tracker"]
  175. MIDDLEWARE += ["requests_tracker.middleware.requests_tracker_middleware"]
  176. INTERNAL_IPS = ["127.0.0.1", "10.0.2.2", "0.0.0.0", "*"]
  177. ################################################################################
  178. ### Staticfile and Template Settings
  179. ################################################################################
  180. STATIC_URL = '/static/'
  181. STATICFILES_DIRS = [
  182. *([str(CUSTOM_TEMPLATES_DIR / 'static')] if CUSTOM_TEMPLATES_DIR else []),
  183. str(Path(PACKAGE_DIR) / TEMPLATES_DIR_NAME / 'static'),
  184. ]
  185. TEMPLATE_DIRS = [
  186. *([str(CUSTOM_TEMPLATES_DIR)] if CUSTOM_TEMPLATES_DIR else []),
  187. str(Path(PACKAGE_DIR) / TEMPLATES_DIR_NAME / 'core'),
  188. str(Path(PACKAGE_DIR) / TEMPLATES_DIR_NAME / 'admin'),
  189. str(Path(PACKAGE_DIR) / TEMPLATES_DIR_NAME),
  190. ]
  191. TEMPLATES = [
  192. {
  193. 'BACKEND': 'django.template.backends.django.DjangoTemplates',
  194. 'DIRS': TEMPLATE_DIRS,
  195. 'APP_DIRS': True,
  196. 'OPTIONS': {
  197. 'context_processors': [
  198. 'django.template.context_processors.debug',
  199. 'django.template.context_processors.request',
  200. 'django.contrib.auth.context_processors.auth',
  201. 'django.contrib.messages.context_processors.messages',
  202. ],
  203. },
  204. },
  205. ]
  206. ################################################################################
  207. ### External Service Settings
  208. ################################################################################
  209. CACHE_DB_FILENAME = 'cache.sqlite3'
  210. CACHE_DB_PATH = CACHE_DIR / CACHE_DB_FILENAME
  211. CACHE_DB_TABLE = 'django_cache'
  212. DATABASE_FILE = Path(OUTPUT_DIR) / SQL_INDEX_FILENAME
  213. DATABASE_NAME = os.environ.get("ARCHIVEBOX_DATABASE_NAME", str(DATABASE_FILE))
  214. DATABASES = {
  215. 'default': {
  216. 'ENGINE': 'django.db.backends.sqlite3',
  217. 'NAME': DATABASE_NAME,
  218. 'OPTIONS': {
  219. 'timeout': 60,
  220. 'check_same_thread': False,
  221. },
  222. 'TIME_ZONE': TIMEZONE,
  223. # DB setup is sometimes modified at runtime by setup_django() in config.py
  224. },
  225. # 'cache': {
  226. # 'ENGINE': 'django.db.backends.sqlite3',
  227. # 'NAME': CACHE_DB_PATH,
  228. # 'OPTIONS': {
  229. # 'timeout': 60,
  230. # 'check_same_thread': False,
  231. # },
  232. # 'TIME_ZONE': TIMEZONE,
  233. # },
  234. }
  235. MIGRATION_MODULES = {'signal_webhooks': None}
  236. # as much as I'd love this to be a UUID or ULID field, it's not supported yet as of Django 5.0
  237. DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
  238. CACHES = {
  239. 'default': {'BACKEND': 'django.core.cache.backends.locmem.LocMemCache'},
  240. # 'sqlite': {'BACKEND': 'django.core.cache.backends.db.DatabaseCache', 'LOCATION': 'cache'},
  241. # 'dummy': {'BACKEND': 'django.core.cache.backends.dummy.DummyCache'},
  242. # 'filebased': {"BACKEND": "django.core.cache.backends.filebased.FileBasedCache", "LOCATION": CACHE_DIR / 'cache_filebased'},
  243. }
  244. EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
  245. STORAGES = {
  246. "default": {
  247. "BACKEND": "django.core.files.storage.FileSystemStorage",
  248. },
  249. "staticfiles": {
  250. "BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
  251. },
  252. "archive": {
  253. "BACKEND": "django.core.files.storage.FileSystemStorage",
  254. "OPTIONS": {
  255. "base_url": "/archive/",
  256. "location": ARCHIVE_DIR,
  257. },
  258. },
  259. # "personas": {
  260. # "BACKEND": "django.core.files.storage.FileSystemStorage",
  261. # "OPTIONS": {
  262. # "base_url": "/personas/",
  263. # "location": PERSONAS_DIR,
  264. # },
  265. # },
  266. }
  267. ################################################################################
  268. ### Security Settings
  269. ################################################################################
  270. SECRET_KEY = SECRET_KEY or get_random_string(50, 'abcdefghijklmnopqrstuvwxyz0123456789_')
  271. ALLOWED_HOSTS = ALLOWED_HOSTS.split(',')
  272. SECURE_BROWSER_XSS_FILTER = True
  273. SECURE_CONTENT_TYPE_NOSNIFF = True
  274. SECURE_REFERRER_POLICY = 'strict-origin-when-cross-origin'
  275. CSRF_COOKIE_SECURE = False
  276. SESSION_COOKIE_SECURE = False
  277. SESSION_COOKIE_DOMAIN = None
  278. SESSION_COOKIE_AGE = 1209600 # 2 weeks
  279. SESSION_EXPIRE_AT_BROWSER_CLOSE = False
  280. SESSION_SAVE_EVERY_REQUEST = True
  281. SESSION_ENGINE = "django.contrib.sessions.backends.db"
  282. AUTH_PASSWORD_VALIDATORS = [
  283. {'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator'},
  284. {'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator'},
  285. {'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator'},
  286. {'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'},
  287. ]
  288. ################################################################################
  289. ### Shell Settings
  290. ################################################################################
  291. SHELL_PLUS = 'ipython'
  292. SHELL_PLUS_PRINT_SQL = False
  293. IPYTHON_ARGUMENTS = ['--no-confirm-exit', '--no-banner']
  294. IPYTHON_KERNEL_DISPLAY_NAME = 'ArchiveBox Django Shell'
  295. if IS_SHELL:
  296. os.environ['PYTHONSTARTUP'] = str(Path(PACKAGE_DIR) / 'core' / 'welcome_message.py')
  297. ################################################################################
  298. ### Internationalization & Localization Settings
  299. ################################################################################
  300. LANGUAGE_CODE = 'en-us'
  301. USE_I18N = True
  302. USE_TZ = True
  303. DATETIME_FORMAT = 'Y-m-d g:iA'
  304. SHORT_DATETIME_FORMAT = 'Y-m-d h:iA'
  305. TIME_ZONE = TIMEZONE # django convention is TIME_ZONE, archivebox config uses TIMEZONE, they are equivalent
  306. from django.conf.locale.en import formats as en_formats
  307. en_formats.DATETIME_FORMAT = DATETIME_FORMAT
  308. en_formats.SHORT_DATETIME_FORMAT = SHORT_DATETIME_FORMAT
  309. ################################################################################
  310. ### Logging Settings
  311. ################################################################################
  312. IGNORABLE_404_URLS = [
  313. re.compile(r'apple-touch-icon.*\.png$'),
  314. re.compile(r'favicon\.ico$'),
  315. re.compile(r'robots\.txt$'),
  316. re.compile(r'.*\.(css|js)\.map$'),
  317. ]
  318. class NoisyRequestsFilter(logging.Filter):
  319. def filter(self, record):
  320. logline = record.getMessage()
  321. # ignore harmless 404s for the patterns in IGNORABLE_404_URLS
  322. for ignorable_url_pattern in IGNORABLE_404_URLS:
  323. ignorable_log_pattern = re.compile(f'^"GET /.*/?{ignorable_url_pattern.pattern[:-1]} HTTP/.*" (200|30.|404) .+$', re.I | re.M)
  324. if ignorable_log_pattern.match(logline):
  325. return 0
  326. # ignore staticfile requests that 200 or 30*
  327. ignoreable_200_log_pattern = re.compile(r'"GET /static/.* HTTP/.*" (200|30.) .+', re.I | re.M)
  328. if ignoreable_200_log_pattern.match(logline):
  329. return 0
  330. return 1
  331. if LOGS_DIR.exists():
  332. ERROR_LOG = (LOGS_DIR / 'errors.log')
  333. else:
  334. # historically too many edge cases here around creating log dir w/ correct permissions early on
  335. # if there's an issue on startup, we trash the log and let user figure it out via stdout/stderr
  336. ERROR_LOG = tempfile.NamedTemporaryFile().name
  337. LOGGING = {
  338. 'version': 1,
  339. 'disable_existing_loggers': False,
  340. 'handlers': {
  341. 'console': {
  342. 'class': 'logging.StreamHandler',
  343. },
  344. 'logfile': {
  345. 'level': 'ERROR',
  346. 'class': 'logging.handlers.RotatingFileHandler',
  347. 'filename': ERROR_LOG,
  348. 'maxBytes': 1024 * 1024 * 25, # 25 MB
  349. 'backupCount': 10,
  350. },
  351. },
  352. 'filters': {
  353. 'noisyrequestsfilter': {
  354. '()': NoisyRequestsFilter,
  355. }
  356. },
  357. 'loggers': {
  358. 'django': {
  359. 'handlers': ['console', 'logfile'],
  360. 'level': 'INFO',
  361. 'filters': ['noisyrequestsfilter'],
  362. },
  363. 'django.server': {
  364. 'handlers': ['console', 'logfile'],
  365. 'level': 'INFO',
  366. 'filters': ['noisyrequestsfilter'],
  367. }
  368. },
  369. }
  370. # Add default webhook configuration to the User model
  371. SIGNAL_WEBHOOKS_CUSTOM_MODEL = 'api.models.OutboundWebhook'
  372. SIGNAL_WEBHOOKS = {
  373. "HOOKS": {
  374. # ... is a special sigil value that means "use the default autogenerated hooks"
  375. "django.contrib.auth.models.User": ...,
  376. "core.models.Snapshot": ...,
  377. "core.models.ArchiveResult": ...,
  378. "core.models.Tag": ...,
  379. "api.models.APIToken": ...,
  380. },
  381. }
  382. ADMIN_DATA_VIEWS = {
  383. "NAME": "Environment",
  384. "URLS": [
  385. {
  386. "route": "config/",
  387. "view": "core.views.live_config_list_view",
  388. "name": "Configuration",
  389. "items": {
  390. "route": "<str:key>/",
  391. "view": "core.views.live_config_value_view",
  392. "name": "config_val",
  393. },
  394. },
  395. {
  396. "route": "binaries/",
  397. "view": "plugantic.views.binaries_list_view",
  398. "name": "Binaries",
  399. "items": {
  400. "route": "<str:key>/",
  401. "view": "plugantic.views.binary_detail_view",
  402. "name": "binary",
  403. },
  404. },
  405. {
  406. "route": "plugins/",
  407. "view": "plugantic.views.plugins_list_view",
  408. "name": "Plugins",
  409. "items": {
  410. "route": "<str:key>/",
  411. "view": "plugantic.views.plugin_detail_view",
  412. "name": "plugin",
  413. },
  414. },
  415. ],
  416. }