settings.py 13 KB

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