# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import functools
import time

import msgpack
import msgpack.exceptions
import redis

from pyramid import viewderivers
from pyramid.interfaces import ISession, ISessionFactory
from zope.interface import implementer

from warehouse.cache.http import add_vary
from warehouse.utils import crypto


def _invalid_method(method):
    @functools.wraps(method)
    def wrapped(self, *args, **kwargs):
        self._error_message()
    return wrapped


@implementer(ISession)
class InvalidSession(dict):

    __contains__ = _invalid_method(dict.__contains__)
    __delitem__ = _invalid_method(dict.__delitem__)
    __getitem__ = _invalid_method(dict.__getitem__)
    __iter__ = _invalid_method(dict.__iter__)
    __len__ = _invalid_method(dict.__len__)
    __setitem__ = _invalid_method(dict.__setitem__)
    clear = _invalid_method(dict.clear)
    copy = _invalid_method(dict.copy)
    fromkeys = _invalid_method(dict.fromkeys)
    get = _invalid_method(dict.get)
    items = _invalid_method(dict.items)
    keys = _invalid_method(dict.keys)
    pop = _invalid_method(dict.pop)
    popitem = _invalid_method(dict.popitem)
    setdefault = _invalid_method(dict.setdefault)
    update = _invalid_method(dict.update)
    values = _invalid_method(dict.values)

    def _error_message(self):
        raise RuntimeError(
            "Cannot use request.session in a view without uses_session=True."
        )

    def __getattr__(self, name):
        self._error_message()

    @property
    def created(self):
        self._error_message()


def _changed_method(method):
    @functools.wraps(method)
    def wrapped(self, *args, **kwargs):
        self.changed()
        return method(self, *args, **kwargs)
    return wrapped


@implementer(ISession)
class Session(dict):

    _csrf_token_key = "_csrf_token"
    _flash_key = "_flash_messages"

    # A number of our methods need to be decorated so that they also call
    # self.changed()
    __delitem__ = _changed_method(dict.__delitem__)
    __setitem__ = _changed_method(dict.__setitem__)
    clear = _changed_method(dict.clear)
    pop = _changed_method(dict.pop)
    popitem = _changed_method(dict.popitem)
    setdefault = _changed_method(dict.setdefault)
    update = _changed_method(dict.update)

    def __init__(self, data=None, session_id=None, new=True):
        # Brand new sessions don't have any data, so we'll just create an empty
        # dictionary for them.
        if data is None:
            data = {}

        # Initialize our actual dictionary here.
        super().__init__(data)

        # We need to track the state of our Session.
        self._sid = session_id
        self._changed = False
        self.new = new
        self.created = int(time.time())

        # We'll track all of the IDs that have been invalidated here
        self.invalidated = set()

    @property
    def sid(self):
        if self._sid is None:
            self._sid = crypto.random_token()
        return self._sid

    def changed(self):
        self._changed = True

    def invalidate(self):
        self.clear()
        self.new = True
        self.created = int(time.time())
        self._changed = False

        # If the current session id isn't None we'll want to record it as one
        # of the ones that have been invalidated.
        if self._sid is not None:
            self.invalidated.add(self._sid)
            self._sid = None

    def should_save(self):
        return self._changed

    # Flash Messages Methods
    def _get_flash_queue_key(self, queue):
        return ".".join(filter(None, [self._flash_key, queue]))

    def flash(self, msg, queue="", allow_duplicate=True):
        queue_key = self._get_flash_queue_key(queue)

        # If we're not allowing duplicates check if this message is already
        # in the queue, and if it is just return immediately.
        if not allow_duplicate and msg in self[queue_key]:
            return

        self.setdefault(queue_key, []).append(msg)

    def peek_flash(self, queue=""):
        return self.get(self._get_flash_queue_key(queue), [])

    def pop_flash(self, queue=""):
        queue_key = self._get_flash_queue_key(queue)
        messages = self.get(queue_key, [])
        self.pop(queue_key, None)
        return messages

    # CSRF Methods
    def new_csrf_token(self):
        self[self._csrf_token_key] = crypto.random_token()
        return self[self._csrf_token_key]

    def get_csrf_token(self):
        token = self.get(self._csrf_token_key)
        if token is None:
            token = self.new_csrf_token()
        return token


