Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Additional Container to Container Version #2

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions container_to_container/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
FROM python:3.12.4

RUN apt-get update
RUN apt -y install mc
RUN apt -y install docker.io
RUN pip install pymongo
RUN pip install email_validator
RUN pip install flask
RUN pip install gunicorn
RUN pip install requests
RUN pip install BeautifulSoup4
RUN apt -y install bash
RUN pip install --upgrade pip
RUN pip install flask_wtf
RUN pip install wtforms
RUN pip install flask_recaptcha
RUN pip install Markup
RUN pip install captcha Pillow

COPY tools .

EXPOSE 80

ENTRYPOINT ["/bin/bash", "-c", "gunicorn wsgi:app --bind 0.0.0.0:80"]


47 changes: 47 additions & 0 deletions container_to_container/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# A different approach

This approach uses container to container communication. This requires that the bug in /overleaf/services/web/app/src/Features/User/UserRegistrationHandler.js is fixed. see https://github.com/overleaf/overleaf/issues/1206

EmailHandler.promises.sendEmail('registered', {

is missing an await.

await EmailHandler.promises.sendEmail('registered', {

I fixed the file outside of the container and mount it into the overleaf container (i.e. modify compose.yaml) via
volumes:
- /root/overleafserver/UserRegistrationHandler.js:/overleaf/services/web/app/src/Features/User/UserRegistrationHandler.js

# More restrictive

We can block users via the tools/blocked_users.json. And we allow domains for tools/allowed_domains.json for which creation of account is allowed. e.g. uni-bremen.de allows @*.uni-bremen.de and @uni-bremen.de. Furthmore, invited people can also create accounts.

# No google

I replaced the google captcha because of data privacy reasons... Just, kidding I wasn't able to make it run. Thus I replaced it by a python solution.

# How to use this version

Set a secret key in tools/secret_key.json.

Set tools/allowed_domains.json to your needs.

Make sure that in tools/main.py is set correctly for your setup:

container_name_mongo:str = "overleafmongo"

port_mongo: int = 27017

container_name_overleaf: str = "overleafserver"

When you are happy with the setting, run:

make_image.sh

And then

up.sh

Don't forget to set your proxy correctly. An example for nginx see nginx.conf.

A full working setup can be found here https://github.com/davrot/overleaf
136 changes: 136 additions & 0 deletions container_to_container/UserRegistrationHandler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
const { User } = require('../../models/User')
const UserCreator = require('./UserCreator')
const UserGetter = require('./UserGetter')
const AuthenticationManager = require('../Authentication/AuthenticationManager')
const NewsletterManager = require('../Newsletter/NewsletterManager')
const logger = require('@overleaf/logger')
const crypto = require('crypto')
const EmailHandler = require('../Email/EmailHandler')
const OneTimeTokenHandler = require('../Security/OneTimeTokenHandler')
const settings = require('@overleaf/settings')
const EmailHelper = require('../Helpers/EmailHelper')
const {
callbackify,
callbackifyMultiResult,
} = require('@overleaf/promise-utils')
const OError = require('@overleaf/o-error')

const UserRegistrationHandler = {
_registrationRequestIsValid(body) {
const invalidEmail = AuthenticationManager.validateEmail(body.email || '')
const invalidPassword = AuthenticationManager.validatePassword(
body.password || '',
body.email
)
return !(invalidEmail || invalidPassword)
},

async _createNewUserIfRequired(user, userDetails) {
if (!user) {
userDetails.holdingAccount = false
return await UserCreator.promises.createNewUser(
{
holdingAccount: false,
email: userDetails.email,
first_name: userDetails.first_name,
last_name: userDetails.last_name,
analyticsId: userDetails.analyticsId,
},
{}
)
}
return user
},

async registerNewUser(userDetails) {
const requestIsValid =
UserRegistrationHandler._registrationRequestIsValid(userDetails)

if (!requestIsValid) {
throw new Error('request is not valid')
}
userDetails.email = EmailHelper.parseEmail(userDetails.email)

let user = await UserGetter.promises.getUserByAnyEmail(userDetails.email)
if (user && user.holdingAccount === false) {
// We add userId to the error object so that the calling function can access
// the id of the already existing user account.
throw new OError('EmailAlreadyRegistered', { userId: user._id })
}

user = await UserRegistrationHandler._createNewUserIfRequired(
user,
userDetails
)

await User.updateOne(
{ _id: user._id },
{ $set: { holdingAccount: false } }
).exec()

await AuthenticationManager.promises.setUserPassword(
user,
userDetails.password
)

if (userDetails.subscribeToNewsletter === 'true') {
try {
NewsletterManager.subscribe(user)
} catch (error) {
logger.warn(
{ err: error, user },
'Failed to subscribe user to newsletter'
)
throw error
}
}

return user
},

async registerNewUserAndSendActivationEmail(email) {
let user
try {
user = await UserRegistrationHandler.registerNewUser({
email,
password: crypto.randomBytes(32).toString('hex'),
})
} catch (error) {
if (error.message === 'EmailAlreadyRegistered') {
logger.debug({ email }, 'user already exists, resending welcome email')
user = await UserGetter.promises.getUserByAnyEmail(email)
} else {
throw error
}
}

const ONE_WEEK = 7 * 24 * 60 * 60 // seconds
const token = await OneTimeTokenHandler.promises.getNewToken(
'password',
{ user_id: user._id.toString(), email: user.email },
{ expiresIn: ONE_WEEK }
)

const setNewPasswordUrl = `${settings.siteUrl}/user/activate?token=${token}&user_id=${user._id}`

try {
await EmailHandler.promises.sendEmail('registered', {
to: user.email,
setNewPasswordUrl,
})
} catch (error) {
logger.warn({ err: error }, 'failed to send activation email')
}

return { user, setNewPasswordUrl }
},
}

module.exports = {
registerNewUser: callbackify(UserRegistrationHandler.registerNewUser),
registerNewUserAndSendActivationEmail: callbackifyMultiResult(
UserRegistrationHandler.registerNewUserAndSendActivationEmail,
['user', 'setNewPasswordUrl']
),
promises: UserRegistrationHandler,
}
20 changes: 20 additions & 0 deletions container_to_container/compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
services:
overleafregister:
image: "overleafregister_image"
container_name: overleafregister
hostname: overleafregister
restart: always

networks:
- overleaf-network

volumes:
- overleaf_register:/data
- /var/run/docker.sock:/var/run/docker.sock

volumes:
overleaf_register:

networks:
overleaf-network:
external: true
2 changes: 2 additions & 0 deletions container_to_container/down.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
docker compose down

1 change: 1 addition & 0 deletions container_to_container/exec.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
docker exec -it overleafregister bash
2 changes: 2 additions & 0 deletions container_to_container/logs.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
docker compose logs -f

1 change: 1 addition & 0 deletions container_to_container/make_image.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
docker build --network host -t overleafregister_image .
43 changes: 43 additions & 0 deletions container_to_container/nginx.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
events {}
http {
server {
listen 80 default_server;
server_name _;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
ssl_certificate /certs/nginx_certificate.pem;
ssl_certificate_key /certs/nginx_key.pem;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_ciphers EECDH+CHACHA20:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5;
add_header Strict-Transport-Security "max-age=31536000; includeSubdomains;";
server_tokens off;
client_max_body_size 50M;

location / {
proxy_pass http://overleafserver:80;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_read_timeout 3m;
proxy_send_timeout 3m;
}

location /register {
proxy_pass http://overleafpython:80;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_read_timeout 3m;
proxy_send_timeout 3m;
}
}
}
5 changes: 5 additions & 0 deletions container_to_container/tools/allowed_domains.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"allowed_domains": [
"uni-bremen.de"
]
}
6 changes: 6 additions & 0 deletions container_to_container/tools/blocked_users.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"blocked_users": [
""
]
}

13 changes: 13 additions & 0 deletions container_to_container/tools/check_invites.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import pymongo

def check_invites(email_to_find: str,container_name:str = "overleafmongo", port: int = 27017) -> bool:
client = pymongo.MongoClient(container_name, port)
db = client.sharelatex
project_invites = db.projectInvites

search_result = project_invites.find_one({"email": email_to_find})
if search_result is None:
return False
else:
return True

13 changes: 13 additions & 0 deletions container_to_container/tools/check_user.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import pymongo

def check_user(email_to_find: str, container_name:str = "overleafmongo", port: int = 27017) -> bool:
client = pymongo.MongoClient(container_name, port)
db = client.sharelatex
users = db.users

search_result = users.find_one({"email": email_to_find})
if search_result is None:
return False
else:
return True

14 changes: 14 additions & 0 deletions container_to_container/tools/create_account.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import subprocess

def create_account(email: str = "[email protected]", container_name: str = "overleafserver"):

work_string: str = f"cd /overleaf/services/web && node modules/server-ce-scripts/scripts/create-user --email={email}"
result = subprocess.run(["docker", "exec", container_name, "/bin/bash", "-ce", work_string ], capture_output=True, text=True)

success: bool = False
for i in result.stdout.splitlines():
if i.startswith(f"Successfully created {email} as a"):
success = True
return success


53 changes: 53 additions & 0 deletions container_to_container/tools/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import json
from flask import Flask, render_template, request, Response, send_from_directory, session
from io import BytesIO
from captcha.image import ImageCaptcha
import random
import base64
from process_emails import process_emails

container_name_mongo:str = "overleafmongo"
port_mongo: int = 27017
container_name_overleaf: str = "overleafserver"

app = Flask(__name__)

with open("secret_key.json", "r") as file:
secret_key: dict = json.load(file)

assert secret_key is not None
assert secret_key["secret_key"] is not None
app.config['SECRET_KEY'] = secret_key["secret_key"]


def generate_captcha():
image = ImageCaptcha(width=280, height=90)
captcha_text = ''.join(random.choices('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', k=6))
data = image.generate(captcha_text)
return captcha_text, data

@app.route("/register", methods=["GET", "POST"])
def index() -> Response:

if request.method == "GET":
captcha_text, captcha_image = generate_captcha()
session['captcha'] = captcha_text
captcha_base64 = base64.b64encode(captcha_image.getvalue()).decode('utf-8')
return render_template('post.html', captcha_image=captcha_base64)

elif request.method == "POST":
post_content = request.form.get('content')
email = request.form.get('email')
user_captcha = request.form.get('captcha')

if user_captcha and user_captcha.upper() == session.get('captcha'):
if process_emails(mail_address=email,container_name_mongo=container_name_mongo,port_mongo=port_mongo,container_name_overleaf=container_name_overleaf):
return f"A email was sent to {email}. Please click the activation link. Please check your spam folder! <p> <a href='https://overleaf.neuro.uni-bremen.de/'>Back to the overleaf site...</a>"
else:
return f"We couldn't register your email {email}."
else:
return f"There was a problem with solving the captcha. Try again. Sorry!"

@app.route("/register/static/<path:path>", methods=["GET"])
def serve_static_files(path) -> Response:
return send_from_directory("static", path)
Loading