0%

State and Identity · capstone

Capstone project: Project: Who May Change the List

Your reading now knows who is asking. A login checks a password and starts a session in an HttpOnly , /me reports the session’s user, and every write needs a token that may write. One thing is missing: a session can start, but it can never end. This project adds POST /logout and then checks the whole chapter against the contract.

What the contract asks

Open SERVICE_CONTRACT.md and read its “Who you are” section as a checklist:

  • POST /login with a known user’s Basic credentials: 200, {"user": name}, and set-cookie: session=<id>; HttpOnly; Max-Age=3600; Path=/. Anything else: 401 with the Basic challenge, no cookie, and no session.

  • GET /me: 200, with the session’s user or null.

  • POST /logout: 204 with no body and no content-length; the session ends, and set-cookie expires the cookie.

  • POST, PUT, and DELETE on books: 401 with www-authenticate: Bearer for a missing, malformed, or unknown token, 403 for a token that may only read, checked before the book id and the body.

  • 405 with allow: POST on /login and /logout, and allow: GET, HEAD on /me.

  • No reply body contains a password, a token, or a session id.

Every row except logout should already hold. Run the helper’s who scenario to see where you stand:

python pl_http.py who

It logs in as ana with Basic credentials, reads /me with the cookie it got, sends POST /logout with that cookie and Content-Length: 0, reads /me again with the same old cookie, and last sends POST /books with the reading token. Today the logout answers HTTP/1.0 404 Not Found, and the second /me still answers {"user": "ana"}: the session is alive.

Ending a session

Logging out takes two steps, and they are not equally important.

First, the server removes the session from state["sessions"]. This is the step that counts. Once the id is gone, the cookie is worthless: any request that carries it, from this client or from anyone who copied it, gets {"user": null}.

Second, the reply asks the client to forget the cookie, with a set-cookie whose Max-Age is 0. Your session_cookie already writes that: session_cookie("", max_age=0) gives session=""; HttpOnly; Max-Age=0; Path=/ when it uses SimpleCookie, which writes the empty as "". An writes session=; HttpOnly; Max-Age=0; Path=/. Either line expires the cookie. A jar that reads this line drops its session cookie at once. But Lesson 1 said it: every attribute is a request the client may ignore. A server that only sent this line and kept the session would leave the old cookie working for anyone who kept a copy.

To find the session, read the session cookie the same way session_user does, and remove it with state["sessions"].pop(value, None). With a default, pop returns it instead of raising when the key is missing, which happens for an unknown or already ended session. Logging out without a session, or twice in a row, is not an error: the client ends up logged out either way, so each answers 204. Nothing on this server ends a session by time, so logout is the only way one ends before the server stops.

Milestones

  1. In handle, add a branch for /logout. A POST ends the session its cookie names, if any, and answers 204 with an empty body, no content-length, and a set-cookie header from session_cookie("", max_age=0). Any other gets not_allowed("POST").

  2. Run python pl_http.py who again. The logout now answers HTTP/1.0 204 No Content, and the next /me with the old cookie answers {"user": null}. The reading token’s POST /books still gets HTTP/1.0 403 Forbidden. Under the 204, the helper prints the line that expires the cookie, here as SimpleCookie writes it:

  set-cookie: session=""; HttpOnly; Max-Age=0; Path=/
  1. You can add a test of logout to tests/test_server.py. make_request takes a of headers, so a test can send a cookie, for example:

def test_logout_ends_the_session():
    state = reading_server.sample_state()
    session_id = reading_server.start_session(state, "ana")
    cookie = {"cookie": "session=" + session_id}
    response = reading_server.handle(make_request("POST", "/logout", headers=cookie), state)
    assert response["status"] == 204
    me = reading_server.handle(make_request("GET", "/me", headers=cookie), state)
    assert json.loads(me["body"].decode("utf-8")) == {"user": None}
  1. Run python -m pytest -q. Every test should pass, the tests that came with the chapter included.

python -m pytest -q

What gets checked

Check my work checks the whole chapter again with users, passwords, tokens, and books it has not shown you, and with session ids it chooses itself, as Lesson 2’s check did. It calls handle and the helpers directly for logins, /me, logout, and every kind of write, compares the state before and after each refusal, and searches every reply for the credentials it sent. It builds your server’s handler class with make_server without opening a , passes a login and a write with no token through it as bytes, and reads the set-cookie and www-authenticate lines that come back. Last, it runs your tests once, as they are; they must all pass, and none may import socket. The who scenario on your is for you to watch; nothing from it is graded.

Then press Check my work. Your list now says who may change it. Every reader, though, still downloads the whole list every time they ask, even when nothing has changed since the last time. Chapter 8 teaches the server to say “nothing changed” and the client to remember what it already has.

Task

Add POST /logout to reading_server.py, and make every rule of “Who you are” in SERVICE_CONTRACT.md hold.

  • POST /logout removes the session its session names from state["sessions"], if there is one, and answers 204 with the body b"", no content-length, and a set-cookie header from session_cookie("", max_age=0). Without a cookie, or for a session already ended, it still answers 204. Any other on /logout gets 405 with allow: POST.

  • A client’s POST /logout sends no body, so, like every POST to this server, it sends content-length: 0; without it, read_body answers 411.

  • Everything from Lessons 2, 5, and 6 still holds: Basic login with a session in an HttpOnly cookie or 401 with the Basic challenge, /me with the user or null, bearer-protected writes with 401 or 403 by cause before the book id and the body, the 405s, and no password, token, or session id in any reply.

Run python pl_http.py who in the to log in, log out, and see the old cookie stop working, and python -m pytest -q to run your tests.

Then press Check my work. With users, tokens, books, and session ids you have not seen, it calls handle and the helpers directly for logins, /me, logout, and every kind of write, comparing the state before and after each refusal and searching each reply for credentials. It builds your server’s handler class with make_server without opening a and passes a login and a write without a token through it as bytes. Last, it runs your tests once, as they are: they must all pass, and none may import socket. Nothing from your terminal is graded.