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

Redditbot with encrypted config file #556

Closed
wants to merge 15 commits into from
Closed
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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
*.bak
*.pyc
.DS_Store
.venv
*.swp
*.pyo
*.secret
*.swp
*.tmp
*~
.DS_Store
.vagrant/
redditbot_replied.json
.venv
__pycache__
github_pat.secret
Expand Down
23 changes: 17 additions & 6 deletions chaos.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ class LessThanFilter(logging.Filter):
"""
Source: https://stackoverflow.com/questions/2302315
"""

def __init__(self, exclusive_maximum, name=""):
super(LessThanFilter, self).__init__(name)
self.max_level = exclusive_maximum
Expand All @@ -58,10 +59,14 @@ def main():
logging_handler_err = logging.StreamHandler(sys.stderr)
logging_handler_err.setLevel(settings.LOG_LEVEL_ERR)

logging.basicConfig(level=logging.NOTSET,
format='%(asctime)s %(name)-12s %(levelname)-8s %(message)s',
datefmt='%m-%d %H:%M',
handlers=[logging_handler_out, logging_handler_err])
logging.basicConfig(
level=logging.NOTSET,
format='%(asctime)s %(name)-12s %(levelname)-8s %(message)s',
datefmt='%m-%d %H:%M',
handlers=[
logging_handler_out,
logging_handler_err
])

logging.getLogger("requests").propagate = False
logging.getLogger("sh").propagate = False
Expand Down Expand Up @@ -91,6 +96,7 @@ def main():
" - starting up and entering event loop",
api_twitter.GetApi())

os.system("pkill chaos_redditbot")
os.system("pkill uwsgi")

subprocess.Popen(["/root/.virtualenvs/chaos/bin/uwsgi",
Expand All @@ -100,10 +106,14 @@ def main():
"--check-static", "/root/workspace/Chaos/server/",
"--daemonize", "/root/workspace/Chaos/log/uwsgi.log"])

# Start Reddit bot
subprocess.Popen([sys.executable, "redditchaosbot.py"])

# Schedule all cron jobs to be run
cron.schedule_jobs(api, api_twitter)

log.info("Setting description to {desc}".format(desc=settings.REPO_DESCRIPTION))
log.info("Setting description to {desc}".format(
desc=settings.REPO_DESCRIPTION))
github_api.repos.set_desc(api, settings.URN, settings.REPO_DESCRIPTION)

log.info("Ensure creation of issue/PR labels")
Expand Down Expand Up @@ -138,7 +148,8 @@ def check_for_prev_crash(api, log):
# Currently, I'm just reading the last 200 lines... which I think
# ought to be enough, but if anyone has a better way to do this,
# please do it.
dump = subprocess.check_output(["tail", "-n", "200", settings.CHAOSBOT_STDERR_LOG])
dump = subprocess.check_output(
["tail", "-n", "200", settings.CHAOSBOT_STDERR_LOG])

# Create a github issue for the problem
title = "Help! I crashed! --CB"
Expand Down
23 changes: 23 additions & 0 deletions encrypt_config_file.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import sys
from cryptography.fernet import Fernet
from symmetric_keys import KeyManager

# You gotta tell us what to call this key
try:
key_name = sys.argv[1]
file_to_encrypt = sys.argv[2]
except KeyError:
print("Usage:", sys.argv[0], "keyname file_to_encrypt")
sys.exit(1)

# Make the symmetric key
key = Fernet.generate_key()

with KeyManager() as key_manager:
key_manager.add_key(key_name, key)

with open(file_to_encrypt, 'r+b') as config_file:
contents = config_file.read()
config_file.seek(0)
config_file.write(Fernet(key).encrypt(contents))
config_file.flush()
16 changes: 13 additions & 3 deletions encryption.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding, rsa

__all__ = ["encrypt", "decrypt"]

def create_decryptor(private_location, public_location):

def create_crypto_functions(private_location, public_location):
try:
with open(private_location, "rb") as key_file:
private_key = serialization.load_pem_private_key(
Expand Down Expand Up @@ -32,6 +34,14 @@ def create_decryptor(private_location, public_location):
)
public_file.write(pem)

def encrypt(plaintext):
return public_key.encrypt(plaintext,
padding.OAEP(
padding.MGF1(hashes.SHA1()),
hashes.SHA1(),
None
))

def decrypt(ciphertext):
return private_key.decrypt(
ciphertext,
Expand All @@ -42,7 +52,7 @@ def decrypt(ciphertext):
)
)

