6 Commits

Author SHA1 Message Date
f712d78f27 Update requirements 2026-04-29 21:35:41 +01:00
295d243c16 Fix Jenkinsfile 2026-04-29 21:29:42 +01:00
c78c25dd68 Fix Jenkinsfile 2026-04-29 21:29:02 +01:00
70318b96a2 Add New Relic monitoring 2026-04-29 21:27:54 +01:00
f893f0a31c Fix security.txt 2026-04-14 13:25:01 +01:00
0c28fe932f Revert comments, add plausible 2026-04-05 23:00:08 +01:00
10 changed files with 269 additions and 108 deletions

3
Jenkinsfile vendored
View File

@@ -6,6 +6,7 @@ pipeline {
DISCORD = credentials('jc_discord') DISCORD = credentials('jc_discord')
DISCORD_ERR_STAGING = credentials('jc_discord_err_staging') DISCORD_ERR_STAGING = credentials('jc_discord_err_staging')
DISCORD_ERR_PROD = credentials('jc_discord_err_prod') DISCORD_ERR_PROD = credentials('jc_discord_err_prod')
NEWRELIC_KEY = credentials('newrelic')
} }
stages{ stages{
@@ -32,7 +33,7 @@ pipeline {
git branch: 'master', git branch: 'master',
credentialsId: 'Git', credentialsId: 'Git',
url: 'git@git.jakecharman.co.uk:jake/jc-ng.git' url: 'git@git.jakecharman.co.uk:jake/jc-ng.git'
sh "sed -i s/dummy/$NEWRELIC_KEY/ src/newrelic.ini"
sh "./build.sh git.jakecharman.co.uk/jake/jakecharman.co.uk $BUILD_NUMBER" sh "./build.sh git.jakecharman.co.uk/jake/jakecharman.co.uk $BUILD_NUMBER"
sh "./build.sh europe-west2-docker.pkg.dev/jakecharman/web/jakecharman.co.uk $BUILD_NUMBER" sh "./build.sh europe-west2-docker.pkg.dev/jakecharman/web/jakecharman.co.uk $BUILD_NUMBER"
} }

View File

@@ -439,6 +439,8 @@ Alias "/static" "/var/www/jc/static/"
</Directory> </Directory>
Alias "/robots.txt" "/var/www/jc/static/robots.txt" Alias "/robots.txt" "/var/www/jc/static/robots.txt"
Alias "/.well-known/security.txt" "/var/www/jc/static/security.txt"
<Files jc.wsgi> <Files jc.wsgi>
Require all granted Require all granted

View File

@@ -11,13 +11,14 @@ import json
from io import BytesIO from io import BytesIO
from requests import post from requests import post
from flask import Flask, render_template, Response, url_for, request, send_from_directory, make_response from flask import Flask, render_template, Response, url_for, request, send_from_directory, make_response
from newrelic import agent
from PIL import Image, UnidentifiedImageError from PIL import Image, UnidentifiedImageError
from .content import ContentArea from .content import ContentArea
from .contact import ContactForm from .contact import ContactForm
from .storage import LocalStorage from .storage import LocalStorage
from .links import Links from .links import Links
from .comments import *
agent.initialize('/var/www/jc/newrelic.ini')
app = Flask(__name__) app = Flask(__name__)
md_path = path.join(path.realpath(path.dirname(__file__)), path.normpath('../projects/')) md_path = path.join(path.realpath(path.dirname(__file__)), path.normpath('../projects/'))
@@ -30,7 +31,6 @@ projects = ContentArea(
app.register_blueprint(projects, url_prefix='/projects') app.register_blueprint(projects, url_prefix='/projects')
app.register_blueprint(ContactForm('contact', __name__), url_prefix='/contact') app.register_blueprint(ContactForm('contact', __name__), url_prefix='/contact')
app.register_blueprint(Links(path.join(md_path, 'links.json'), 'links', __name__), url_prefix='/links') app.register_blueprint(Links(path.join(md_path, 'links.json'), 'links', __name__), url_prefix='/links')
app.register_blueprint(Approval(path.join(projects.md_directory.uri, 'comments.db'), 'comments', __name__), url_prefix='/comments')
class DiscordLogger(logging.Handler): class DiscordLogger(logging.Handler):
''' Simple logging handler to send a message to Discord ''' ''' Simple logging handler to send a message to Discord '''
@@ -142,7 +142,7 @@ def sitemap():
url = ET.SubElement(root, 'url') url = ET.SubElement(root, 'url')
ET.SubElement(url, 'loc').text = base_url + route ET.SubElement(url, 'loc').text = base_url + route
ET.SubElement(url, 'lastmod').text = date ET.SubElement(url, 'lastmod').text = date
for article in projects.get_live_posts(): for article in projects.get_all_posts():
if 'link' in article.metadata: if 'link' in article.metadata:
continue continue
url = ET.SubElement(root, 'url') url = ET.SubElement(root, 'url')

View File

@@ -1,60 +0,0 @@
#!/usr/bin/python3
import sqlite3
from flask import Blueprint, Response, request
from uuid import uuid4
from requests import post
from os import environ
class PostComments():
def __init__(self, post_id: str, db_path: str):
self.__db_path = db_path
self.__post_id = post_id
self._webhook = environ['DISCORD_WEBHOOK']
with sqlite3.connect(db_path) as db:
cursor = db.cursor()
cursor.execute("CREATE TABLE IF NOT EXISTS comments (name TEXT, comment TEXT, date INT, post_id TEXT, approved BOOL, key TEXT)")
self.comments = cursor.execute(
"SELECT name, comment, date FROM comments WHERE approved = 1 AND post_id = ? ORDER BY date DESC",
(post_id,)
).fetchall()
def send_to_discord(self, name: str, comment: str, comment_id: int, key: str):
''' Send the message '''
message_to_send = f'New comment from {name}\n\n{comment}'
if len(message_to_send) > 2000:
chars_to_lose = len(message_to_send) - 1900
message_to_send = message_to_send[-chars_to_lose:]
message_to_send += f'\n\n[Approve](https://jakecharman.co.uk/comments/approve/{comment_id}?key={key})'
post(self._webhook, data={'content': message_to_send}, timeout=30)
def make_comment(self, name: str, comment: str):
with sqlite3.connect(self.__db_path) as db:
key = str(uuid4())
cursor = db.cursor()
cursor.execute(
"INSERT INTO comments (name, comment, date, post_id, approved, key) VALUES (?, ?, datetime('now'), ?, 0, ?)",
(name, comment, self.__post_id, key)
)
db.commit()
self.send_to_discord(name, comment, cursor.lastrowid, key)
return cursor.lastrowid
class Approval(Blueprint):
def __init__(self, db_path: str, *args, **kwargs):
super().__init__(*args, **kwargs)
self.__db_path = db_path
self.add_url_rule('/approve/<comment_id>', view_func=self.approve)
def approve(self, comment_id: str):
with sqlite3.connect(self.__db_path) as db:
cursor = db.cursor()
key = cursor.execute("SELECT key FROM comments WHERE rowid = ?", (comment_id,)).fetchone()[0]
if request.args.get('key') == key:
with sqlite3.connect(self.__db_path) as db:
cursor = db.cursor()
cursor.execute("UPDATE comments SET approved = 1 WHERE rowid = ?", (comment_id,))
db.commit()
return Response(status=200)
return Response(status=403)

View File

@@ -6,9 +6,8 @@ from datetime import datetime
import frontmatter import frontmatter
from markdown import markdown from markdown import markdown
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from flask import render_template, Response, Blueprint, request, redirect from flask import render_template, Response, Blueprint
from .storage import LocalStorage from .storage import LocalStorage
from .comments import PostComments, Approval
class ContentArea(Blueprint): class ContentArea(Blueprint):
def __init__(self, directory: LocalStorage, *args, **kwargs): def __init__(self, directory: LocalStorage, *args, **kwargs):
@@ -25,7 +24,7 @@ class ContentArea(Blueprint):
self.add_url_rule('/', view_func=self.projects) self.add_url_rule('/', view_func=self.projects)
self.add_url_rule('/category/<category_id>/', view_func=self.category) self.add_url_rule('/category/<category_id>/', view_func=self.category)
self.add_url_rule('/<article_id>', view_func=self.article) self.add_url_rule('/<article_id>', view_func=self.article)
self.add_url_rule('/<article_id>/comment', view_func=self.comment, methods=['POST']) #self.add_url_rule('/image/<image_name>', view_func=self.image)
def processor(self) -> dict: def processor(self) -> dict:
''' Jninja processors ''' ''' Jninja processors '''
@@ -141,16 +140,6 @@ class ContentArea(Blueprint):
return Response(status=500) return Response(status=500)
the_article = articles[0] the_article = articles[0]
comments = PostComments(the_article.metadata['id'], path.join(self.md_directory.uri, 'comments.db')).comments
return render_template('article.html', post=markdown(the_article.content), return render_template('article.html', post=markdown(the_article.content),
metadata=the_article.metadata, metadata=the_article.metadata,
comments = comments,
page_title=f'{the_article.metadata["title"]} - ') page_title=f'{the_article.metadata["title"]} - ')
def comment(self, article_id: str):
PostComments(article_id, path.join(self.md_directory.uri, 'comments.db')).make_comment(
request.form['name'],
request.form['comment']
)
return redirect(f'/projects/{article_id}?comment=true#comments')

View File

@@ -22,23 +22,4 @@
{{post|safe}} {{post|safe}}
</section> </section>
</main> </main>
<section id="comments">
<h2>{{comments | length}} Comment{% if comments | length != 1 %}s{% endif %}</h2>
{% for comment in comments %}
<div class="comment">
<strong>{{ comment[0] }} - {{ comment[2] | human_date }}</strong>
<p>{{ comment[1] }}</p>
</div>
{% endfor %}
<h3>Leave a comment</h3>
{% if request.args.get('comment') is none %}
<form action="./{{metadata.id}}/comment" method="post">
<input type="text", name="name" placeholder="Name" required>
<textarea name="comment" placeholder="Comment" required></textarea>
<input type="submit" value="Submit">
</form>
{% else %}
<p>Thank you! Your comment will appear once it is approved</p>
{% endif %}
</section>
{% endblock %} {% endblock %}

255
src/newrelic.ini Normal file
View File

@@ -0,0 +1,255 @@
# ---------------------------------------------------------------------------
#
# This file configures the New Relic Python Agent.
#
# The path to the configuration file should be supplied to the function
# newrelic.agent.initialize() when the agent is being initialized.
#
# The configuration file follows a structure similar to what you would
# find for Microsoft Windows INI files. For further information on the
# configuration file format see the Python ConfigParser documentation at:
#
# http://docs.python.org/library/configparser.html
#
# For further discussion on the behaviour of the Python agent that can
# be configured via this configuration file see:
#
# https://docs.newrelic.com/docs/apm/agents/python-agent/configuration/python-agent-configuration/
#
# ---------------------------------------------------------------------------
# Here are the settings that are common to all environments.
[newrelic]
# You must specify the license key associated with your New
# Relic account. This may also be set using the NEW_RELIC_LICENSE_KEY
# environment variable. This key binds the Python Agent's data to
# your account in the New Relic service. For more information on
# storing and generating license keys, see
# https://docs.newrelic.com/docs/apis/intro-apis/new-relic-api-keys/#ingest-license-key
license_key = dummy
# The application name. Set this to be the name of your
# application as you would like it to show up in New Relic UI.
# You may also set this using the NEW_RELIC_APP_NAME environment variable.
# The UI will then auto-map instances of your application into a
# entry on your home dashboard page. You can also specify multiple
# app names to group your aggregated data. For further details,
# please see:
# https://docs.newrelic.com/docs/apm/agents/manage-apm-agents/app-naming/use-multiple-names-app/
app_name = jakecharman.co.uk
# When "true", the agent collects performance data about your
# application and reports this data to the New Relic UI at
# newrelic.com. This global switch is normally overridden for
# each environment below. It may also be set using the
# NEW_RELIC_MONITOR_MODE environment variable.
monitor_mode = true
# Sets the name of a file to log agent messages to. Whatever you
# set this to, you must ensure that the permissions for the
# containing directory and the file itself are correct, and
# that the user that your web application runs as can write out
# to the file. If not able to out a log file, it is also
# possible to say "stderr" and output to standard error output.
# This would normally result in output appearing in your web
# server log. It can also be set using the NEW_RELIC_LOG
# environment variable.
log_file = stdout
# Sets the level of detail of messages sent to the log file, if
# a log file location has been provided. Possible values, in
# increasing order of detail, are: "critical", "error", "warning",
# "info" and "debug". When reporting any agent issues to New
# Relic technical support, the most useful setting for the
# support engineers is "debug". However, this can generate a lot
# of information very quickly, so it is best not to keep the
# agent at this level for longer than it takes to reproduce the
# problem you are experiencing. This may also be set using the
# NEW_RELIC_LOG_LEVEL environment variable.
log_level = info
# High Security Mode enforces certain security settings, and prevents
# them from being overridden, so that no sensitive data is sent to New
# Relic. Enabling High Security Mode means that request parameters are
# not collected and SQL can not be sent to New Relic in its raw form.
# To activate High Security Mode, it must be set to 'true' in this
# local .ini configuration file AND be set to 'true' in the
# server-side configuration in the New Relic user interface. It can
# also be set using the NEW_RELIC_HIGH_SECURITY environment variable.
# For details, see
# https://docs.newrelic.com/docs/subscriptions/high-security
high_security = false
# The Python Agent will attempt to connect directly to the New
# Relic service. If there is an intermediate firewall between
# your host and the New Relic service that requires you to use a
# HTTP proxy, then you should set both the "proxy_host" and
# "proxy_port" settings to the required values for the HTTP
# proxy. The "proxy_user" and "proxy_pass" settings should
# additionally be set if proxy authentication is implemented by
# the HTTP proxy. The "proxy_scheme" setting dictates what
# protocol scheme is used in talking to the HTTP proxy. This
# would normally always be set as "http" which will result in the
# agent then using a SSL tunnel through the HTTP proxy for end to
# end encryption.
# See https://docs.newrelic.com/docs/apm/agents/python-agent/configuration/python-agent-configuration/#proxy
# for information on proxy configuration via environment variables.
# proxy_scheme = http
# proxy_host = hostname
# proxy_port = 8080
# proxy_user =
# proxy_pass =
# Capturing request parameters is off by default. To enable the
# capturing of request parameters, first ensure that the setting
# "attributes.enabled" is set to "true" (the default value), and
# then add "request.parameters.*" to the "attributes.include"
# setting. For details about attributes configuration, please
# consult the documentation.
# attributes.include = request.parameters.*
# The transaction tracer captures deep information about slow
# transactions and sends this to the UI on a periodic basis. The
# transaction tracer is enabled by default. Set this to "false"
# to turn it off.
transaction_tracer.enabled = true
# Threshold in seconds for when to collect a transaction trace.
# When the response time of a controller action exceeds this
# threshold, a transaction trace will be recorded and sent to
# the UI. Valid values are any positive float value, or (default)
# "apdex_f", which will use the threshold for a dissatisfying
# Apdex controller action - four times the Apdex T value.
transaction_tracer.transaction_threshold = apdex_f
# When the transaction tracer is on, SQL statements can
# optionally be recorded. The recorder has three modes, "off"
# which sends no SQL, "raw" which sends the SQL statement in its
# original form, and "obfuscated", which strips out numeric and
# string literals.
transaction_tracer.record_sql = obfuscated
# Threshold in seconds for when to collect stack trace for a SQL
# call. In other words, when SQL statements exceed this
# threshold, then capture and send to the UI the current stack
# trace. This is helpful for pinpointing where long SQL calls
# originate from in an application.
transaction_tracer.stack_trace_threshold = 0.5
# Determines whether the agent will capture query plans for slow
# SQL queries. Only supported in MySQL and PostgreSQL. Set this
# to "false" to turn it off.
transaction_tracer.explain_enabled = true
# Threshold for query execution time below which query plans
# will not not be captured. Relevant only when "explain_enabled"
# is true.
transaction_tracer.explain_threshold = 0.5
# Space separated list of function or method names in form
# 'module:function' or 'module:class.function' for which
# additional function timing instrumentation will be added.
transaction_tracer.function_trace =
# The error collector captures information about uncaught
# exceptions or logged exceptions and sends them to UI for
# viewing. The error collector is enabled by default. Set this
# to "false" to turn it off. For more details on errors, see
# https://docs.newrelic.com/docs/apm/agents/manage-apm-agents/agent-data/manage-errors-apm-collect-ignore-or-mark-expected/
error_collector.enabled = true
# To stop specific errors from reporting to the UI, set this to
# a space separated list of the Python exception type names to
# ignore. The exception name should be of the form 'module:class'.
error_collector.ignore_classes =
# Expected errors are reported to the UI but will not affect the
# Apdex or error rate. To mark specific errors as expected, set this
# to a space separated list of the Python exception type names to
# expected. The exception name should be of the form 'module:class'.
error_collector.expected_classes =
# Browser monitoring is the Real User Monitoring feature of the UI.
# For those Python web frameworks that are supported, this
# setting enables the auto-insertion of the browser monitoring
# JavaScript fragments.
browser_monitoring.auto_instrument = true
# A thread profiling session can be scheduled via the UI when
# this option is enabled. The thread profiler will periodically
# capture a snapshot of the call stack for each active thread in
# the application to construct a statistically representative
# call tree. For more details on the thread profiler tool, see
# https://docs.newrelic.com/docs/apm/apm-ui-pages/events/thread-profiler-tool/
thread_profiler.enabled = true
# Your application deployments can be recorded through the
# New Relic REST API. To use this feature provide your API key
# below then use the `newrelic-admin record-deploy` command.
# This can also be set using the NEW_RELIC_API_KEY
# environment variable.
# api_key =
# Distributed tracing lets you see the path that a request takes
# through your distributed system. For more information, please
# consult our distributed tracing planning guide.
# https://docs.newrelic.com/docs/transition-guide-distributed-tracing
distributed_tracing.enabled = true
# This setting enables log decoration, the forwarding of log events,
# and the collection of logging metrics if these sub-feature
# configurations are also enabled. If this setting is false, no
# logging instrumentation features are enabled. This can also be
# set using the NEW_RELIC_APPLICATION_LOGGING_ENABLED environment
# variable.
# application_logging.enabled = true
# If true, the agent captures log records emitted by your application
# and forwards them to New Relic. `application_logging.enabled` must
# also be true for this setting to take effect. You can also set
# this using the NEW_RELIC_APPLICATION_LOGGING_FORWARDING_ENABLED
# environment variable.
# application_logging.forwarding.enabled = true
# If true, the agent decorates logs with metadata to link to entities,
# hosts, traces, and spans. `application_logging.enabled` must also
# be true for this setting to take effect. This can also be set
# using the NEW_RELIC_APPLICATION_LOGGING_LOCAL_DECORATING_ENABLED
# environment variable.
# application_logging.local_decorating.enabled = true
# If true, the agent captures metrics related to the log lines
# being sent up by your application. This can also be set
# using the NEW_RELIC_APPLICATION_LOGGING_METRICS_ENABLED
# environment variable.
# application_logging.metrics.enabled = true
# ---------------------------------------------------------------------------
#
# The application environments. These are specific settings which
# override the common environment settings. The settings related to a
# specific environment will be used when the environment argument to the
# newrelic.agent.initialize() function has been defined to be either
# "development", "test", "staging" or "production".
#
[newrelic:development]
monitor_mode = false
[newrelic:test]
monitor_mode = false
[newrelic:staging]
app_name = jakecharman.co.uk (Staging)
monitor_mode = true
[newrelic:production]
monitor_mode = true
# ---------------------------------------------------------------------------

View File

@@ -7,3 +7,4 @@ python-frontmatter>=1.1.0
requests>=2.32.3 requests>=2.32.3
pillow>=11.0.0 pillow>=11.0.0
mod_wsgi>=5.0.2 mod_wsgi>=5.0.2
newrelic

3
src/static/security.txt Normal file
View File

@@ -0,0 +1,3 @@
Contact: mailto:security@jakecharman.co.uk
Expires: 2036-01-01T00:00:00.000Z
Preferred-Languages: en

View File

@@ -329,14 +329,3 @@ pre{
height: 31vw; height: 31vw;
object-fit: cover; object-fit: cover;
} }
#comments {
border-top: 1px solid #e5e5e5;
}
.comment {
background-color: #4c4c4c;
margin: 10px;
padding: 5px;
border-radius: 10px;
}