@implementer(ISessionFactory)
class SessionFactory:

    cookie_name = "session_id"
    max_age = 12 * 60 * 60  # 12 hours

    def __init__(self, secret, url):
        self.redis = redis.StrictRedis.from_url(url)
        self.signer = crypto.TimestampSigner(secret, salt="session")

    def __call__(self, request):
        return self._process_request(request)

    def _redis_key(self, session_id):
        return "warehouse/session/data/{}".format(session_id)

    def _process_request(self, request):
        # Register a callback with the request so we can save the session once
        # it's finished.
        request.add_response_callback(self._process_response)

        # Load our session ID from the request.
        session_id = request.cookies.get(self.cookie_name)

        # If we do not have a session ID then we'll just use a new empty
        # session.
        if session_id is None:
            return Session()

        # Check to make sure we have a valid session id
        try:
            session_id = self.signer.unsign(session_id, max_age=self.max_age)
            session_id = session_id.decode("utf8")
        except crypto.BadSignature:
            return Session()

        # Fetch the serialized data from redis
        bdata = self.redis.get(self._redis_key(session_id))

        # If the session didn't exist in redis, we'll give the user a new
        # session.
        if bdata is None:
            return Session()

        # De-serialize our session data
        try:
            data = msgpack.unpackb(bdata, encoding="utf8", use_list=True)
        except (msgpack.exceptions.UnpackException,
                msgpack.exceptions.ExtraData):
            # If the session data was invalid we'll give the user a new session
            return Session()

        # If we were able to load existing session data, load it into a
        # Session class
        session = Session(data, session_id, False)

        return session

    def _process_response(self, request, response):
        # If the request has an InvalidSession, then the view can't have
        # accessed the session, and we can just skip all of this anyways.
        if isinstance(request.session, InvalidSession):
            return

        # Check to see if the session has been marked to be deleted, if it has
        # benn then we'll delete it, and tell our response to delete the
        # session cookie as well.
        if request.session.invalidated:
            for session_id in request.session.invalidated:
                self.redis.delete(self._redis_key(session_id))

            if not request.session.should_save():
                response.delete_cookie(self.cookie_name)

        # Check to see if the session has been marked to be saved, generally
        # this means that the session data has been modified and thus we need
        # to store the new data.
        if request.session.should_save():
            # Save our session in Redis
            self.redis.setex(
                self._redis_key(request.session.sid),
                self.max_age,
                msgpack.packb(
                    request.session,
                    encoding="utf8",
                    use_bin_type=True,
                ),
            )

            # Send our session cookie to the client
            response.set_cookie(
                self.cookie_name,
                self.signer.sign(request.session.sid.encode("utf8")),
                max_age=self.max_age,
                httponly=True,
                secure=request.scheme == "https",
            )


def session_view(view, info):
    if info.options.get("uses_session"):
        # If we're using the session, then we'll just return the original view
        # with a small wrapper around it to ensure that it has a Vary: Cookie
        # header.
        return add_vary("Cookie")(view)
    elif info.exception_only:
        return view
    else:
        # If we're not using the session on this view, then we'll wrap the view
        # with a wrapper that just ensures that the session cannot be used.
        @functools.wraps(view)
        def wrapped(context, request):
            # This whole method is a little bit of an odd duck, we want to make
            # sure that we don't actually *access* request.session, because
            # doing so triggers the machinery to create a new session. So
            # instead we will dig into the request object __dict__ to
            # effectively do the same thing, jsut without triggering an access
            # on request.session.

            # Save the original session so that we can restore it once the
            # inner views have been called.
            nothing = object()
            original_session = request.__dict__.get("session", nothing)

            # This particular view hasn't been set to allow access to the
            # session, so we'll just assign an InvalidSession to
            # request.session
            request.__dict__["session"] = InvalidSession()

            try:
                # Invoke the real view
                return view(context, request)
            finally:
                # Restore the original session so that things like
                # pyramid_debugtoolbar can access it.
                if original_session is nothing:
                    del request.__dict__["session"]
                else:
                    request.__dict__["session"] = original_session

        return wrapped


session_view.options = {"uses_session"}


