Compare commits

...

47 Commits

Author SHA1 Message Date
950c9a6aea gitea ui tweaks 2025-01-02 02:22:57 +00:00
414c300b41 recenter gitea link 2024-09-05 20:24:47 -07:00
a5ac19a2a4 Merge pull request 'remove hard tabs oops' (#5) from tab_to_softtab into master
Reviewed-on: #5
2024-09-02 22:48:05 +00:00
78c0895418 disable gitea reg if link on tld 2024-09-02 07:47:47 +00:00
8cbe7eecd3 remove tabs from html 2024-09-02 07:22:23 +00:00
9090da987f gitea link in header 2024-09-02 00:04:58 -07:00
650e77210e move onion address to nginx header 2024-08-20 19:40:33 +00:00
16a45c495d restart always incomp with env switch 2024-08-10 06:30:54 -07:00
08ae04a154 ssh tunnel env switch and logging 2024-08-10 06:12:26 -07:00
78384a31fb Merge pull request 'initial working ssh entry' (#4) from ssh_in into master
Reviewed-on: #4
2024-08-10 02:18:05 +00:00
60280917c6 set default to prod 2024-08-10 02:08:35 +00:00
8f8c0c1401 fix composes 2024-08-09 18:58:40 -07:00
979adc3b13 initial working ssh entry 2024-08-09 18:47:22 -07:00
619ce9b0bd long username titles 2024-08-08 18:44:49 +00:00
bcec5d05c8 fix titles 2024-08-08 18:28:57 +00:00
71f8ff967c minor title fix profile 2024-08-08 11:25:28 -07:00
d61fd3bb2c fix post wrapping 2024-08-08 11:12:36 -07:00
234230c14a restorve dev values for env example checked safe 2024-08-08 10:14:31 -07:00
a22c1a4e1e minor color 2024-08-08 16:51:06 +00:00
51693a7860 add registration switch env composes 2024-08-08 16:14:28 +00:00
8f95303d11 add registration switch env 2024-08-08 16:10:43 +00:00
92b314623a appearance tweaks 2024-08-08 15:14:44 +00:00
1c6293fda3 alpine for tor 2024-08-08 04:42:12 -07:00
c295d52520 Merge pull request 'add tor entrypoint' (#3) from hstor into master
Reviewed-on: #3
2024-08-08 11:14:18 +00:00
4e948af492 add tor env switch 2024-08-08 02:57:33 -07:00
b9a432b356 working tor 2024-08-08 02:17:49 -07:00
1ced0d8b24 fix db backup script 2024-08-07 13:51:46 +00:00
d1d64b181a db root localhost only 2024-08-06 16:27:32 +00:00
cd61c513c7 add prod/local compose 2024-08-06 09:03:17 -07:00
bd5b04eeae db user restrict access to container 2024-08-06 09:01:04 -07:00
2ba3fe0a7e navbar current decorator 2024-08-05 09:09:51 -07:00
82baefb3d2 ro certs 2024-08-05 14:55:38 +00:00
7c286e1235 uwsgi and thread don't play nice 2024-08-05 14:22:32 +00:00
b65daf3784 prod cleanup for live c10 2024-08-05 09:55:47 +00:00
834c236091 Merge pull request 'flask site buildout' (#2) from mgtut1 into master
Reviewed-on: #2
2024-08-05 08:41:02 +00:00
5168e6cd73 post c10 cleanup 2024-08-05 01:36:10 -07:00
b3b188f370 mgt c10 complete merge to put bootstrap css changes on their own fork 2024-08-05 01:28:09 -07:00
3d1f21ffcb mgt c10 checkpoint with debug 2024-08-05 00:59:01 -07:00
ed9df4db6f mgt c10 checkpoint 2024-08-04 22:00:07 -07:00
61c8cadb87 mgt c9 complete 2024-08-04 13:14:58 -07:00
94434d4f8e avatar tweaks 2024-08-04 10:55:28 -07:00
2f920f3b6f mgt c8 finish 2024-08-04 09:40:09 -07:00
63bdf8d164 mgt c8 checkpoint after tests 2024-08-04 05:48:02 -07:00
469785ee33 mgt c8 checkpoint at tests 2024-08-04 05:44:20 -07:00
eb0f19b109 pre-c8 checkpoint 2024-08-04 04:39:49 -07:00
d7a0167cd6 mgt c7 finish 2024-08-03 11:45:37 -07:00
5b0cd1d22f checkpoint before maillog attempt 2024-08-03 08:58:32 -07:00
48 changed files with 1298 additions and 276 deletions

8
.gitignore vendored
View File

@ -1,6 +1,8 @@
gitea/
gitea
.env
pmb-pf/
pmb-pf
venv
zapp.db
db/bu
tor/hidden_service
sshtun/oilykey

View File

@ -2,12 +2,10 @@
### Sec:
* This repo is public. Mind cred slip-ups.
* Please note changes to /etc/sshd/sshd_conf made by lll script. If different method is used, audit manually.
* Note app Dockerfile debug console, found at /console. Werkzeug/flask is WILDLY insecure if left in dev/dbg.
* Avoid docker socks stuff.
- This repo is public. Mind cred slip-ups.
- Please note changes to /etc/sshd/sshd_conf made by lll script. If different method is used, audit manually.
- Note app Dockerfile debug console, found at /console. Werkzeug/flask is WILDLY insecure if left in dev/dbg.
- Avoid docker socks stuff.
### Install:
@ -49,19 +47,34 @@ set up cron job for script
pmb-pf - git clone of my mail thing
other - ref and non-sensitive files for dns
### Timeline:
### Setup cheat:
set up certbot dns\
see tar of cert dir with script
- set up certbot dns (prod)
- see tar of cert dir with script (prod)
- flask vs uwsgi in backend compose section (prod)
- build vs local image in pmb-pf compose section
- git clone pmb-pf
- copy example .env in root dir
- copy example .env in pmb-pf
- copy example conf in proxy
- do pmb-pf setup, and adjust root .env
- mind backend config db settings
### Notes:
This repo is minimally-sensitive. Falling outside the repo dir structure are reference awesome-compose files used as baseline -- nginx-flask-mysql -- and certs, containing letsencrypt script. Script may be backed up into repo carefully, sanitizing any tkens.
TODO: gitea subdomain will require wildcard cert -- therefore "*.oily.dad" AND "oily.dad" DONE
### Changing gitea subdomain:
Find in proxy/conf.\
Find in gitea conf.\
Rebuild images.
### MariaDB backup:
```
mariadb-dump -uroot -pxxxx gitea > /bu/19840101.sql
mariadb -uroot -pxxxx gitea < /bu/19840101.sql
```
### Todo:
- gitea subdomain will require wildcard cert -- therefore "*.oily.dad" AND "oily.dad" DONE
- move more stuff from backend config into root .env

View File

@ -1,3 +1,4 @@
venv
migrations
zapp.db

View File

@ -1,3 +1,3 @@
FLASK_APP=microblog.py
FLASK_DEBUG=0
FLASK_DEBUG=1

12
backend/Dockerfile Executable file → Normal file
View File

@ -1,17 +1,21 @@
# syntax=docker/dockerfile:1.4
FROM python:3-slim-bookworm AS builder
RUN apt update && apt install -y libmariadb-dev gcc
# Second line optional/debug/qol
RUN apt update && apt install -y \
libmariadb-dev gcc \
mariadb-client
WORKDIR /code
COPY requirements.txt /code
RUN target=/root/.cache/pip \
pip3 install -r requirements.txt
pip3 install --root-user-action=ignore -q -r requirements.txt
# Need to make this explicit as part of expansion, no migrations or venv
# Dockerignore has this skip migrations, venv, sqlite db
COPY . .
#ENV FLASK_APP app.py
ENV FLASK_APP microblog.py
# This might be scary to leave on
#ENV FLASK_ENV development

View File

@ -18,8 +18,16 @@ pip install flask-migrate
pip install flask-login
pip install email-validator
pip install pydenticon
pip install flask-mail
pip install pyjwt
Prod only, require sys packages:
pip install mariadb
pip install uwsgi
...
```
Freeze/requirements.txt. Better to audit this inside python:3-bookworm-slim container.
```
pip freeze > requirements.txt
```
@ -36,8 +44,11 @@ flask db downgrade base
flask db upgrade
```
Full reset:
Full reset or maria init:
```
sql:
drop table users;
drop table posts;
rm app.db
rm -r migrations
flask db init
@ -54,6 +65,9 @@ Dockerfile needs dockerignore or preferably explicitly defined copies for:
- requirements
- not dotflaskenv, vars set with dockerfile
## docker setup:
- py logger handler StreamHandler is buggy to the point of being useless. Use logfile and link to stdout in Docker build
## notes:
- compose has entry that overrides flask with uwsgi for prod
- miminal environment vars come through project env, pass through compose

View File

@ -3,6 +3,9 @@ from config import Config
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from flask_login import LoginManager
import logging, sys
from logging.handlers import SMTPHandler
from flask_mail import Mail
app = Flask(__name__)
app.config.from_object(Config)
@ -10,6 +13,28 @@ db = SQLAlchemy(app)
migrate = Migrate(app, db)
login = LoginManager(app)
login.login_view = 'login'
mail=Mail(app)
if not app.debug:
if app.config['MAIL_SERVER']:
auth = None
secure = None
mail_handler = SMTPHandler(
mailhost=(app.config['MAIL_SERVER'], app.config['MAIL_PORT']),
fromaddr=app.config['FROM_ADDRESS'],
toaddrs=app.config['ADMINS'], subject='MB failure.',
credentials=auth, secure=secure)
mail_handler.setLevel(logging.ERROR)
app.logger.addHandler(mail_handler)
if app.config['DC_LOGGING']:
print('#################### TEST PRINT STDERR DEBUG', file=sys.stderr)
dclog = logging.StreamHandler(stream=sys.stderr)
dclog.setLevel(logging.INFO)
dclog.propagate = False
app.logger.addHandler(dclog)
app.logger.info('@@@@@@@@@@@@@@@@@@@@@ TEST LOGGER INFO MESSAGE')
from app import routes, models, errors

29
backend/app/email.py Normal file
View File

@ -0,0 +1,29 @@
from threading import Thread
from flask import render_template
from flask_mail import Message
from app import mail, app
def send_async_email(app, msg):
with app.app_context():
mail.send(msg)
def send_email(subject, sender, recipients, text_body, html_body):
msg = Message(subject, sender=sender, recipients=recipients)
msg.body = text_body
msg.html = html_body
mail.send(msg)
# Thread works surprisingly badly behind uwsgi, just let a uwsgi worker do its thing instead.
#Thread(target=send_async_email, args=(app, msg)).start()
def send_password_reset_email(user):
token = user.get_reset_password_token()
hostname = app.config['REAL_HOSTNAME']
send_email('reset oily password',
sender=app.config['ADMINS'][0],
recipients=[user.email],
text_body=render_template('email/reset_password.txt', hostname=hostname, user=user, token=token),
html_body=render_template('email/reset_password.html', hostname=hostname, user=user, token=token))

View File

@ -17,18 +17,44 @@ class RegistrationForm(FlaskForm):
password = PasswordField('Password', validators=[DataRequired()])
password2 = PasswordField('Repeat Password', validators=[DataRequired()])
submit = SubmitField('Register')
def validate_username(self, username):
user = db.session.scalar(sa.select(User).where(User.username == username.data))
if user is not None:
raise ValidationError('Please use a different username.')
def validate_email(self, email):
user = db.session.scalar(sa.select(User).where(User.email == email.data))
if user is not None:
raise ValidationError('Please use a different email address.')
class ResetPasswordForm(FlaskForm):
password = PasswordField('Password', validators=[DataRequired()])
password2 = PasswordField('Repeat Password', validators=[DataRequired()])
submit = SubmitField('Request Reset')
class EditProfileForm(FlaskForm):
username = StringField('Username', validators=[DataRequired()])
about_me = TextAreaField('About me', validators=[Length(min=0, max=140)])
submit = SubmitField('Update')
def __init__(self, original_username, *args, **kwargs):
super().__init__(*args, **kwargs)
self.original_username = original_username
def validate_username(self, username):
if username.data != self.original_username:
user = db.session.scalar(sa.select(User).where(User.username == username.data))
if user is not None:
raise ValidationError('Please use a different username.')
class ResetPasswordRequestForm(FlaskForm):
email = StringField('Email', validators=[DataRequired(), Email()])
submit = SubmitField('Request Password Reset')
class PostForm(FlaskForm):
post = TextAreaField('Post', validators=[DataRequired(), Length(min=1, max=140)])
submit = SubmitField('Submit')
class EmptyForm(FlaskForm):
submit = SubmitField('Submit')

View File

@ -1,14 +1,24 @@
import os
from datetime import datetime, timezone
from typing import Optional
import sqlalchemy as sa
import sqlalchemy.orm as so
from werkzeug.security import generate_password_hash, check_password_hash
import pydenticon, hashlib, base64
import os, pydenticon, hashlib, base64, jwt
from time import time
from app import db, login
from app import db, login, app
from flask_login import UserMixin
#debug
#import sys
followers = sa.Table(
'followers',
db.metadata,
sa.Column('follower_id', sa.Integer, sa.ForeignKey('user.id'), primary_key=True),
sa.Column('followed_id', sa.Integer, sa.ForeignKey('user.id'), primary_key=True)
)
class User(UserMixin, db.Model):
id: so.Mapped[int] = so.mapped_column(primary_key=True)
username: so.Mapped[str] = so.mapped_column(sa.String(64), index=True, unique=True)
@ -17,26 +27,52 @@ class User(UserMixin, db.Model):
posts: so.WriteOnlyMapped['Post'] = so.relationship(back_populates='author')
about_me: so.Mapped[Optional[str]] = so.mapped_column(sa.String(140))
last_seen: so.Mapped[Optional[datetime]] = so.mapped_column(default=lambda: datetime.now(timezone.utc))
following: so.WriteOnlyMapped['User'] = so.relationship(
secondary=followers,
primaryjoin=(followers.c.follower_id == id),
secondaryjoin=(followers.c.followed_id == id),
back_populates='followers')
followers: so.WriteOnlyMapped['User'] = so.relationship(
secondary=followers,
primaryjoin=(followers.c.followed_id == id),
secondaryjoin=(followers.c.follower_id == id),
back_populates='following')
def set_password(self, password):
self.password_hash = generate_password_hash(password)
def check_password(self, password):
return check_password_hash(self.password_hash, password)
def get_reset_password_token(self, expires_in=600):
token = jwt.encode({'reset_password': self.id, 'exp': time() + expires_in},
app.config['SECRET_KEY'], algorithm='HS256')
return token
@staticmethod
def verify_reset_password_token(token):
try:
id = jwt.decode(token,
app.config['SECRET_KEY'], algorithms='HS256')['reset_password']
except:
return
return db.session.get(User, id)
def gen_avatar(self, write_png=True):
foreground = [ "rgb(45,79,255)",
"rgb(254,180,44)",
"rgb(226,121,234)",
"rgb(30,179,253)",
"rgb(232,77,65)",
"rgb(49,203,115)",
"rgb(141,69,170)" ]
background = "rgb(22,22,22)"
foreground = ['#ACE1AF',
'#ACC4E1',
'#E1ACDE',
'#E1CAAC',
'#AFFF00',
'#00FFCF',
'#5000FF',
'#FF0030']
background = '#030303'
digest = hashlib.md5(self.email.lower().encode('utf-8')).hexdigest()
basedir = os.path.abspath(os.path.dirname(__file__))
pngloc = os.path.join(basedir, 'usercontent', 'identicon', str(digest) + '.png')
icongen = pydenticon.Generator(5, 5, digest=hashlib.md5, foreground=foreground, background=background)
pngicon = icongen.generate(self.email, 120, 120, padding=(10, 10, 10, 10), inverted=False, output_format="png")
icongen = pydenticon.Generator(8, 5, digest=hashlib.md5, foreground=foreground, background=background)
pngicon = icongen.generate(self.email, 150, 240, padding=(10, 10, 10, 10), inverted=False, output_format="png")
if write_png:
pngfile = open(pngloc, "wb")
pngfile.write(pngicon)
@ -49,6 +85,36 @@ class User(UserMixin, db.Model):
pngloc = os.path.join(basedir, 'usercontent', 'identicon', digest + '.png')
return pngloc
def follow(self, user):
if not self.is_following(user):
self.following.add(user)
def unfollow(self, user):
if self.is_following(user):
self.following.remove(user)
def is_following(self, user):
query = self.following.select().where(User.id == user.id)
return db.session.scalar(query) is not None
def followers_count(self):
query = sa.select(sa.func.count()).select_from(self.followers.select().subquery())
return db.session.scalar(query)
def following_count(self):
query = sa.select(sa.func.count()).select_from(self.following.select().subquery())
return db.session.scalar(query)
def following_posts(self):
Author = so.aliased(User)
Follower = so.aliased(User)
return (
sa.select(Post)
.join(Post.author.of_type(Author))
.join(Author.followers.of_type(Follower), isouter=True)
.where(sa.or_(
Follower.id == self.id,
Author.id == self.id
))
.group_by(Post)
.order_by(Post.timestamp.desc())
)
def __repr__(self):
return '<User {}>'.format(self.username)

View File

@ -5,8 +5,12 @@ from datetime import datetime, timezone
import sqlalchemy as sa
from app import app, db
from app.forms import LoginForm, RegistrationForm, EditProfileForm
from app.models import User
from app.forms import LoginForm, RegistrationForm, EditProfileForm, EmptyForm, PostForm, ResetPasswordRequestForm, ResetPasswordForm
from app.models import User, Post
from app.email import send_password_reset_email
#debug:
#import sys
@app.before_request
def before_request():
@ -14,24 +18,38 @@ def before_request():
current_user.last_seen = datetime.now(timezone.utc)
db.session.commit()
@app.route('/')
@app.route('/index')
@app.route('/', methods=['GET', 'POST'])
@app.route('/index', methods=['GET', 'POST'])
@login_required
def index():
app.logger.info('@@@@@@@@@@@@@@@@@ INFO LOG TEST INDEX')
form = PostForm()
#user = {'username': 'aaa', 'email': 'a@a.a'}
if form.validate_on_submit():
post = Post(body=form.post.data, author=current_user)
db.session.add(post)
db.session.commit()
flash('Your post has been added.')
return redirect(url_for('index'))
#posts = db.session.scalars(current_user.following_posts()).all()
page = request.args.get('page', 1, type=int)
posts = db.paginate(current_user.following_posts(), page=page, per_page=app.config['POSTS_PER_PAGE'], error_out=False)
next_url = url_for('index', page=posts.next_num) if posts.has_next else None
prev_url = url_for('index', page=posts.prev_num) if posts.has_prev else None
user = {'username': 'Finnaa'}
posts = [
{
'author': {'username': 'john'},
'body': 'Beautiful day 1'
},
{
'author': {'username': 'susie'},
'body': 'Movie is good.'
}
]
#return posts;
return render_template('index.html', title='Home', posts=posts)
return render_template('index.html', title='oily home', form=form, posts=posts.items, next_url=next_url, prev_url=prev_url)
@app.route('/explore')
@login_required
def explore():
query = sa.select(Post).order_by(Post.timestamp.desc())
page = request.args.get('page', 1, type=int)
ppp = app.config['POSTS_PER_PAGE'] * 2
posts = db.paginate(query, page=page, per_page=ppp, error_out=False)
next_url = url_for('explore', page=posts.next_num) if posts.has_next else None
prev_url = url_for('explore', page=posts.prev_num) if posts.has_prev else None
return render_template('explore.html', title='oily explore', posts=posts.items, next_url=next_url, prev_url=prev_url)
@app.route('/login', methods=['GET', 'POST'])
def login():
@ -48,7 +66,7 @@ def login():
if not next_page or urlsplit(next_page).netloc != '':
next_page = url_for('index')
return redirect(next_page)
return render_template('login.html', title='Sign In', form=form)
return render_template('login.html', title='oily sign in', form=form)
@app.route('/logout')
def logout():
@ -59,6 +77,9 @@ def logout():
def register():
if current_user.is_authenticated:
return redirect(url_for('index'))
if not app.config['ALLOW_REGISTRATION'] == "true":
flash('Registration temporarily disabled.')
return redirect(url_for('login'))
form = RegistrationForm()
if form.validate_on_submit():
user = User(username=form.username.data, email=form.email.data)
@ -68,22 +89,43 @@ def register():
#user.gen_avatar()
flash('User has been created.')
return redirect(url_for('login'))
return render_template('register.html', title='Register', form=form)
return render_template('register.html', title='oily register', form=form)
@app.route('/reset_password/<token>', methods=['GET', 'POST'])
def reset_password(token):
if current_user.is_authenticated:
return redirect(url_for('index'))
user = User.verify_reset_password_token(token)
if not user:
return redirect(url_for('index'))
form = ResetPasswordForm()
if form.validate_on_submit():
user.set_password(form.password.data)
db.session.commit()
flash('Your password has been reset.')
return redirect(url_for('login'))
return render_template('reset_password.html', form=form)
@app.route('/user/<username>')
@login_required
def user(username):
user = db.first_or_404(sa.select(User).where(User.username == username))
posts = [
{'author': user, 'body': 'Test1'},
{'author': user, 'body': 'Test2?'}
]
return render_template('user.html', user=user, posts=posts)
shortname = (current_user.username[:10] + '...') if len(current_user.username) > 14 else current_user.username
page = request.args.get('page', 1, type=int)
query = user.posts.select().order_by(Post.timestamp.desc())
posts = db.paginate(query, page=page, per_page=app.config['POSTS_PER_PAGE'], error_out=False)
next_url = url_for('user', username=user.username, page=posts.next_num) if posts.has_next else None
prev_url = url_for('user', username=user.username, page=posts.prev_num) if posts.has_prev else None
form = EmptyForm()
return render_template('user.html', title='oily profile - '+shortname, user=user, posts=posts.items, next_url=next_url, prev_url=prev_url, form=form)
@app.route('/edit_profile', methods=['GET', 'POST'])
@login_required
def edit_profile():
form = EditProfileForm()
form = EditProfileForm(current_user.username)
shortname = (current_user.username[:10] + '...') if len(current_user.username) > 14 else current_user.username
if form.validate_on_submit():
current_user.username = form.username.data
current_user.about_me = form.about_me.data
@ -93,5 +135,57 @@ def edit_profile():
elif request.method == 'GET':
form.username.data = current_user.username
form.about_me.data = current_user.about_me
return render_template('edit_profile.html', title='Edit Profile', form=form)
return render_template('edit_profile.html', title='oily edit profile - '+shortname, form=form)
@app.route('/follow/<username>', methods=['POST'])
@login_required
def follow(username):
form = EmptyForm()
if form.validate_on_submit():
user = db.session.scalar(sa.select(User).where(User.username == username))
if user is None:
flash(f'User {username} not found.')
return redirect(url_for('index'))
if user == current_user:
flash('You cannot follow yourself.')
return redirect(url_for('user', username=username))
current_user.follow(user)
db.session.commit()
flash(f'You are now following {username}.')
return redirect(url_for('user', username=username))
else:
return redirect(url_for('index'))
@app.route('/unfollow/<username>', methods=['POST'])
@login_required
def unfollow(username):
form = EmptyForm()
if form.validate_on_submit():
user = db.session.scalar(sa.select(User).where(User.username == username))
if user is None:
flash(f'User {username} not found.')
return redirect(url_for('index'))
if user == current_user:
flash('You cannot unfollow yourself.')
return redirect(url_for('user', username=username))
current_user.unfollow(user)
db.session.commit()
flash(f'You have unfollowed {username}.')
return redirect(url_for('user', username=username))
else:
return redirect(url_for('index'))
@app.route('/reset_password_request', methods=['GET', 'POST'])
def reset_password_request():
if current_user.is_authenticated:
return redirect(url_for('index'))
form = ResetPasswordRequestForm()
if form.validate_on_submit():
user = db.session.scalar(sa.select(User).where(User.email == form.email.data))
if user:
send_password_reset_email(user)
flash('Password reset sent.')
return redirect(url_for('login'))
return render_template('reset_password_request.html', title='oily password reset', form=form)

View File

@ -2,7 +2,7 @@
{% block content %}
<h1>File Not Found</h1>
<p><a href="{{ url_for('index') }}">Back</a></p>
<h2>File Not Found</h1>
<p><a href="{{ url_for('index') }}">Back</a></p>
{% endblock %}

View File

@ -2,8 +2,8 @@
{% block content %}
<h1>An unexpected error has occurred.</h1>
<p>Administrator has been notified.</p>
<p><a href="{{ url_for('index') }}">Back</a></p>
<h2>An unexpected error has occurred.</h1>
<p>Administrator has been notified.</p>
<p><a href="{{ url_for('index') }}">Back</a></p>
{% endblock %}

View File

@ -1,6 +1,11 @@
<table>
<tr valign="top">
<td><img width="60" height=="60" src="data:image/png;base64,{{ user.gen_avatar(write_png=False) }}"></td>
<td>{{ post.author.username }} says:<br>{{ post.body }}</td>
</tr>
</table>
<tr style="vertical-align: top;">
<td style="vertical-align: middle; width: 60px;">
<img style="vertical-align: middle; width: 40px;" src="data:image/png;base64,{{ post.author.gen_avatar(write_png=False) }}">
</td>
<td style="word-break: break-word;">
<a href="{{ url_for('user', username=post.author.username) }}">
{{ post.author.username }}
</a> says:<br>
{{ post.body }}
</td>
</tr>

View File

@ -0,0 +1,14 @@
<div>
{% if prev_url %}
<a class="button" href="{{ prev_url }}">Newer</a>
{% else %}
<a class="button" aria-disabled=true>Newer</a>
{% endif %}
{% if next_url %}
<a class="button" href="{{ next_url }}">Older</a>
{% else %}
<a class="button" aria-disabled=true>Older</a>
{% endif %}
</div>

View File

@ -1,38 +1,44 @@
<!doctype html>
<html>
<head>
<link rel="stylesheet" href="{{ url_for('static', filename='simple.css') }}">
{% if title %}
<title>{{ title }} - blog</title>
{% else %}
<title>Welcome to blog.</title>
{% endif %}
</head>
<body>
<div>
blgo:
<a href="{{ url_for('index') }}">home</a>
{% if current_user.is_anonymous %}
<a href="{{ url_for('login') }}">login</a>
{% else %}
<a href="{{ url_for('user', username=current_user.username) }}">Profile</a>
<a href="{{ url_for('logout') }}">logout</a>
{% endif %}
<head>
<link rel="stylesheet" href="{{ url_for('static', filename='simple.css') }}">
{% if title %}
<title>{{ title }}</title>
{% else %}
<title>oily page</title>
{% endif %}
</head>
<body>
<header>
<nav>
<a {% block indexcurrent %}{% endblock %} href="{{ url_for('index') }}">home</a>
<a {% block explorecurrent %}{% endblock %} href="{{ url_for('explore') }}">explore</a>
{% if current_user.is_anonymous %}
<a {% block logincurrent %}{% endblock %} href="{{ url_for('login') }}">login</a>
{% else %}
<a {% block profilecurrent %}{% endblock %} href="{{ url_for('user', username=current_user.username) }}">profile</a>
<a href="{{ url_for('logout') }}">logout</a>
{% endif %}
<a href="https://gut.oily.dad/explore/repos">
<img style="vertical-align: middle; horizontal-align: center; height: 22px" src="https://gut.oily.dad/assets/img/logo.svg" alt="Logo" aria-hidden="true">
</a>
</nav>
</div>
<hr>
{% with messages = get_flashed_messages() %}
{% if messages %}
<ul>
{% for message in messages %}
<p class="notice">{{ message }}</p>
{% endfor %}
</ul>
{% endif %}
{% endwith %}
<h2>oily.dad</h2>
</header>
<hr>
{% with messages = get_flashed_messages() %}
{% if messages %}
<ul>
{% for message in messages %}
<p class="notice">{{ message }}</p>
{% endfor %}
</ul>
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</body>
{% block content %}{% endblock %}
</body>
</html>

View File

@ -1,24 +1,24 @@
{% extends "base.html" %}
{% block content %}
<h1>Edit Profile</h1>
<form action="" method="post">
{{ form.hidden_tag() }}
<p>
{{ form.username.label }}
{{ form.username(size=32) }}
{% for error in form.username.errors %}
<span style="color: red;">[{{ error }}]</span>
{% endfor %}
</p>
<p>
{{ form.about_me.label }}
{{ form.about_me(cols=50, rows=4) }}
{% for error in form.about_me.errors %}
<span style="color: red;">[{{ error }}]</span>
{% endfor %}
</p>
<p>{{ form.submit }}</p>
</form>
<h2>Edit Profile</h1>
<form action="" method="post">
{{ form.hidden_tag() }}
<p>
{{ form.username.label }}
{{ form.username(size=32) }}
{% for error in form.username.errors %}
<span style="color: red;">[{{ error }}]</span>
{% endfor %}
</p>
<p>
{{ form.about_me.label }}
{{ form.about_me(cols=50, rows=4) }}
{% for error in form.about_me.errors %}
<span style="color: red;">[{{ error }}]</span>
{% endfor %}
</p>
<p>{{ form.submit }}</p>
</form>
{% endblock %}

View File

@ -0,0 +1,10 @@
<!doctype html>
<html>
<body>
<p>User {{ user.username }} requested password reset.</p>
<p>Reset link:</p>
<p><a href="{{ hostname }}{{ url_for('reset_password', token=token) }}">click here</a>
<p>If you did not request this, ignore this message.</p>
</body>
</html>

View File

@ -0,0 +1,7 @@
User {{ user.username }} requested password reset.
Reset link follows.
{{ hostname }}{{ url_for('reset_password', token=token) }}
If you did not request this, ignore this message.

View File

@ -0,0 +1,30 @@
{% extends "base.html" %}
{% block explorecurrent %}class="current"{% endblock %}
{% block content %}
{% if form %}
<form action="" method="post">
{{ form.hidden_tag() }}
<p>
{{ form.post.label }}
{{ form.post(cols=32, rows=4) }}
{% for error in form.post.errors %}
<span style="color: red;">[{{ error }}]</span>
{% endfor %}
</p>
<p>{{ form.submit() }}</p>
</form>
{% endif %}
<table>
{% for post in posts %}
{% include '_post.html' %}
{% endfor %}
</table>
{% include '_postnav.html' %}
{% endblock %}

View File

@ -1,10 +1,29 @@
{% extends "base.html" %}
{% block indexcurrent %}class="current"{% endblock %}
{% block content %}
<h1>Hello, {{ current_user.username }}!</h1>
{% for post in posts %}
<div><p>{{ post.author.username }} says: <b>{{ post.body }}</b></p></div>
{% endfor %}
{% if form %}
<form action="" method="post">
{{ form.hidden_tag() }}
<p>
{{ form.post.label }}
{{ form.post(cols=32, rows=4) }}
{% for error in form.post.errors %}
<span style="color: red;">[{{ error }}]</span>
{% endfor %}
</p>
<p>{{ form.submit() }}</p>
</form>
{% endif %}
<table>
{% for post in posts %}
{% include '_post.html' %}
{% endfor %}
</table>
{% include '_postnav.html' %}
{% endblock %}

View File

@ -1,28 +1,33 @@
{% extends "base.html" %}
{% block content %}
<h1>Sign In</h1>
<form action="" method="post" novalidate>
{{ form.hidden_tag() }}
<p>
{{ form.username.label }}<br>
{{ form.username(size=32) }}<br>
{% for error in form.username.errors %}
<span style="color: red;">[{{ error }}]</span>
{% endfor %}
</p>
<p>
{{ form.password.label }}<br>
{{ form.password(size=32) }}<br>
{% for error in form.password.errors %}
<span style="color: red;">[{{ error }}]</span>
{% endfor %}
</p>
<p>{{ form.remember_me() }} {{ form.remember_me.label }}</p>
<p>{{ form.submit }}</p>
</form>
{% block logincurrent %}class="current"{% endblock %}
<p><a href="{{ url_for('register') }}">Register Here</a></p>
{% block content %}
<h2>Sign In</h1>
<form action="" method="post" novalidate>
{{ form.hidden_tag() }}
<p>
{{ form.username.label }}<br>
{{ form.username(size=32) }}<br>
{% for error in form.username.errors %}
<span style="color: red;">[{{ error }}]</span>
{% endfor %}
</p>
<p>
{{ form.password.label }}<br>
{{ form.password(size=32) }}<br>
{% for error in form.password.errors %}
<span style="color: red;">[{{ error }}]</span>
{% endfor %}
</p>
<p>{{ form.remember_me() }} {{ form.remember_me.label }}</p>
<p>
{{ form.submit }}
<a class="button" href="{{ url_for('register') }}">Register</a>
</p>
</form>
<p><a href="{{ url_for('reset_password_request') }}">Reset Password</a></p>
{% endblock %}

View File

@ -2,39 +2,39 @@
{% block content %}
<h1>Register</h1>
<form action="" method="post">
{{ form.hidden_tag() }}
<p>
{{ form.username.label }}<br>
{{ form.username(size=32) }}<br>
{% for error in form.username.errors %}
<span style="color: red;">[{{ error }}]</span>
{% endfor %}
</p>
<p>
{{ form.email.label }}<br>
{{ form.email(size=64) }}<br>
{% for error in form.email.errors %}
<span style="color: red;">[{{ error }}]</span>
{% endfor %}
</p>
<p>
{{ form.password.label }}<br>
{{ form.password(size=64) }}<br>
{% for error in form.password.errors %}
<span style="color: red;">[{{ error }}]</span>
{% endfor %}
</p>
<p>
{{ form.password2.label }}<br>
{{ form.password2(size=64) }}<br>
{% for error in form.password2.errors %}
<span style="color: red;">[{{ error }}]</span>
{% endfor %}
</p>
<p>{{ form.submit }}</p>
</form>
<h2>Register</h1>
<form action="" method="post">
{{ form.hidden_tag() }}
<p>
{{ form.username.label }}<br>
{{ form.username(size=32) }}<br>
{% for error in form.username.errors %}
<span style="color: red;">[{{ error }}]</span>
{% endfor %}
</p>
<p>
{{ form.email.label }}<br>
{{ form.email(size=64) }}<br>
{% for error in form.email.errors %}
<span style="color: red;">[{{ error }}]</span>
{% endfor %}
</p>
<p>
{{ form.password.label }}<br>
{{ form.password(size=64) }}<br>
{% for error in form.password.errors %}
<span style="color: red;">[{{ error }}]</span>
{% endfor %}
</p>
<p>
{{ form.password2.label }}<br>
{{ form.password2(size=64) }}<br>
{% for error in form.password2.errors %}
<span style="color: red;">[{{ error }}]</span>
{% endfor %}
</p>
<p>{{ form.submit }}</p>
</form>
{% endblock %}

View File

@ -0,0 +1,26 @@
{% extends "base.html" %}
{% block content %}
<h2>Reset Your Password</h1>
<form action="" method="post">
{{ form.hidden_tag() }}
<p>
{{ form.password.label }}
{{ form.password(size=32) }}
{% for error in form.password.errors %}
<span style="color: red;">[{{ error }}]</span>
{% endfor %}
</p>
<p>
{{ form.password2.label }}
{{ form.password2(size=32) }}
{% for error in form.password2.errors %}
<span style="color: red;">[{{ error }}]</span>
{% endfor %}
</p>
<p>{{ form.submit() }}</p>
</form>
{% endblock %}

View File

@ -0,0 +1,17 @@
{% extends "base.html" %}
{% block content %}
<h2>Reset Password</h1>
<form action="" method="post">
{{ form.hidden_tag() }}
<p>
{{ form.email.label }}<br>
{{ form.email(size=64) }}<br>
{% for error in form.email.errors %}
<span style="color: red;">[{{ error }}]</span>
{% endfor %}
</p>
<p>{{ form.submit() }}</p>
</form>
{% endblock %}

View File

@ -1,24 +1,42 @@
{% extends "base.html" %}
{% block profilecurrent %}class="current"{% endblock %}
{% block content %}
<table>
<tr valign="top">
<td><img src="data:image/png;base64,{{ user.gen_avatar(write_png=False) }}"></td>
<td>
<h1>User: {{ user.username }}</h1>
<h1>CUdebug: {{ current_user }}</h1>
<h1>Udebug: {{ user }}</h1>
{% if user.about_me %}<p>{{ user.about_me }}</p>{% endif %}
{% if user.last_seen %}<p>Last activity:{{ user.last_seen }}</p>{% endif %}
</td>
</tr>
</table>
{% if user == current_user %}
<p><a href="{{ url_for('edit_profile') }}">Edit Profile</a></p>
{% endif %}
<hr>
{% for post in posts %}
{% include '_post.html' %}
{% endfor %}
<article>
<h2>
<img style="vertical-align: middle; width: 60px;" src="data:image/png;base64,{{ user.gen_avatar(write_png=False) }}">
User: {{ user.username }}
</h2>
{% if user.about_me %}<p>{{ user.about_me }}</p>{% endif %}
{% if user.last_seen %}<p>Last activity: {{ user.last_seen }}</p>{% endif %}
{% if user == current_user %}
<p><a class="button" href="{{ url_for('edit_profile') }}">Edit Profile</a></p>
{% elif not current_user.is_following(user) %}
<p>
<form action="{{ url_for('follow', username=user.username) }}" method="post">
{{ form.hidden_tag() }}
{{ form.submit(value='Follow') }}
</form>
</p>
{% else %}
<p>
<form action="{{ url_for('unfollow', username=user.username) }}" method="post">
{{ form.hidden_tag() }}
{{ form.submit(value='Unfollow') }}
</form>
</p>
{% endif %}
</article>
<hr>
<table>
{% for post in posts %}
{% include '_post.html' %}
{% endfor %}
</table>
{% include '_postnav.html' %}
{% endblock %}

View File

@ -4,8 +4,22 @@ basedir = os.path.abspath(os.path.dirname(__file__))
# Remove or fallbacks for prod
class Config:
SECRET_KEY = os.environ.get('FLASK_SECRET_KEY') or 'flasksk'
SECRET_KEY = os.environ.get('DOTENV_FLASK_SECRET_KEY')
#SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'zapp.db')
SQLALCHEMY_DATABASE_URI = 'mariadb+mariadbconnector://flasku:' + os.environ.get('MYSQL_PASSWORD') + '@db:3306/flask'
SQLALCHEMY_DATABASE_URI = 'mariadb+mariadbconnector://flasku:' + os.environ.get('DOTENV_MYSQL_PASSWORD') + '@db:3306/flask'
MAIL_SERVER = 'pmb'
MAIL_PORT = 25
MAIL_USE_TLS = False
MAIL_USERNAME = ''
MAIL_PASSWORD = ''
ADMINS = [os.environ.get('DOTENV_ADMIN_EMAIL')]
FROM_ADDRESS = os.environ.get('DOTENV_FROM_ADDRESS')
REAL_HOSTNAME = os.environ.get('DOTENV_REAL_HOSTNAME')
ALLOW_REGISTRATION = os.environ.get('DOTENV_ALLOW_REGISTRATION')
DC_LOGGING = True
POSTS_PER_PAGE=5

4
backend/dbdb.sh Executable file
View File

@ -0,0 +1,4 @@
#!/bin/bash
# Okay to publish -- creds are local dev only
mariadb -hdb -uflasku -pflaskp flask

View File

@ -5,6 +5,7 @@ dnspython==2.6.1
email_validator==2.2.0
Flask==3.0.3
Flask-Login==0.6.3
Flask-Mail==0.10.0
Flask-Migrate==4.0.7
Flask-SQLAlchemy==3.1.1
Flask-WTF==1.2.1
@ -13,12 +14,15 @@ idna==3.7
itsdangerous==2.2.0
Jinja2==3.1.4
Mako==1.3.5
mariadb==1.1.10
MarkupSafe==2.1.5
packaging==24.1
pillow==10.4.0
pydenticon==0.3.1
PyJWT==2.9.0
python-dotenv==1.0.1
SQLAlchemy==2.0.31
typing_extensions==4.12.2
uWSGI==2.0.26
Werkzeug==3.0.3
WTForms==3.1.2
mariadb

95
backend/tests.py Normal file
View File

@ -0,0 +1,95 @@
import os
os.environ['DATABASE_URL'] = 'sqlite://'
from datetime import datetime, timezone, timedelta
import unittest
from app import app, db
from app.models import User, Post
class UserModelCase(unittest.TestCase):
def setUp(self):
self.app_context = app.app_context()
self.app_context.push()
db.create_all()
def tearDown(self):
db.session.remove()
db.drop_all()
self.app_context.pop()
def test_password_hashing(self):
u = User(username='susan', email='susan@example.com')
u.set_password('cat')
self.assertFalse(u.check_password('dog'))
self.assertTrue(u.check_password('cat'))
def test_follow(self):
u1 = User(username='john', email='john@example.com')
u2 = User(username='susan', email='susan@example.com')
db.session.add(u1)
db.session.add(u2)
db.session.commit()
following = db.session.scalars(u1.following.select()).all()
followers = db.session.scalars(u2.followers.select()).all()
self.assertEqual(following, [])
self.assertEqual(followers, [])
u1.follow(u2)
db.session.commit()
self.assertTrue(u1.is_following(u2))
self.assertEqual(u1.following_count(), 1)
self.assertEqual(u2.followers_count(), 1)
u1_following = db.session.scalars(u1.following.select()).all()
u2_followers = db.session.scalars(u2.followers.select()).all()
self.assertEqual(u1_following[0].username, 'susan')
self.assertEqual(u2_followers[0].username, 'john')
u1.unfollow(u2)
db.session.commit()
self.assertFalse(u1.is_following(u2))
self.assertEqual(u1.following_count(), 0)
self.assertEqual(u2.followers_count(), 0)
def test_follow_posts(self):
# create four users
u1 = User(username='john', email='john@example.com')
u2 = User(username='susan', email='susan@example.com')
u3 = User(username='mary', email='mary@example.com')
u4 = User(username='david', email='david@example.com')
db.session.add_all([u1, u2, u3, u4])
# create four posts
now = datetime.now(timezone.utc)
p1 = Post(body="post from john", author=u1,
timestamp=now + timedelta(seconds=1))
p2 = Post(body="post from susan", author=u2,
timestamp=now + timedelta(seconds=4))
p3 = Post(body="post from mary", author=u3,
timestamp=now + timedelta(seconds=3))
p4 = Post(body="post from david", author=u4,
timestamp=now + timedelta(seconds=2))
db.session.add_all([p1, p2, p3, p4])
db.session.commit()
# setup the followers
u1.follow(u2) # john follows susan
u1.follow(u4) # john follows david
u2.follow(u3) # susan follows mary
u3.follow(u4) # mary follows david
db.session.commit()
# check the following posts of each user
f1 = db.session.scalars(u1.following_posts()).all()
f2 = db.session.scalars(u2.following_posts()).all()
f3 = db.session.scalars(u3.following_posts()).all()
f4 = db.session.scalars(u4.following_posts()).all()
self.assertEqual(f1, [p2, p4, p1])
self.assertEqual(f2, [p2, p3])
self.assertEqual(f3, [p3, p4])
self.assertEqual(f4, [p4])
if __name__ == '__main__':
unittest.main(verbosity=2)

View File

@ -1,25 +1,23 @@
services:
db:
image: mariadb:lts
command: "--skip-name-resolve=OFF"
restart: always
healthcheck:
#test: ['CMD-SHELL', 'mysqladmin ping -h 127.0.0.1 --password="${DOTENV_MYSQL_ROOT_PASSWORD}" --silent']
test: ['CMD', 'healthcheck.sh', '--connect', '--innodb_initialized']
interval: 10s
retries: 5
timeout: 5s
start_period: 10s
start_period: 5s
volumes:
- db-data:/var/lib/mysql
- ./db/init:/docker-entrypoint-initdb.d/
- ./db/bu:/bu
networks:
- backnet
environment:
#- MYSQL_DATABASE=gitea
#- MYSQL_USER=gitea
#- MYSQL_PASSWORD=gitea
#- MYSQL_ROOT_PASSWORD=rootpass
- MYSQL_ROOT_PASSWORD=${DOTENV_MYSQL_ROOT_PASSWORD}
- MARIADB_ROOT_HOST=localhost
- MARIADB_ROOT_PASSWORD=${DOTENV_MYSQL_ROOT_PASSWORD}
expose:
- 3306
- 33060
@ -28,15 +26,25 @@ services:
build:
context: backend
target: builder
# Next two are only debug, used without restart
#stdin_open: true
#tty: true
restart: always
# Comment following line to use flask (1worker, dev), uncomment to use uwsgi (wsgi)
#command: ["uwsgi", "--http", "0.0.0.0:8000", "--master", "-p", "4", "-w", "app:server"]
command: ["uwsgi", "--http", "0.0.0.0:8000", "--master", "-p", "4", "-w", "microblog:app"]
container_name: backend
environment:
- MYSQL_USER=flasku
#- MYSQL_PASSWORD=flaskp
- MYSQL_PASSWORD=${DOTENV_MYSQL_FLASK_PASSWORD}
- TOKEN_I=${DOTENV_TOKEN_I}
- TOKEN_C=${DOTENV_TOKEN_C}
- DOTENV_MYSQL_PASSWORD=${DOTENV_MYSQL_FLASK_PASSWORD}
- DOTENV_FLASK_SECRET_KEY=${FLASK_SECRET_KEY}
- DOTENV_TOKEN_I=${FLASK_TOKEN_I}
- DOTENV_TOKEN_C=${FLASK_TOKEN_C}
- DOTENV_ADMIN_EMAIL=${FLASK_ADMIN_EMAIL}
- DOTENV_FROM_ADDRESS=${FLASK_MAIL_FROM}
- DOTENV_JWT_PHRASE=${FLASK_JWT_PHRASE}
- DOTENV_REAL_HOSTNAME=${FLASK_REAL_HOSTNAME}
- DOTENV_ALLOW_REGISTRATION=${ALLOW_REGISTRATION}
#ports:
# - 8000:8000
expose:
@ -69,8 +77,10 @@ services:
- GITEA__mailer__SMTP_PORT=25
- GITEA__service__REGISTER_EMAIL_CONFIRM=true
- GITEA__service__ENABLE_NOTIFY_MAIL=true
- GITEA__server__LANDING_PAGE=explore
- GITEA__ui__REACTIONS="+1, -1, fu, heart, laugh, confused, hooray, eyes, gun, boom, poop, kiss"
# To disable new users after setup:
#- GITEA__service__DISABLE_REGISTRATION=false
- GITEA__service__DISABLE_REGISTRATION=true
networks:
- backnet
- frontnet
@ -88,17 +98,27 @@ services:
proxy:
build: proxy
restart: always
#volumes:
# - /home/finn/d/cert/var/lib/letsencrypt:/var/lib/letsencrypt
# - /home/finn/d/cert/etc/letsencrypt:/etc/letsencrypt
volumes:
- /home/finn/d/cert/var/lib/letsencrypt:/var/lib/letsencrypt:ro
- /home/finn/d/cert/etc/letsencrypt:/etc/letsencrypt:ro
ports:
- 80:80
- 443:443
- "80:80"
- "443:443"
depends_on:
- backend
networks:
- frontnet
hs:
container_name: tor_service
build:
context: tor
environment:
- USE_TOR=${USE_TOR}
depends_on:
- backend
networks:
- frontnet
pmb:
#build:
# args:
@ -117,10 +137,26 @@ services:
networks:
- backnet
sshtun:
build:
context: sshtun
dockerfile: Dockerfile
restart: on-failure
environment:
- USE_TUN=${USE_TUN}
ports:
- "22222:22"
expose:
- "11112"
networks:
- frontnet
volumes:
db-data:
pmb-root:
networks:
backnet:
name: backnet
frontnet:
name: frontnet

View File

@ -1,25 +1,23 @@
services:
db:
image: mariadb:lts
command: "--skip-name-resolve=OFF"
restart: always
healthcheck:
#test: ['CMD-SHELL', 'mysqladmin ping -h 127.0.0.1 --password="${DOTENV_MYSQL_ROOT_PASSWORD}" --silent']
test: ['CMD', 'healthcheck.sh', '--connect', '--innodb_initialized']
interval: 10s
retries: 5
timeout: 5s
start_period: 10s
start_period: 5s
volumes:
- db-data:/var/lib/mysql
- ./db/init:/docker-entrypoint-initdb.d/
- ./db/bu:/bu
networks:
- backnet
environment:
#- MYSQL_DATABASE=gitea
#- MYSQL_USER=gitea
#- MYSQL_PASSWORD=gitea
#- MYSQL_ROOT_PASSWORD=rootpass
- MYSQL_ROOT_PASSWORD=${DOTENV_MYSQL_ROOT_PASSWORD}
- MARIADB_ROOT_HOST=localhost
- MARIADB_ROOT_PASSWORD=${DOTENV_MYSQL_ROOT_PASSWORD}
expose:
- 3306
- 33060
@ -28,15 +26,25 @@ services:
build:
context: backend
target: builder
# Next two are only debug, used without restart
#stdin_open: true
#tty: true
restart: always
# Comment following line to use flask (1worker, dev), uncomment to use uwsgi (wsgi)
#command: ["uwsgi", "--http", "0.0.0.0:8000", "--master", "-p", "4", "-w", "app:server"]
#command: ["uwsgi", "--http", "0.0.0.0:8000", "--master", "-p", "4", "-w", "microblog:app"]
container_name: backend
environment:
- MYSQL_USER=flasku
#- MYSQL_PASSWORD=flaskp
- MYSQL_PASSWORD=${DOTENV_MYSQL_FLASK_PASSWORD}
- TOKEN_I=${DOTENV_TOKEN_I}
- TOKEN_C=${DOTENV_TOKEN_C}
- DOTENV_MYSQL_PASSWORD=${DOTENV_MYSQL_FLASK_PASSWORD}
- DOTENV_FLASK_SECRET_KEY=${FLASK_SECRET_KEY}
- DOTENV_TOKEN_I=${FLASK_TOKEN_I}
- DOTENV_TOKEN_C=${FLASK_TOKEN_C}
- DOTENV_ADMIN_EMAIL=${FLASK_ADMIN_EMAIL}
- DOTENV_FROM_ADDRESS=${FLASK_MAIL_FROM}
- DOTENV_JWT_PHRASE=${FLASK_JWT_PHRASE}
- DOTENV_REAL_HOSTNAME=${FLASK_REAL_HOSTNAME}
- DOTENV_ALLOW_REGISTRATION=${ALLOW_REGISTRATION}
#ports:
# - 8000:8000
expose:
@ -70,7 +78,7 @@ services:
- GITEA__service__REGISTER_EMAIL_CONFIRM=true
- GITEA__service__ENABLE_NOTIFY_MAIL=true
# To disable new users after setup:
#- GITEA__service__DISABLE_REGISTRATION=false
- GITEA__service__DISABLE_REGISTRATION=true
networks:
- backnet
- frontnet
@ -89,16 +97,26 @@ services:
build: proxy
restart: always
#volumes:
# - /home/finn/d/cert/var/lib/letsencrypt:/var/lib/letsencrypt
# - /home/finn/d/cert/etc/letsencrypt:/etc/letsencrypt
# - /home/finn/d/cert/var/lib/letsencrypt:/var/lib/letsencrypt:ro
# - /home/finn/d/cert/etc/letsencrypt:/etc/letsencrypt:ro
ports:
- 80:80
- 443:443
- "80:80"
- "443:443"
depends_on:
- backend
networks:
- frontnet
hs:
container_name: tor_service
build:
context: tor
environment:
- USE_TOR=${USE_TOR}
depends_on:
- backend
networks:
- frontnet
pmb:
#build:
# args:
@ -117,10 +135,26 @@ services:
networks:
- backnet
sshtun:
build:
context: sshtun
dockerfile: Dockerfile
restart: on-failure
environment:
- USE_TUN=${USE_TUN}
ports:
- "22222:22"
expose:
- "11112"
networks:
- frontnet
volumes:
db-data:
pmb-root:
networks:
backnet:
name: backnet
frontnet:
name: frontnet

View File

@ -1,25 +1,23 @@
services:
db:
image: mariadb:lts
command: "--skip-name-resolve=OFF"
restart: always
healthcheck:
#10-focal test: ['CMD-SHELL', 'mysqladmin ping -h 127.0.0.1 --password="${DOTENV_MYSQL_ROOT_PASSWORD}" --silent']
test: ['CMD', 'healthcheck.sh', '--connect', '--innodb_initialized']
interval: 10s
retries: 5
timeout: 5s
start_period: 10s
start_period: 5s
volumes:
- db-data:/var/lib/mysql
- ./db/init:/docker-entrypoint-initdb.d/
- ./db/bu:/bu
networks:
- backnet
environment:
#- MYSQL_DATABASE=gitea
#- MYSQL_USER=gitea
#- MYSQL_PASSWORD=gitea
#- MYSQL_ROOT_PASSWORD=rootpass
- MYSQL_ROOT_PASSWORD=${DOTENV_MYSQL_ROOT_PASSWORD}
- MARIADB_ROOT_HOST=localhost
- MARIADB_ROOT_PASSWORD=${DOTENV_MYSQL_ROOT_PASSWORD}
expose:
- 3306
- 33060
@ -28,15 +26,25 @@ services:
build:
context: backend
target: builder
# Next two are only debug, used without restart
#stdin_open: true
#tty: true
restart: always
# Comment following line to use flask (1worker, dev), uncomment to use uwsgi (wsgi)
command: ["uwsgi", "--http", "0.0.0.0:8000", "--master", "-p", "4", "-w", "app:server"]
command: ["uwsgi", "--http", "0.0.0.0:8000", "--master", "-p", "4", "-w", "microblog:app"]
container_name: backend
environment:
- MYSQL_USER=flasku
#- MYSQL_PASSWORD=flaskp
- MYSQL_PASSWORD=${DOTENV_MYSQL_FLASK_PASSWORD}
- TOKEN_I=${DOTENV_TOKEN_I}
- TOKEN_C=${DOTENV_TOKEN_C}
- DOTENV_MYSQL_PASSWORD=${DOTENV_MYSQL_FLASK_PASSWORD}
- DOTENV_FLASK_SECRET_KEY=${FLASK_SECRET_KEY}
- DOTENV_TOKEN_I=${FLASK_TOKEN_I}
- DOTENV_TOKEN_C=${FLASK_TOKEN_C}
- DOTENV_ADMIN_EMAIL=${FLASK_ADMIN_EMAIL}
- DOTENV_FROM_ADDRESS=${FLASK_MAIL_FROM}
- DOTENV_JWT_PHRASE=${FLASK_JWT_PHRASE}
- DOTENV_REAL_HOSTNAME=${FLASK_REAL_HOSTNAME}
- DOTENV_ALLOW_REGISTRATION=${ALLOW_REGISTRATION}
#ports:
# - 8000:8000
expose:
@ -69,8 +77,10 @@ services:
- GITEA__mailer__SMTP_PORT=25
- GITEA__service__REGISTER_EMAIL_CONFIRM=true
- GITEA__service__ENABLE_NOTIFY_MAIL=true
- GITEA__server__LANDING_PAGE=explore
- GITEA__ui__REACTIONS="+1, -1, fu, heart, laugh, confused, hooray, eyes, gun, boom, poop, kiss"
# To disable new users after setup:
#- GITEA__service__DISABLE_REGISTRATION=false
- GITEA__service__DISABLE_REGISTRATION=true
networks:
- backnet
- frontnet
@ -89,16 +99,26 @@ services:
build: proxy
restart: always
volumes:
- /home/finn/d/cert/var/lib/letsencrypt:/var/lib/letsencrypt
- /home/finn/d/cert/etc/letsencrypt:/etc/letsencrypt
- /home/finn/d/cert/var/lib/letsencrypt:/var/lib/letsencrypt:ro
- /home/finn/d/cert/etc/letsencrypt:/etc/letsencrypt:ro
ports:
- 80:80
- 443:443
- "80:80"
- "443:443"
depends_on:
- backend
networks:
- frontnet
hs:
container_name: tor_service
build:
context: tor
environment:
- USE_TOR=${USE_TOR}
depends_on:
- backend
networks:
- frontnet
pmb:
#build:
# args:
@ -117,10 +137,26 @@ services:
networks:
- backnet
sshtun:
build:
context: sshtun
dockerfile: Dockerfile
restart: on-failure
environment:
- USE_TUN=${USE_TUN}
ports:
- "22222:22"
expose:
- "11112"
networks:
- frontnet
volumes:
db-data:
pmb-root:
networks:
backnet:
name: backnet
frontnet:
name: frontnet

View File

@ -3,10 +3,10 @@ CREATE DATABASE IF NOT EXISTS `gitea`;
CREATE DATABASE IF NOT EXISTS `flask`;
-- create root user and grant rights
CREATE USER 'gitea' IDENTIFIED BY 'giteap';
CREATE USER 'flasku' IDENTIFIED BY 'flaskp';
CREATE USER 'gitea'@'gitea.backnet' IDENTIFIED BY 'giteap';
CREATE USER 'flasku'@'backend.backnet' IDENTIFIED BY 'flaskp';
--CREATE USER 'gitea'@'localhost' IDENTIFIED BY 'gitea';
--GRANT ALL ON `gitea` TO 'gitea'@'localhost';
GRANT ALL ON gitea.* TO 'gitea';
GRANT ALL ON flask.* TO 'flasku';
GRANT ALL ON gitea.* TO 'gitea'@'gitea.backnet';
GRANT ALL ON flask.* TO 'flasku'@'backend.backnet';

32
dotenv
View File

@ -1,20 +1,34 @@
# Example .env file
DOTENV_MYSQL_ROOT_PASSWORD_OLD=rootp
DOTENV_MYSQL_ROOT_PASSWORD=rootp
DOTENV_MYSQL_ROOT_PASSWORD_OLD="rootp"
DOTENV_MYSQL_ROOT_PASSWORD="rootp"
DOTENV_MYSQL_GITEA_PASSWORD=giteap
DOTENV_MYSQL_FLASK_PASSWORD=flaskp
DOTENV_MYSQL_GITEA_PASSWORD="giteap"
DOTENV_MYSQL_FLASK_PASSWORD="flaskp"
GITEA_MAIL_FROM=
GITEA_MAIL_FROM="git@aaa"
# Build ARG GPG_PP. May still need to be empty to avoid breakage.
BUILD_GPG_PP=
# Tor:
# true/false:
USE_TOR=false
# SSH Tun:
# true/false:
USE_TUN=false
# Backend:
FLASK_SECRET_KEY="flaskkey"
# Inconsequential token: minimal inconvenience if exposed
DOTENV_TOKEN_I=dti
FLASK_TOKEN_I="dti"
# Consequential token: protect
DOTENV_TOKEN_C=dtc
FLASK_TOKEN_C="dtc"
# true/false:
ALLOW_REGISTRATION=true
FLASK_MAIL_FROM="git@aaa"
# admin email must be valid send from with mail subsystem
FLASK_ADMIN_EMAIL="git@aaa"
FLASK_JWT_PHRASE="jwtphrase"
FLASK_REAL_HOSTNAME="localhost"

27
other/dbbu.sh Executable file
View File

@ -0,0 +1,27 @@
#!/bin/bash
# dump sql db backups
#
if [[ -z $1 ]] ; then
echo "dbbu.sh <gitea|flask|all>"
exit 0
fi
source ../.env
source .env
TIMESTAMP=$(date +%s)
if [[ $1 == "gitea" || $1 == "all" ]] ; then
docker-compose exec db bash -c "mariadb-dump -uroot -p$DOTENV_MYSQL_ROOT_PASSWORD gitea > /bu/gitea_bu_$TIMESTAMP.sql"
docker-compose exec db chmod a+rw /bu/gitea_bu_$TIMESTAMP.sql
docker-compose exec db chown ubuntu:ubuntu /bu/gitea_bu_$TIMESTAMP.sql
fi
if [[ $1 == "flask" || $1 == "all" ]] ; then
docker-compose exec db echo $DOTENV_MYSQL_ROOT_PASSWORD
docker-compose exec db bash -c "mariadb-dump -uroot -p$DOTENV_MYSQL_ROOT_PASSWORD flask > /bu/flask_bu_$TIMESTAMP.sql"
docker-compose exec db chmod a+rw /bu/flask_bu_$TIMESTAMP.sql
docker-compose exec db chown ubuntu:ubuntu /bu/flask_bu_$TIMESTAMP.sql
fi

View File

@ -19,10 +19,10 @@ echo "Changing app db passwords in 5 seconds..."
sleep 6
# Flask
docker-compose exec db mariadb --database=mysql -uroot -p$DOTENV_MYSQL_ROOT_PASSWORD_OLD -e "ALTER USER 'flasku' IDENTIFIED BY '"$DOTENV_MYSQL_FLASK_PASSWORD"';"
docker-compose exec db mariadb --database=mysql -uroot -p$DOTENV_MYSQL_ROOT_PASSWORD_OLD -e "ALTER USER 'flasku'@'backend.backnet' IDENTIFIED BY '"$DOTENV_MYSQL_FLASK_PASSWORD"';"
# Gitea
docker-compose exec db mariadb --database=mysql -uroot -p$DOTENV_MYSQL_ROOT_PASSWORD_OLD -e "ALTER USER 'gitea' IDENTIFIED BY '"$DOTENV_MYSQL_GITEA_PASSWORD"';"
docker-compose exec db mariadb --database=mysql -uroot -p$DOTENV_MYSQL_ROOT_PASSWORD_OLD -e "ALTER USER 'gitea'@'gitea.backnet' IDENTIFIED BY '"$DOTENV_MYSQL_GITEA_PASSWORD"';"
docker-compose exec db mariadb --database=mysql -uroot -p$DOTENV_MYSQL_ROOT_PASSWORD_OLD -e "FLUSH PRIVILEGES;"

View File

@ -4,5 +4,9 @@ server {
location / {
proxy_pass http://backend:8000;
}
location /gutty{
proxy_pass http://gitea:3000;
}
}

View File

@ -1,8 +1,53 @@
server {
listen 80;
server_name localhost;
location / {
proxy_pass http://backend:8000;
}
#server {
# listen 80;
# server_name localhost;
# location / {
# proxy_pass http://backend:8000;
# }
# always redirect to https
server {
listen 80 default_server;
server_name _;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
# use the certificates
ssl_certificate /etc/letsencrypt/live/oily.dad/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/oily.dad/privkey.pem;
server_name oily.dad www.oily.dad;
root /var/www/html;
index index.php index.html index.htm;
add_header Onion-Location http://oilydada7ckiseinkbeathsefwgkvjrce743xy7x7iiybkuxh4vheead.onion$request_uri;
location / {
proxy_pass http://backend:8000/;
}
}
server {
listen 443 ssl http2;
# use the certificates
ssl_certificate /etc/letsencrypt/live/oily.dad/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/oily.dad/privkey.pem;
server_name gut.oily.dad;
root /var/www/html;
index index.php index.html index.htm;
location / {
client_max_body_size 512M;
#proxy_pass http://localhost:3000;
proxy_set_header Connection $http_connection;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://gitea:3000/;
}
}

17
proxy/giteaconf Normal file
View File

@ -0,0 +1,17 @@
server {
listen 80;
server_name localhost;
location / {
client_max_body_size 512M;
#proxy_pass http://localhost:3000;
proxy_pass http://gitea:3000;
proxy_set_header Connection $http_connection;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}

View File

@ -22,6 +22,7 @@ server {
root /var/www/html;
index index.php index.html index.htm;
add_header Onion-Location http://oilydada7ckiseinkbeathsefwgkvjrce743xy7x7iiybkuxh4vheead.onion$request_uri;
location / {
proxy_pass http://backend:8000/;

18
sshtun/Dockerfile Normal file
View File

@ -0,0 +1,18 @@
FROM debian:12-slim
RUN apt update && apt install -y openssh-server socat
RUN adduser --disabled-password --gecos "" finn
RUN mkdir /home/finn/.ssh
# only one pubkey -- wildcard to conceal filename
COPY ./oilykey/*.pub /home/finn/.ssh/authorized_keys
RUN mkdir /var/run/sshd
RUN echo "PermitRootLogin no" >> /etc/ssh/sshd_config
RUN echo "PasswordAuthentication no" >> /etc/ssh/sshd_config
COPY ./entrypoint.sh /
ENTRYPOINT ["/entrypoint.sh"]

14
sshtun/entrypoint.sh Executable file
View File

@ -0,0 +1,14 @@
#!/bin/bash
# Container goal: egress
# first: physical_box$ autossh -N -R 11111:localhost:11434 -i sshtun/oilykey/<SOMEKEY> -p 22222 <rem_vps_url>
# will forward rem_c_port:physical_box:physical_box_port ...some args... rem_vps_p rem_vps_url
# then: frontnet_c$ curl sshtun.frontnet:11112 --> physical_box:11434
if $USE_TUN ; then
echo "@@@@@@@@@@ SSH TUNNEL ENABLED BY ENV"
nohup socat TCP-LISTEN:11112,fork TCP:localhost:11111 &
/usr/sbin/sshd -De
else
echo "@@@@@@@@@@ SSH TUNNEL DISABLED BY ENV"
fi

20
tor/Dockerfile Normal file
View File

@ -0,0 +1,20 @@
FROM alpine
RUN adduser --disabled-password --gecos "" tor
RUN apk update && apk add tor
COPY hidden_service /hidden_service
COPY torrc /etc/tor/torrc
COPY entrypoint.sh /
RUN chown -R tor /etc/tor
RUN chown -R tor /hidden_service
RUN chown -R tor /entrypoint.sh
RUN chmod -R go-rwx /etc/tor
RUN chmod -R go-rwx /hidden_service
USER tor
ENTRYPOINT ["/entrypoint.sh"]

5
tor/README.md Normal file
View File

@ -0,0 +1,5 @@
## Tor entry:
Untracked in this dir, a directory called "hidden_service".
This dir is sensitive, and requires manual backup strategy.

8
tor/entrypoint.sh Executable file
View File

@ -0,0 +1,8 @@
#!/bin/ash
if $USE_TOR ; then
echo "@@@@@@@@@@ TOR ENABLED BY ENV"
exec /usr/bin/tor
else
echo "@@@@@@@@@@ TOR DISABLED BY ENV"
fi

195
tor/torrc Normal file
View File

@ -0,0 +1,195 @@
## Configuration file for a typical Tor user
## Last updated 9 October 2013 for Tor 0.2.5.2-alpha.
## (may or may not work for much older or much newer versions of Tor.)
##
## Lines that begin with "## " try to explain what's going on. Lines
## that begin with just "#" are disabled commands: you can enable them
## by removing the "#" symbol.
##
## See 'man tor', or https://www.torproject.org/docs/tor-manual.html,
## for more options you can use in this file.
##
## Tor will look for this file in various places based on your platform:
## https://www.torproject.org/docs/faq#torrc
## Tor opens a socks proxy on port 9050 by default -- even if you don't
## configure one below. Set "SocksPort 0" if you plan to run Tor only
## as a relay, and not make any local application connections yourself.
#SocksPort 9050 # Default: Bind to localhost:9050 for local connections.
#SocksPort 192.168.0.1:9100 # Bind to this address:port too.
## Entry policies to allow/deny SOCKS requests based on IP address.
## First entry that matches wins. If no SocksPolicy is set, we accept
## all (and only) requests that reach a SocksPort. Untrusted users who
## can access your SocksPort may be able to learn about the connections
## you make.
#SocksPolicy accept 192.168.0.0/16
#SocksPolicy reject *
## Logs go to stdout at level "notice" unless redirected by something
## else, like one of the below lines. You can have as many Log lines as
## you want.
##
## We advise using "notice" in most cases, since anything more verbose
## may provide sensitive information to an attacker who obtains the logs.
##
## Send all messages of level 'notice' or higher to /var/log/tor/notices.log
#Log notice file /var/log/tor/notices.log
## Send every possible message to /var/log/tor/debug.log
#Log debug file /var/log/tor/debug.log
## Use the system log instead of Tor's logfiles
#Log notice syslog
## To send all messages to stderr:
#Log debug stderr
## Uncomment this to start the process in the background... or use
## --runasdaemon 1 on the command line. This is ignored on Windows;
## see the FAQ entry if you want Tor to run as an NT service.
#RunAsDaemon 1
## The directory for keeping all the keys/etc. By default, we store
## things in $HOME/.tor on Unix, and in Application Data\tor on Windows.
#DataDirectory /var/lib/tor
## The port on which Tor will listen for local connections from Tor
## controller applications, as documented in control-spec.txt.
#ControlPort 9051
## If you enable the controlport, be sure to enable one of these
## authentication methods, to prevent attackers from accessing it.
#HashedControlPassword 16:872860B76453A77D60CA2BB8C1A7042072093276A3D701AD684053EC4C
#CookieAuthentication 1
############### This section is just for location-hidden services ###
## Once you have configured a hidden service, you can look at the
## contents of the file ".../hidden_service/hostname" for the address
## to tell people.
##
## HiddenServicePort x y:z says to redirect requests on port x to the
## address y:z.
HiddenServiceDir /hidden_service/
HiddenServicePort 80 backend:8000
#HiddenServiceDir /var/lib/tor/hidden_service/
#HiddenServicePort 80 127.0.0.1:80
#HiddenServiceDir /var/lib/tor/other_hidden_service/
#HiddenServicePort 80 127.0.0.1:80
#HiddenServicePort 22 127.0.0.1:22
################ This section is just for relays #####################
#
## See https://www.torproject.org/docs/tor-doc-relay for details.
## Required: what port to advertise for incoming Tor connections.
#ORPort 9001
## If you want to listen on a port other than the one advertised in
## ORPort (e.g. to advertise 443 but bind to 9090), you can do it as
## follows. You'll need to do ipchains or other port forwarding
## yourself to make this work.
#ORPort 443 NoListen
#ORPort 127.0.0.1:9090 NoAdvertise
## The IP address or full DNS name for incoming connections to your
## relay. Leave commented out and Tor will guess.
#Address noname.example.com
## If you have multiple network interfaces, you can specify one for
## outgoing traffic to use.
# OutboundBindAddress 10.0.0.5
## A handle for your relay, so people don't have to refer to it by key.
#Nickname ididnteditheconfig
## Define these to limit how much relayed traffic you will allow. Your
## own traffic is still unthrottled. Note that RelayBandwidthRate must
## be at least 20 KB.
## Note that units for these config options are bytes per second, not bits
## per second, and that prefixes are binary prefixes, i.e. 2^10, 2^20, etc.
#RelayBandwidthRate 100 KB # Throttle traffic to 100KB/s (800Kbps)
#RelayBandwidthBurst 200 KB # But allow bursts up to 200KB/s (1600Kbps)
## Use these to restrict the maximum traffic per day, week, or month.
## Note that this threshold applies separately to sent and received bytes,
## not to their sum: setting "4 GB" may allow up to 8 GB total before
## hibernating.
##
## Set a maximum of 4 gigabytes each way per period.
#AccountingMax 4 GB
## Each period starts daily at midnight (AccountingMax is per day)
#AccountingStart day 00:00
## Each period starts on the 3rd of the month at 15:00 (AccountingMax
## is per month)
#AccountingStart month 3 15:00
## Administrative contact information for this relay or bridge. This line
## can be used to contact you if your relay or bridge is misconfigured or
## something else goes wrong. Note that we archive and publish all
## descriptors containing these lines and that Google indexes them, so
## spammers might also collect them. You may want to obscure the fact that
## it's an email address and/or generate a new address for this purpose.
#ContactInfo Random Person <nobody AT example dot com>
## You might also include your PGP or GPG fingerprint if you have one:
#ContactInfo 0xFFFFFFFF Random Person <nobody AT example dot com>
## Uncomment this to mirror directory information for others. Please do
## if you have enough bandwidth.
#DirPort 9030 # what port to advertise for directory connections
## If you want to listen on a port other than the one advertised in
## DirPort (e.g. to advertise 80 but bind to 9091), you can do it as
## follows. below too. You'll need to do ipchains or other port
## forwarding yourself to make this work.
#DirPort 80 NoListen
#DirPort 127.0.0.1:9091 NoAdvertise
## Uncomment to return an arbitrary blob of html on your DirPort. Now you
## can explain what Tor is if anybody wonders why your IP address is
## contacting them. See contrib/tor-exit-notice.html in Tor's source
## distribution for a sample.
#DirPortFrontPage /etc/tor/tor-exit-notice.html
## Uncomment this if you run more than one Tor relay, and add the identity
## key fingerprint of each Tor relay you control, even if they're on
## different networks. You declare it here so Tor clients can avoid
## using more than one of your relays in a single circuit. See
## https://www.torproject.org/docs/faq#MultipleRelays
## However, you should never include a bridge's fingerprint here, as it would
## break its concealability and potentionally reveal its IP/TCP address.
#MyFamily $keyid,$keyid,...
## A comma-separated list of exit policies. They're considered first
## to last, and the first match wins. If you want to _replace_
## the default exit policy, end this with either a reject *:* or an
## accept *:*. Otherwise, you're _augmenting_ (prepending to) the
## default exit policy. Leave commented to just use the default, which is
## described in the man page or at
## https://www.torproject.org/documentation.html
##
## Look at https://www.torproject.org/faq-abuse.html#TypicalAbuses
## for issues you might encounter if you use the default exit policy.
##
## If certain IPs and ports are blocked externally, e.g. by your firewall,
## you should update your exit policy to reflect this -- otherwise Tor
## users will be told that those destinations are down.
##
## For security, by default Tor rejects connections to private (local)
## networks, including to your public IP address. See the man page entry
## for ExitPolicyRejectPrivate if you want to allow "exit enclaving".
##
#ExitPolicy accept *:6660-6667,reject *:* # allow irc ports but no more
#ExitPolicy accept *:119 # accept nntp as well as default exit policy
#ExitPolicy reject *:* # no exits allowed
## Bridge relays (or "bridges") are Tor relays that aren't listed in the
## main directory. Since there is no complete public list of them, even an
## ISP that filters connections to all the known Tor relays probably
## won't be able to block all the bridges. Also, websites won't treat you
## differently because they won't know you're running Tor. If you can
## be a real relay, please do; but if not, be a bridge!
#BridgeRelay 1
## By default, Tor will advertise your bridge to users through various
## mechanisms like https://bridges.torproject.org/. If you want to run
## a private bridge, for example because you'll give out your bridge
## address manually to your friends, uncomment this line:
#PublishServerDescriptor 0