def reply() -> werkzeug.Response: """Attempt to send a Reply from a Journalist to a Source. Empty messages are rejected, and an informative error message is flashed on the client. In the case of unexpected errors involving database transactions (potentially caused by racing request threads that modify the same the database object) logging is done in such a way so as not to write potentially sensitive information to disk, and a generic error message is flashed on the client. Returns: flask.Response: The user is redirected to the same Source collection view, regardless if the Reply is created successfully. """ form = ReplyForm() if not form.validate_on_submit(): for error in form.message.errors: flash(error, "error") return redirect(url_for('col.col', filesystem_id=g.filesystem_id)) g.source.interaction_count += 1 filename = "{0}-{1}-reply.gpg".format(g.source.interaction_count, g.source.journalist_filename) current_app.crypto_util.encrypt( form.message.data, [ current_app.crypto_util.get_fingerprint(g.filesystem_id), config.JOURNALIST_KEY ], output=current_app.storage.path(g.filesystem_id, filename), ) try: reply = Reply(g.user, g.source, filename) db.session.add(reply) db.session.flush() seen_reply = SeenReply(reply_id=reply.id, journalist_id=g.user.id) db.session.add(seen_reply) db.session.commit() store.async_add_checksum_for_file(reply) except Exception as exc: flash( gettext("An unexpected error occurred! Please " "inform your admin."), "error") # We take a cautious approach to logging here because we're dealing # with responses to sources. It's possible the exception message # could contain information we don't want to write to disk. current_app.logger.error( "Reply from '{}' (ID {}) failed: {}!".format( g.user.username, g.user.id, exc.__class__)) else: flash( Markup("<b>{}</b> {}".format( # Translators: Precedes a message confirming the success of an operation. escape(gettext("Success!")), escape(gettext("Your reply has been stored.")))), 'success') finally: return redirect(url_for('col.col', filesystem_id=g.filesystem_id))
def test_async_add_checksum_for_file(config, app_storage, db_model): """ Check that when we execute the `add_checksum_for_file` function, the database object is correctly updated with the actual hash of the file. We have to create our own app in order to have more control over the SQLAlchemy sessions. The fixture pushes a single app context that forces us to work within a single transaction. """ app = create_app(config) with app.app_context(): db.create_all() source_user = create_source_user( db_session=db.session, source_passphrase=PassphraseGenerator.get_default(). generate_passphrase(), source_app_storage=app_storage, ) source = source_user.get_db_record() target_file_path = app_storage.path(source.filesystem_id, "1-foo-msg.gpg") test_message = b"hash me!" expected_hash = "f1df4a6d8659471333f7f6470d593e0911b4d487856d88c83d2d187afa195927" with open(target_file_path, "wb") as f: f.write(test_message) if db_model == Submission: db_obj = Submission(source, target_file_path, app_storage) else: journalist, _ = utils.db_helper.init_journalist() db_obj = Reply(journalist, source, target_file_path, app_storage) db.session.add(db_obj) db.session.commit() db_obj_id = db_obj.id job = async_add_checksum_for_file(db_obj, app_storage) utils.asynchronous.wait_for_redis_worker(job, timeout=5) with app.app_context(): # requery to get a new object db_obj = db_model.query.filter_by(id=db_obj_id).one() assert db_obj.checksum == "sha256:" + expected_hash
def test_async_add_checksum_for_file(config, db_model): ''' Check that when we execute the `add_checksum_for_file` function, the database object is correctly updated with the actual hash of the file. We have to create our own app in order to have more control over the SQLAlchemy sessions. The fixture pushes a single app context that forces us to work within a single transaction. ''' app = create_app(config) with app.app_context(): db.create_all() source, _ = utils.db_helper.init_source_without_keypair() target_file_path = app.storage.path(source.filesystem_id, '1-foo-msg.gpg') test_message = b'hash me!' expected_hash = 'f1df4a6d8659471333f7f6470d593e0911b4d487856d88c83d2d187afa195927' with open(target_file_path, 'wb') as f: f.write(test_message) if db_model == Submission: db_obj = Submission(source, target_file_path) else: journalist, _ = utils.db_helper.init_journalist() db_obj = Reply(journalist, source, target_file_path) db.session.add(db_obj) db.session.commit() db_obj_id = db_obj.id job = async_add_checksum_for_file(db_obj) utils.asynchronous.wait_for_redis_worker(job, timeout=5) with app.app_context(): # requery to get a new object db_obj = db_model.query.filter_by(id=db_obj_id).one() assert db_obj.checksum == 'sha256:' + expected_hash
def submit() -> werkzeug.Response: allow_document_uploads = current_app.instance_config.allow_document_uploads form = SubmissionForm() if not form.validate(): for field, errors in form.errors.items(): for error in errors: flash(error, "error") return redirect(url_for('main.lookup')) msg = request.form['msg'] fh = None if allow_document_uploads and 'fh' in request.files: fh = request.files['fh'] # Don't submit anything if it was an "empty" submission. #878 if not (msg or fh): if allow_document_uploads: flash( gettext( "You must enter a message or choose a file to submit." ), "error") else: flash(gettext("You must enter a message."), "error") return redirect(url_for('main.lookup')) fnames = [] journalist_filename = g.source.journalist_filename first_submission = g.source.interaction_count == 0 if msg: g.source.interaction_count += 1 fnames.append( current_app.storage.save_message_submission( g.filesystem_id, g.source.interaction_count, journalist_filename, msg)) if fh: g.source.interaction_count += 1 fnames.append( current_app.storage.save_file_submission( g.filesystem_id, g.source.interaction_count, journalist_filename, fh.filename, fh.stream)) if first_submission: flash_message = render_template( 'first_submission_flashed_message.html') flash(Markup(flash_message), "success") else: if msg and not fh: html_contents = gettext('Thanks! We received your message.') elif fh and not msg: html_contents = gettext('Thanks! We received your document.') else: html_contents = gettext('Thanks! We received your message and ' 'document.') flash_message = render_template( 'next_submission_flashed_message.html', html_contents=html_contents) flash(Markup(flash_message), "success") new_submissions = [] for fname in fnames: submission = Submission(g.source, fname) db.session.add(submission) new_submissions.append(submission) if g.source.pending: g.source.pending = False # Generate a keypair now, if there's enough entropy (issue #303) # (gpg reads 300 bytes from /dev/random) entropy_avail = get_entropy_estimate() if entropy_avail >= 2400: db_uri = current_app.config['SQLALCHEMY_DATABASE_URI'] async_genkey(current_app.crypto_util, db_uri, g.filesystem_id, g.codename) current_app.logger.info( "generating key, entropy: {}".format(entropy_avail)) else: current_app.logger.warning( "skipping key generation. entropy: {}".format( entropy_avail)) g.source.last_updated = datetime.utcnow() db.session.commit() for sub in new_submissions: store.async_add_checksum_for_file(sub) normalize_timestamps(g.filesystem_id) return redirect(url_for('main.lookup'))
def submit(logged_in_source: SourceUser) -> werkzeug.Response: allow_document_uploads = InstanceConfig.get_default().allow_document_uploads form = SubmissionForm() if not form.validate(): for field, errors in form.errors.items(): for error in errors: flash_msg("error", None, error) return redirect(url_for("main.lookup")) msg = request.form["msg"] fh = None if allow_document_uploads and "fh" in request.files: fh = request.files["fh"] # Don't submit anything if it was an "empty" submission. #878 if not (msg or fh): if allow_document_uploads: html_contents = gettext("You must enter a message or choose a file to submit.") else: html_contents = gettext("You must enter a message.") flash_msg("error", None, html_contents) return redirect(url_for("main.lookup")) fnames = [] logged_in_source_in_db = logged_in_source.get_db_record() first_submission = logged_in_source_in_db.interaction_count == 0 if first_submission: min_len = InstanceConfig.get_default().initial_message_min_len if (min_len > 0) and (msg and not fh) and (len(msg) < min_len): flash_msg( "error", None, gettext("Your first message must be at least {} characters long.").format( min_len ), ) return redirect(url_for("main.lookup")) # if the new_user_codename key is not present in the session, this is # not a first session new_codename = session.get("new_user_codename", None) codenames_rejected = InstanceConfig.get_default().reject_message_with_codename if new_codename is not None: if codenames_rejected and codename_detected(msg, new_codename): flash_msg( "error", None, gettext("Please do not submit your codename!"), gettext( "Keep your codename secret, and use it to log in later to " "check for replies." ), ) return redirect(url_for("main.lookup")) if not os.path.exists(Storage.get_default().path(logged_in_source.filesystem_id)): current_app.logger.debug( "Store directory not found for source '{}', creating one.".format( logged_in_source_in_db.journalist_designation ) ) os.mkdir(Storage.get_default().path(logged_in_source.filesystem_id)) if msg: logged_in_source_in_db.interaction_count += 1 fnames.append( Storage.get_default().save_message_submission( logged_in_source_in_db.filesystem_id, logged_in_source_in_db.interaction_count, logged_in_source_in_db.journalist_filename, msg, ) ) if fh: logged_in_source_in_db.interaction_count += 1 fnames.append( Storage.get_default().save_file_submission( logged_in_source_in_db.filesystem_id, logged_in_source_in_db.interaction_count, logged_in_source_in_db.journalist_filename, fh.filename, fh.stream, ) ) if first_submission or msg or fh: if first_submission: html_contents = gettext( "Thank you for sending this information to us. Please " "check back later for replies." ) elif msg and not fh: html_contents = gettext("Thanks! We received your message.") elif fh and not msg: html_contents = gettext("Thanks! We received your document.") else: html_contents = gettext("Thanks! We received your message and document.") flash_msg("success", gettext("Success!"), html_contents) new_submissions = [] for fname in fnames: submission = Submission(logged_in_source_in_db, fname, Storage.get_default()) db.session.add(submission) new_submissions.append(submission) logged_in_source_in_db.pending = False logged_in_source_in_db.last_updated = datetime.now(timezone.utc) db.session.commit() for sub in new_submissions: store.async_add_checksum_for_file(sub, Storage.get_default()) normalize_timestamps(logged_in_source) return redirect(url_for("main.lookup"))