def includeme(config):
    config.set_session_factory(
        SessionFactory(
            config.registry.settings["sessions.secret"],
            config.registry.settings["sessions.url"],
        ),
    )

    config.add_view_deriver(
        session_view,
        over="csrf_view",
        under=viewderivers.INGRESS,
    )

warehouse/sessions.py
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import functools
import time

import msgpack
import msgpack.exceptions
import redis

from pyramid import viewderivers
from pyramid.interfaces import ISession, ISessionFactory
from zope.interface import implementer

from warehouse.cache.http import add_vary
from warehouse.utils import crypto


def _invalid_method(method):
    @functools.wraps(method)
    def wrapped(self, *args, **kwargs):
        self._error_message()
    return wrapped


@implementer(ISession)
class InvalidSession(dict):

    __contains__ = _invalid_method(dict.__contains__)
    __delitem__ = _invalid_method(dict.__delitem__)
    __getitem__ = _invalid_method(dict.__getitem__)
    __iter__ = _invalid_method(dict.__iter__)
    __len__ = _invalid_method(dict.__len__)
    __setitem__ = _invalid_method(dict.__setitem__)
    clear = _invalid_method(dict.clear)
    copy = _invalid_method(dict.copy)
    fromkeys = _invalid_method(dict.fromkeys)
    get = _invalid_method(dict.get)
    items = _invalid_method(dict.items)
    keys = _invalid_method(dict.keys)
    pop = _invalid_method(dict.pop)
    popitem = _invalid_method(dict.popitem)
    setdefault = _invalid_method(dict.setdefault)
    update = _invalid_method(dict.update)
    values = _invalid_method(dict.values)

    def _error_message(self):
        raise RuntimeError(
            "Cannot use request.session in a view without uses_session=True."
        )

    def __getattr__(self, name):
        self._error_message()

    @property
    def created(self):
        self._error_message()


def _changed_method(method):
    @functools.wraps(method)
    def wrapped(self, *args, **kwargs):
        self.changed()
        return method(self, *args, **kwargs)
    return wrapped


@implementer(ISession)
class Session(dict):

    _csrf_token_key = "_csrf_token"
    _flash_key = "_flash_messages"

    # A number of our methods need to be decorated so that they also call
    # self.changed()
    __delitem__ = _changed_method(dict.__delitem__)
    __setitem__ = _changed_method(dict.__setitem__)
    clear = _changed_method(dict.clear)
    pop = _changed_method(dict.pop)
    popitem = _changed_method(dict.popitem)
    setdefault = _changed_method(dict.setdefault)
    update = _changed_method(dict.update)

    def __init__(self, data=None, session_id=None, new=True):
        # Brand new sessions don't have any data, so we'll just create an empty
        # dictionary for them.
        if data is None:
            data = {}

        # Initialize our actual dictionary here.
        super().__init__(data)

        # We need to track the state of our Session.
        self._sid = session_id
        self._changed = False
        self.new = new
        self.created = int(time.time())

        # We'll track all of the IDs that have been invalidated here
        self.invalidated = set()

    @property
    def sid(self):
        if self._sid is None:
            self._sid = crypto.random_token()
        return self._sid

    def changed(self):
        self._changed = True

    def invalidate(self):
        self.clear()
        self.new = True
        self.created = int(time.time())
        self._changed = False

        # If the current session id isn't None we'll want to record it as one
        # of the ones that have been invalidated.
        if self._sid is not None:
            self.invalidated.add(self._sid)
            self._sid = None

    def should_save(self):
        return self._changed

    # Flash Messages Methods
    def _get_flash_queue_key(self, queue):
        return ".".join(filter(None, [self._flash_key, queue]))

    def flash(self, msg, queue="", allow_duplicate=True):
        queue_key = self._get_flash_queue_key(queue)

        # If we're not allowing duplicates check if this message is already
        # in the queue, and if it is just return immediately.
        if not allow_duplicate and msg in self[queue_key]:
            return

        self.setdefault(queue_key, []).append(msg)

    def peek_flash(self, queue=""):
        return self.get(self._get_flash_queue_key(queue), [])

    def pop_flash(self, queue=""):
        queue_key = self._get_flash_queue_key(queue)
        messages = self.get(queue_key, [])
        self.pop(queue_key, None)
        return messages

    # CSRF Methods
    def new_csrf_token(self):
        self[self._csrf_token_key] = crypto.random_token()
        return self[self._csrf_token_key]

    def get_csrf_token(self):
        token = self.get(self._csrf_token_key)
        if token is None:
            token = self.new_csrf_token()
        return token