return decrypt
return encrypt, decrypt


decrypt = create_decryptor("privkey", "server/pubkey.txt")
encrypt, decrypt = create_crypto_functions("/etc/privkey", "server/pubkey.txt")
1 change: 1 addition & 0 deletions redditbot.config
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
gAAAAABZMhR4ejTZdcGI3-W4ANpPk9EfyiQP71Nafrj0YLyFML1yctx_tzEeJcXT9-9Tp51z9hD0aDos7ivXFOlth7sRlDojY6-v-2T3_-wv3_jfTpQNV4zxfkzrKFVDRX4kVcNhu6YkkORAcJZu_5aNgCNBTGh2tQjG4WVIfvvCx-2RyP5TKOFC5CPSMlVvTqeaau4GbK2Qk7ZJq6oMCI00z5jSeRQW2Uh87zKnvu_HZicxDIjTIJfNP0bTxB08eLcjP5hr4cNqDSkc0-JOIvaPB721intWDuoLVLCragfz56utKslMD3gIDeCsevAUov6tN8b0qYYjJctk0PpWn-TXuxDFXZ8-mRswJbfbQZVWJN4bH84NX2oaAUgan6qclkAMu12VovlMX5dJd_LdmcA2Q_sWIsJlJIdrAtVA8Lrvow8RJ13BQi3tjeV_3g6VD5G0PhJ0HrGPCURTczuXYIpkyKfKXBpU0PnePMjKIf4izUeeBprQQk-ozvGRMBMAlldUtZd91wGY1An6ULrY5fPBWSankJTZdUJG9cM309LSCYZaUgLZWMw=
92 changes: 72 additions & 20 deletions redditchaosbot.py
Original file line number Diff line number Diff line change
@@ -1,35 +1,87 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import logging
import json
import sys
import time
from atomicwrites import atomic_write
import praw
from cryptography.fernet import Fernet
from symmetric_keys import KeyManager
from server.server import set_proc_name

set_proc_name("chaos_redditbot")
log = logging.getLogger("chaos_redditbot")
handler = logging.StreamHandler()
formatter = logging.Formatter(
'%(asctime)s %(name)-12s %(levelname)-8s %(message)s')
handler.setFormatter(formatter)
log.addHandler(handler)
log.setLevel(logging.INFO)

'''Authenticated instance of Reddit'''
# --------------------------------------------------------------------
# https://praw.readthedocs.io/en/latest/getting_started/installation.html
# https://www.reddit.com/prefs/apps
reddit = praw.Reddit(user_agent='Chaosbot social experiment',
client_id='KbKcHMguzNRKUw', client_secret="n-2lxQYOKtJinlJUWJr_Zv7g1rw",
username='redditchaosbot', password='cha0sb0t')

reddit.read_only = True
try:
with open("redditbot.config", 'rb') as config_file:
with KeyManager() as key_manager:
key = key_manager.get_key("redditchaosbot")
fernet = Fernet(key)
config = json.loads(fernet.decrypt(config_file.read()).decode('utf-8'))
except FileNotFoundError as ex:
log.critical(ex)
sys.exit(1)

submission = reddit.submission(id='6criij')
subreddit = reddit.subreddit('programming')
# --------------------------------------------------------------------
reddit = praw.Reddit(
user_agent='Chaosbot social experiment',
client_id=config['client_id'],
client_secret=config['client_secret'],
username='chaosthebotreborn',
password=config['password'])

subreddit = reddit.subreddit('chaosthebot')

'''WHAT ARE PEOPLE SAYING ABOUT CHAOSBOT?'''
# --------------------------------------------------------------------
# in reddit thread: https://goo.gl/5ETNmF
submission.comment_sort = 'new'
top_level_comments = list(submission.comments)
newcom = top_level_comments[1]
print(newcom.body)

already_replied = set()
replied_file = "redditbot_replied.json"
try:
with open(replied_file) as f:
already_replied.update(json.load(f)['already_replied'])
except:
pass

'''FIND COMMENTS ABOUT CHAOS'''
# --------------------------------------------------------------------
# TBD
# for comment in reddit.subreddit('programming').comments(limit=5):
# import pdb; pdb.set_trace()

def save_already_replied(replied_list):
with atomic_write(replied_file, overwrite=True) as f:
json.dump({'already_replied': list(replied_list)}, f)


