Serve Several Domains and Screen Requests
The shop opens a second site, an outlet, with its own users. It must not serve some countries.
It limits password guessing per client address, and it wants every change to an order to say
which version it changes. These are decided from the request itself: its host, the address it came
from, its headers. All are declared in the exposition annotation of the Context, except require, which is a directive on a route.
§Serve the shop on several domains
An authority is your name for a domain the application is served on. toa create wrote one, local: localhost:8000. The shop names its two domains instead, and keeps localhost:8000 as
the host of shop in local, the environment of your machine:
# context.toa.yaml
exposition:
authorities:
shop: api.store.example
shop@local: localhost:8000
outlet: api.outlet.example $ npm run restart Rename local before the application has users, for the reason given below.
Keys are names you choose, values are hosts as they arrive in the host header of a request. authorities is the one required key of the annotation; a shop on a single domain declares one
entry.
A host can differ per environment:
exposition:
authorities:
shop: api.store.example
shop@staging: api.staging.store.example A request to a host that is not declared is served too, and its authority is the host itself. A host header that is not a host (or a bracketed IPv6 address) with an optional port is answered 400 Bad Request.
§Identities belong to an authority
Users of shop and users of outlet are separate, even when a person uses the same email for
both:
- A username under
shopand the same username underoutletare two usernames, and may be two identities. - A
Tokenissued under one authority is answered401under another. - A Google account that signs in under both is two identities.
- Emailed codes and passkeys work only under the authority they were issued or added in.
The first administrator is named together with its authority for the same reason.
Do not rename an authority once it has users. Their passwords and tokens are kept under the
name; changing shop to store leaves all of them unable to sign in. Changing the host behind a
name is safe.
§Tell an operation which domain was asked
The catalog, store.catalog, shows different
products in the outlet. Map the authority into the input:
# components/store.catalog/manifest.toa.yaml
exposition:
/:
GET:
auth:anonymous: true
map:authority: authority
endpoint: enumerate The value of map:authority is the input property that receives the name: shop, not api.store.example. For a host that is not declared it receives the host. An
operation that asks for a sign-in code gets the authority it
must pass the same way.
§Tell the gateway the client address
In production the gateway is behind a load balancer, an ingress or a CDN, and every connection comes from that machine. The address of the client is in a header the edge writes. Name it:
# context.toa.yaml
exposition:
ip: cf-connecting-ip Name the header your edge overwrites: CF-Connecting-IP, X-Real-IP, X-Forwarded-For,
depending on the infrastructure. A client can send any of these itself; name one the edge does not
control and clients choose their own address. Where the header holds a list, the last value is
taken.
Without ip, or for a request without the header, the request has no address, and whatever
counts by address lets it through. Two things count by address:
- the limit on failed sign-ins,
bouncer, which cannot be declared withoutip; - throttling keyed on
ip, described in Input and Output, which also protects the gateway’s own endpoints for other applications.
§Refuse requests by country
A CDN reports the visitor’s country in a header, which country names. censor refuses the
requests from the countries it lists:
# context.toa.yaml
exposition:
country: cf-ipcountry
censor: [KP, SY] GET /store/catalog/ HTTP/1.1
host: api.store.example
cf-ipcountry: KP 451 Unavailable For Legal Reasons The body is empty, whatever the path; nothing is authenticated and no operation is called.
- A country matches exactly:
kpis notKP. - A request without the header, or from a country that is not listed, is served.
censorlists at least one country, and refuses nothing withoutcountry.- A page in a browser can read the status and show a message: the reply carries CORS headers and the preflight is answered.
- The header is taken as it arrives. As with
ip, it is worth what the edge that writes it is worth: a client that reaches the gateway around the edge sends what it likes.
§Require a header
A client that changes an order must say which version of it it has seen, so that two people
editing at once do not overwrite each other. require:header makes a route insist on a header,
here a PUT to the inherited transit on the customer’s routes:
# components/store.orders/manifest.toa.yaml
exposition:
/:customer:
auth:id: customer
/:id:
require:header: if-match
PUT: transit PUT /store/orders/2428c31ecb6e4a51a24ef52f0c4181b9/7d1a0c1e5b6f4f0f9a3b2c4d5e6f7a8b/ HTTP/1.1
host: api.store.example
authorization: Token eyJhbGciOiJkaXIi...
content-type: application/yaml
status: cancelled 400 Bad Request
Header required: if-match require:headers takes a list:
require:headers: [if-match, if-unmodified-since] What if-match then does is in Cache Replies and Prevent Lost Updates.
§Test it
The steps are the ones toa create wrote, which Write a Test explains.
The second scenario signs a customer up at the sign-up route and
creates an order at the customer’s routes.
# features/screening.feature
Feature: Screening requests
Scenario: A request from a refused country
When the request is sent:
"""
GET /store/catalog/ HTTP/1.1
cf-ipcountry: KP
"""
Then the response is received:
"""
451 Unavailable For Legal Reasons
"""
Scenario: An order is not changed without naming its version
When the request is sent:
"""
POST /accounts/ HTTP/1.1
content-type: application/yaml
accept: application/yaml
name: Bob
"""
Then the response is received:
"""
201 Created
authorization: Token ${{ bob.token }}
id: ${{ bob.id }}
"""
When the request is sent:
"""
POST /store/orders/${{ bob.id }}/ HTTP/1.1
authorization: Token ${{ bob.token }}
content-type: application/yaml
accept: application/yaml
items: []
total: 0
"""
Then the response is received:
"""
201 Created
id: ${{ order.id }}
"""
When the request is sent:
"""
PUT /store/orders/${{ bob.id }}/${{ order.id }}/ HTTP/1.1
authorization: Token ${{ bob.token }}
content-type: application/yaml
status: cancelled
"""
Then the response is received:
"""
400 Bad Request
""" $ npm run restart
$ npm run features §Reference
Keys of exposition in context.toa.yaml:
| Key | Value | Default |
|---|---|---|
authorities | Map of authority name to host. Required. | |
ip | Name of the header that holds the client address. | none: requests have no address |
bouncer | attempts, interval; see Limit password guessing. Requires ip. | off |
country | Name of the header that holds the client’s country. | none: requests have no country |
censor | Countries answered 451; requires country. | off |
Directives:
| Directive | Value |
|---|---|
require:header | A header the request must carry. |
require:headers | A list of them. |
map:authority | The input property that receives the authority. |