@implementer(ISessionFactory)
class SessionFactory:

    cookie_name = "session_id"
    max_age = 12 * 60 * 60  # 12 hours

    def __init__(self, secret, url):
        self.redis = redis.StrictRedis.from_url(url)
        self.signer = crypto.TimestampSigner(secret, salt="session")

    def __call__(self, request):
        return self._process_request(request)

    def _redis_key(self, session_id):
        return "warehouse/session/data/{}".format(session_id)

    def _process_request(self, request):
        # Register a callback with the request so we can save the session once
        # it's finished.
        request.add_response_callback(self._process_response)

        # Load our session ID from the request.
        session_id = request.cookies.get(self.cookie_name)

        # If we do not have a session ID then we'll just use a new empty
        # session.
        if session_id is None:
            return Session()

        # Check to make sure we have a valid session id
        try:
            session_id = self.signer.unsign(session_id, max_age=self.max_age)
            session_id = session_id.decode("utf8")
        except crypto.BadSignature:
            return Session()

        # Fetch the serialized data from redis
        bdata = self.redis.get(self._redis_key(session_id))

        # If the session didn't exist in redis, we'll give the user a new
        # session.
        if bdata is None:
            return Session()

        # De-serialize our session data
        try:
            data = msgpack.unpackb(bdata, encoding="utf8", use_list=True)
        except (msgpack.exceptions.UnpackException,
                msgpack.exceptions.ExtraData):
            # If the session data was invalid we'll give the user a new session
            return Session()

        # If we were able to load existing session data, load it into a
        # Session class
        session = Session(data, session_id, False)

        return session

    def _process_response(self, request, response):
        # If the request has an InvalidSession, then the view can't have
        # accessed the session, and we can just skip all of this anyways.
        if isinstance(request.session, InvalidSession):
            return

        # Check to see if the session has been marked to be deleted, if it has
        # benn then we'll delete it, and tell our response to delete the
        # session cookie as well.
        if request.session.invalidated:
            for session_id in request.session.invalidated:
                self.redis.delete(self._redis_key(session_id))

            if not request.session.should_save():
                response.delete_cookie(self.cookie_name)

        # Check to see if the session has been marked to be saved, generally
        # this means that the session data has been modified and thus we need
        # to store the new data.
        if request.session.should_save():
            # Save our session in Redis
            self.redis.setex(
                self._redis_key(request.session.sid),
                self.max_age,
                msgpack.packb(
                    request.session,
                    encoding="utf8",
                    use_bin_type=True,
                ),
            )

            # Send our session cookie to the client
            response.set_cookie(
                self.cookie_name,
                self.signer.sign(request.session.sid.encode("utf8")),
                max_age=self.max_age,
                httponly=True,
                secure=request.scheme == "https",
                samesite=b"lax"
            )


def session_view(view, info):
    if info.options.get("uses_session"):
        # If we're using the session, then we'll just return the original view
        # with a small wrapper around it to ensure that it has a Vary: Cookie
        # header.
        return add_vary("Cookie")(view)
    elif info.exception_only:
        return view
    else:
        # If we're not using the session on this view, then we'll wrap the view
        # with a wrapper that just ensures that the session cannot be used.
        @functools.wraps(view)
        def wrapped(context, request):
            # This whole method is a little bit of an odd duck, we want to make
            # sure that we don't actually *access* request.session, because
            # doing so triggers the machinery to create a new session. So
            # instead we will dig into the request object __dict__ to
            # effectively do the same thing, jsut without triggering an access
            # on request.session.

            # Save the original session so that we can restore it once the
            # inner views have been called.
            nothing = object()
            original_session = request.__dict__.get("session", nothing)

            # This particular view hasn't been set to allow access to the
            # session, so we'll just assign an InvalidSession to
            # request.session
            request.__dict__["session"] = InvalidSession()

            try:
                # Invoke the real view
                return view(context, request)
            finally:
                # Restore the original session so that things like
                # pyramid_debugtoolbar can access it.
                if original_session is nothing:
                    del request.__dict__["session"]
                else:
                    request.__dict__["session"] = original_session

        return wrapped