def process_comment(comment):
log.info("Processing comment id %s", comment.id)
if comment.id not in already_replied and "hey chaosbot" in comment.body.lower():
log.info("Attempting to reply to comment %s", comment.id)
comment.reply("Hey {}!".format(comment.author.name))
already_replied.add(comment.id)
# Save every chance we get
save_already_replied(already_replied)


def main():
try:
for comment in subreddit.stream.comments():
try:
process_comment(comment)
except praw.exceptions.APIException:
# Save every chance we get
save_already_replied(already_replied)
sleep_time = reddit.auth.limits['reset_timestamp'] - time.time()
log.info("Sleeping for %d seconds", sleep_time)
time.sleep(sleep_time)
finally:
save_already_replied(already_replied)


if __name__ == "__main__":
main()
15 changes: 13 additions & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,41 @@ ansible==2.3.0.0
appdirs==1.4.3
arrow==0.10.0
asn1crypto==0.22.0
atomicwrites==1.1.5
certifi==2017.4.17
cffi==1.10.0
chardet==3.0.3
cryptography==1.9
emoji==0.4.5
enum34==1.1.6
flake8==3.3.0
idna==2.5
ipaddress==1.0.18
Jinja2==2.9.6
MarkupSafe==1.0
mccabe==0.6.1
packaging==16.8
paramiko==2.1.2
praw==4.5.1
prawcore==0.10.1
pyasn1==0.2.3
pycodestyle==2.3.1
pycparser==2.17
pycrypto==2.6.1
pyflakes==1.5.0
PyMySQL==0.7.11
pyparsing==2.2.0
python-dateutil==2.6.0
PyYAML==3.12
requests==2.17.3
schedule==0.4.2
sh==1.12.13
six==1.10.0
flake8==3.3.0
unidiff==0.5.4
update-checker==0.16
urllib3==1.21.1
python-twitter==3.3
pymysql
hug==2.3.0
uWSGI==2.0.15
peewee
peewee
1 change: 1 addition & 0 deletions symmetric_keys.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"redditchaosbot": "XZNtqEBHjid0OdZ+jlgV6fUnuVBH9OjAVpM1KCJ8/WXEniFLilENgji2KfS+ZRVLevojcimhR2vMYPhCTi8ogZw4aqenvb1s+oDmSJ1F+wk8P8B3wdULoamg5ccM+4xANQyptsV5WJSqR1hfqoAMYIsXSR4O/gbArl9ga0dkQJRGs7aQvMqz22bSw+LI3fnuhyqi5fT0KZmCrotMptig/ocQcBuZoVraWAfrTyLzCKEp0aAHmRncdOAr2pqRCwNpcCNV19ScofYEcakLav5vA7XOSVIx9hRWQbBNfJPCSCZBy69VqGU8N+Jf9IILyCh3Q1HgiN7Ho1kY5roA344gAg=="}
59 changes: 59 additions & 0 deletions symmetric_keys.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import json
import base64
try:
from encryption import decrypt, encrypt
except:
# We may need to encrypt when not in a server environment
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives.serialization import load_pem_public_key

def encrypt(plaintext):
with open("server/pubkey.txt", "rb") as key_file:
public_key = load_pem_public_key(key_file.read(), default_backend())
return public_key.encrypt(plaintext,
padding.OAEP(
padding.MGF1(hashes.SHA1()),
hashes.SHA1(),
None
))

def decrypt(ciphertext):
raise NotImplementedError("This only works in a running server env")


# It's a context manager y'all
class KeyManager():

def __init__(self, keys_file_name="symmetric_keys.json"):
self.keys_file_name = keys_file_name
self.existing_keys = {}

# Try to load existing keys
try:
with open(keys_file_name) as keys_file:
self.existing_keys = json.load(keys_file)
except FileNotFoundError:
# Welp, aren't any
pass

def __enter__(self):
return self

def get_key(self, key_name):
# base64 decode the string object in the dict and decrypt and return
# base64.b64decode() returns bytes for us
return decrypt(base64.b64decode(self.existing_keys[key_name]))

def add_key(self, key_name, key):
# encrypt the base64 encoded ASCII bytes then b64encode that
# and decode it to a Unicode string (JSON doesn't work with bytes)
# and store it
self.existing_keys[key_name] = base64.b64encode(
encrypt(key)).decode('ascii')

def __exit__(self, exc_type, exc_value, traceback):
# Save the keys when we are finished
with open(self.keys_file_name, 'w') as keys_file:
json.dump(self.existing_keys, keys_file)