session_view.options = {"uses_session"}


def includeme(config):
    config.set_session_factory(
        SessionFactory(
            config.registry.settings["sessions.secret"],
            config.registry.settings["sessions.url"],
        ),
    )

    config.add_view_deriver(
        session_view,
        over="csrf_view",
        under=viewderivers.INGRESS,
    )

warehouse/sessions.py
diff --git a/requirements/main.txt b/requirements/main.txt
index a76118b5f594..18025daa798d 100644
--- a/requirements/main.txt
+++ b/requirements/main.txt
@@ -457,9 +457,9 @@ vine==1.1.4 \
 webencodings==0.5.1 \
     --hash=sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78 \
     --hash=sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923
-WebOb==1.7.4 \
-    --hash=sha256:63f4220492476c5c716b615baed7bf3d27040b3105014375787160dee0943115 \
-    --hash=sha256:8d10af182fda4b92193113ee1edeb687ab9dc44336b37d6804e413f0240d40d9
+WebOb==1.8.0 \
+    --hash=sha256:ae809c05b667c3457a2937cdb4a7c7f07e90f26c651a340d37fdd1d5cf1fed27 \
+    --hash=sha256:6fca7aa39bd2f6d2ff71f15a22223ff256c91f60b1ab52dac0ab38dc6ea9142f
 whitenoise==3.3.1 \
     --hash=sha256:15f43b2e701821b95c9016cf469d29e2a546cb1c7dead584ba82c36f843995cf \
     --hash=sha256:9d81515f2b5b27051910996e1e860b1332e354d9e7bcf30c98f21dcb6713e0dd
diff --git a/requirements/tests.txt b/requirements/tests.txt
index e701d7ddbb66..f8094f4a357c 100644
--- a/requirements/tests.txt
+++ b/requirements/tests.txt
@@ -224,9 +224,9 @@ urllib3==1.22 \
 waitress==1.1.0 \
     --hash=sha256:40b0f297a7f3af61fbfbdc67e59090c70dc150a1601c39ecc9f5f1d283fb931b \
     --hash=sha256:d33cd3d62426c0f1b3cd84ee3d65779c7003aae3fc060dee60524d10a57f05a9
-WebOb==1.7.4 \
-    --hash=sha256:63f4220492476c5c716b615baed7bf3d27040b3105014375787160dee0943115 \
-    --hash=sha256:8d10af182fda4b92193113ee1edeb687ab9dc44336b37d6804e413f0240d40d9
+WebOb==1.8.0 \
+    --hash=sha256:ae809c05b667c3457a2937cdb4a7c7f07e90f26c651a340d37fdd1d5cf1fed27 \
+    --hash=sha256:6fca7aa39bd2f6d2ff71f15a22223ff256c91f60b1ab52dac0ab38dc6ea9142f
 WebTest==2.0.29 \
     --hash=sha256:9136514159a2e76a21751bf4ab5d3371e539c8ada8b950fcf68e307d9e584a07 \
     --hash=sha256:dbbccc15ac2465066c95dc3a7de0d30cde3791e886ccbd7e91d5d2a2580c922d
diff --git a/tests/unit/test_sessions.py b/tests/unit/test_sessions.py
index 0baee1c117b5..8bc57b3c27b0 100644
--- a/tests/unit/test_sessions.py
+++ b/tests/unit/test_sessions.py
@@ -497,7 +497,7 @@ def test_invalidated_deletes_save_non_secure(self, monkeypatch,
         )
         response = pretend.stub(
             set_cookie=pretend.call_recorder(
-                lambda cookie, data, max_age, httponly, secure: None
+                lambda cookie, data, max_age, httponly, secure, samesite: None
             )
         )
         session_factory._process_response(pyramid_request, response)
@@ -532,6 +532,7 @@ def test_invalidated_deletes_save_non_secure(self, monkeypatch,
                 max_age=12 * 60 * 60,
                 httponly=True,
                 secure=False,
+                samesite=b"lax",
             ),
         ]
 
diff --git a/tests/unit/utils/test_compression.py b/tests/unit/utils/test_compression.py
index 8fba42cc342e..ca30c68f575f 100644
--- a/tests/unit/utils/test_compression.py
+++ b/tests/unit/utils/test_compression.py
@@ -14,7 +14,7 @@
 import pytest
 
 from pyramid.response import Response
-from webob.acceptparse import Accept, NoAccept
+from webob.acceptparse import AcceptEncodingValidHeader, AcceptEncodingNoHeader
 from webob.response import gzip_app_iter
 
 from warehouse.utils.compression import _compressor as compressor
@@ -54,7 +54,7 @@ def test_bails_if_content_encoding(self):
         ],
     )
     def test_sets_vary(self, vary, expected):
-        request = pretend.stub(accept_encoding=NoAccept())
+        request = pretend.stub(accept_encoding=AcceptEncodingNoHeader())
         response = Response(body=b"foo")
         response.vary = vary
 
@@ -66,7 +66,9 @@ def test_compresses_non_streaming(self):
         decompressed_body = b"foofoofoofoofoofoofoofoofoofoofoofoofoofoo"
         compressed_body = b"".join(list(gzip_app_iter([decompressed_body])))
 
-        request = pretend.stub(accept_encoding=Accept("gzip"))
+        request = pretend.stub(
+            accept_encoding=AcceptEncodingValidHeader("gzip")
+        )
         response = Response(body=decompressed_body)
         response.md5_etag()
 
@@ -83,7 +85,9 @@ def test_compresses_streaming(self):
         decompressed_body = b"foofoofoofoofoofoofoofoofoofoofoofoofoofoo"
         compressed_body = b"".join(list(gzip_app_iter([decompressed_body])))
 
-        request = pretend.stub(accept_encoding=Accept("gzip"))
+        request = pretend.stub(
+            accept_encoding=AcceptEncodingValidHeader("gzip")
+        )
         response = Response(app_iter=iter([decompressed_body]))
 
         compressor(request, response)
@@ -96,7 +100,9 @@ def test_compresses_streaming_with_etag(self):
         decompressed_body = b"foofoofoofoofoofoofoofoofoofoofoofoofoofoo"
         compressed_body = b"".join(list(gzip_app_iter([decompressed_body])))
 
-        request = pretend.stub(accept_encoding=Accept("gzip"))
+        request = pretend.stub(
+            accept_encoding=AcceptEncodingValidHeader("gzip")
+        )
         response = Response(app_iter=iter([decompressed_body]))
         response.etag = "foo"
 
@@ -111,7 +117,9 @@ def test_buffers_small_streaming(self):
         decompressed_body = b"foofoofoofoofoofoofoofoofoofoofoofoofoofoo"
         compressed_body = b"".join(list(gzip_app_iter([decompressed_body])))
 
-        request = pretend.stub(accept_encoding=Accept("gzip"))
+        request = pretend.stub(
+            accept_encoding=AcceptEncodingValidHeader("gzip")
+        )
         response = Response(
             app_iter=iter([decompressed_body]),
             content_length=len(decompressed_body),
@@ -124,7 +132,9 @@ def test_buffers_small_streaming(self):
         assert response.body == compressed_body
 
     def test_doesnt_compress_too_small(self):
-        request = pretend.stub(accept_encoding=Accept("gzip"))
+        request = pretend.stub(
+            accept_encoding=AcceptEncodingValidHeader("gzip")
+        )
         response = Response(body=b"foo")
 
         compressor(request, response)
diff --git a/warehouse/sessions.py b/warehouse/sessions.py
index a52318f0eb7c..548f760c757a 100644
--- a/warehouse/sessions.py
+++ b/warehouse/sessions.py
@@ -263,6 +263,7 @@ def _process_response(self, request, response):
                 max_age=self.max_age,
                 httponly=True,
                 secure=request.scheme == "https",
+                samesite=b"lax"
             )
 
 

Set samesite=lax on session cookies
This is a strong defense-in-depth mechanism for protecting against CSRF. It's currently only respected by Chrome, but Firefox will add it as well.
Update webob to 1.8.0

This PR updates [WebOb](https://pypi.org/project/WebOb) from **1.7.4** to **1.8.0**.



<details>
  <summary>Changelog</summary>
  
  
   ### 1.8.0
   ```
   ------------------

Feature
~~~~~~~

- ``request.POST`` now supports any requests with the appropriate
  Content-Type. Allowing any HTTP method to access form encoded content,
  including DELETE, PUT, and others. See
  https://github.com/Pylons/webob/pull/352

Compatibility
~~~~~~~~~~~~~

- WebOb is no longer officially supported on Python 3.3 which was EOL&#39;ed on
  2017-09-29.

Backwards Incompatibilities
~~~~~~~~~~~~~~~~~~~~~~~~~~~

- Many changes have been made to the way WebOb does Accept handling, not just
  for the Accept header itself, but also for Accept-Charset, Accept-Encoding
  and Accept-Language. This was a `Google Summer of Code
  &lt;https://developers.google.com/open-source/gsoc/&gt;`_ project completed by
  Whiteroses (https://github.com/whiteroses). Many thanks to Google for running
  GSoC, the Python Software Foundation for organising and a huge thanks to Ira
  for completing the work. See https://github.com/Pylons/webob/pull/338 and
  https://github.com/Pylons/webob/pull/335. Documentation is available at
  https://docs.pylonsproject.org/projects/webob/en/master/api/webob.html

- When calling a ``wsgify`` decorated function, the default arguments passed
  to ``wsgify`` are now used when called with the request, and not as a
  `start_response`

  .. code::

     def hello(req, name):
         return &quot;Hello, %s!&quot; % name
     app = wsgify(hello, args=(&quot;Fred&quot;,))

     req = Request.blank(&#39;/&#39;)
     resp = req.get_response(app)   =&gt; &quot;Hello, Fred&quot;
     resp2 = app(req)  =&gt; &quot;Hello, Fred&quot;

  Previously the ``resp2`` line would have failed with a ``TypeError``. With
  this change there is no way to override the default arguments with no
  arguments. See https://github.com/Pylons/webob/pull/203

- When setting ``app_iter`` on a ``Response`` object the ``content_md5`` header
  is no longer cleared. This behaviour is odd and disallows setting the
  ``content_md5`` and then returning an iterator for chunked content encoded
  responses. See https://github.com/Pylons/webob/issues/86

Experimental Features
~~~~~~~~~~~~~~~~~~~~~

These features are experimental and may change at any point in the future.

- The cookie APIs now have the ability to set the SameSite attribute on a
  cookie in both ``webob.cookies.make_cookie`` and
  ``webob.cookies.CookieProfile``. See https://github.com/Pylons/webob/pull/255

Bugfix
~~~~~~

- Exceptions now use string.Template.safe_substitute rather than
  string.Template.substitute. The latter would raise for missing mappings, the
  former will simply not substitute the missing variable. This is safer in case
  the WSGI environ does not contain the keys necessary for the body template.
  See https://github.com/Pylons/webob/issues/345.

- Request.host_url, Request.host_port, Request.domain correctly parse IPv6 Host
  headers as provided by a browser. See
  https://github.com/Pylons/webob/pull/332

- Request.authorization would raise ValueError for unusual or malformed header
  values. See https://github.com/Pylons/webob/issues/231

- Allow unnamed fields in form data to be properly transcoded when calling
  request.decode with an alternate encoding. See
  https://github.com/Pylons/webob/pull/309

- ``Response.__init__`` would discard ``app_iter`` when a ``Response`` had no
  body, this would cause issues when ``app_iter`` was an object that was tied
  to the life-cycle of a web application and had to be properly closed.
  ``app_iter`` is more advanced API for ``Response`` and thus even if it
  contains a body and is thus against the HTTP RFC&#39;s, we should let the users
  shoot themselves by returning a body. See
  https://github.com/Pylons/webob/issues/305
   ```
   
  
</details>


 

<details>
  <summary>Links</summary>
  
  - PyPI: https://pypi.org/project/webob
  - Changelog: https://pyup.io/changelogs/webob/
  - Homepage: http://webob.org/
</details>


