Skip to content
Merged
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
5 changes: 5 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,11 @@ domain named after its public key - `https://{key}.genhttp.run/`
- **The subdomain serves the lambda only while it has no domain of its
own that its tier serves**; with one, it redirects there the same way. A
domain of its own works as it did.
- **`www.` in front of a domain redirects to the domain - decided by the
owner**, the same way, unless the owner entered the domain with `www.`:
then that is where it answers, and the name without is not sent there.
Never served at both names (two sites to a search engine), and never
where the `www.` is another lambda's domain or the platform's own name.
- **Finding a lambda by an address reads the key as it is spelled
(`LambdaKeys.TryRead`), never by the rules for claiming one**
(`TryNormalize`): a rule made later - a reserved name, `xn--` - must not
Expand Down
14 changes: 12 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,11 @@ agents' endpoint and git stay where they are, on the platform.
only there: its subdomain sends visitors on to the domain the same way, so
there is one address for search engines to keep and for visitors to pass
on. Removing the domain, or the tier that serves it, has the subdomain
answer again.
answer again. For the same reason `www.` in front of the domain sends its
visitors on to it - `www.shop.example.com` to `shop.example.com` - unless
the owner entered the domain with `www.`, which is then where it answers,
and nothing is sent there from the name without; or unless that `www.` is
the domain of another lambda, or one of the platform's own names.
- **Nothing there.** A subdomain nothing is online at - a key nobody has, a
lambda that is offline, a name no key can be - is told so in a plain page
that leads to the site, or in a line of JSON for anything but a browser.
Expand Down Expand Up @@ -1643,7 +1647,7 @@ every other request, so it needs a certificate - issued by hand for now, with
the web root above, into a folder of its own:

```bash
certbot certonly --webroot -w /opt/genhttp-lambda/acme -d shop.example.com
certbot certonly --webroot -w /opt/genhttp-lambda/acme -d shop.example.com -d www.shop.example.com

mkdir -p /opt/genhttp-lambda/certs/shop.example.com
install -m 0644 -o root -g 1001 /etc/letsencrypt/live/shop.example.com/fullchain.pem /opt/genhttp-lambda/certs/shop.example.com/
Expand All @@ -1657,6 +1661,12 @@ renewal on that engine. Until the certificate is there, visitors of the
domain are redirected to HTTPS and shown the certificate every unknown name
is shown, which does not carry their name.

The `www.` name goes on the same certificate: a visitor who types it is sent
on to the domain, and has to trust the server before they can be told so.
Leave it out where its DNS does not point here, or certbot fails for both
names. The challenge for `www.` is sent on to the domain like any other
request once the domain is set, which the certificate authority follows.

Mind the order: once the domain is set, the lambda's subdomain sends its
visitors to the domain at once. The challenge is answered for any host as
soon as its DNS points here, whether the domain is set or not, so the
Expand Down
15 changes: 15 additions & 0 deletions src/Frontend/src/control/DomainTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,9 @@ function Records({ domain, configured, state, checking, onCheck }: {
const ours = new Set([ADDRESSES.v4, ADDRESSES.v6]);
const pointsHere = resolved.some((address) => ours.has(address));
const elsewhere = resolved.filter((address) => !ours.has(address));
// typed with www by some, and sent on to the domain by the server - unless
// the owner chose the name with www, which is then the domain itself
const www = domain.startsWith('www.') ? null : `www.${domain}`;

return (
<section className="mt-8">
Expand Down Expand Up @@ -239,10 +242,22 @@ function Records({ domain, configured, state, checking, onCheck }: {
<tbody className="divide-y divide-slate-100 dark:divide-ink-850">
<Record type="A" name={domain} value={ADDRESSES.v4} />
<Record type="AAAA" name={domain} value={ADDRESSES.v6} />
{www && (
<>
<Record type="A" name={www} value={ADDRESSES.v4} />
<Record type="AAAA" name={www} value={ADDRESSES.v6} />
</>
)}
</tbody>
</table>
</div>

{www && (
<p className="mt-2 text-[13px] text-slate-600 dark:text-slate-400">
{said.www(<span className="font-mono">{www}</span>, <span className="font-mono">{domain}</span>)}
</p>
)}

{configured && state.dns && (
<p className="mt-3 flex items-start gap-2 text-[13px]">
{pointsHere ? (
Expand Down
7 changes: 6 additions & 1 deletion src/Frontend/src/locales/de/editor/domain.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,12 @@ export const domain: EditorMessages['domain'] = {
point: 'Domain auf diesen Server zeigen lassen',
check: 'Nochmal prüfen',
records:
'Legen Sie beim DNS-Anbieter der Domain diese zwei Einträge an. Den AAAA-Eintrag können Sie weglassen, wenn die Domain nicht über IPv6 erreichbar sein soll.',
'Legen Sie beim DNS-Anbieter der Domain diese Einträge an. Die AAAA-Einträge können Sie weglassen, wenn die Domain nicht über IPv6 erreichbar sein soll.',
www: (www, domain) => (
<>
Besucher, die {www} eingeben, werden zu {domain} weitergeleitet, damit Suchmaschinen eine einzige Adresse behalten. Die zugehörigen Einträge sind optional.
</>
),
type: 'Typ',
name: 'Name',
value: 'Wert',
Expand Down
8 changes: 7 additions & 1 deletion src/Frontend/src/locales/en/editor/domain.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,13 @@ export const domain = {
point: 'Point the domain at this server',
check: 'Check again',
records:
'At whoever manages the DNS of the domain, add these two records. Leave out the AAAA record if you would rather not be reachable over IPv6.',
'At whoever manages the DNS of the domain, add these records. Leave out the AAAA records if you would rather not be reachable over IPv6.',
/** Under the DNS records, when the domain was entered without www: why there are records for the name with www as well. */
www: (www: Node, domain: Node) => (
<>
Visitors who type {www} are sent on to {domain}, so search engines keep one address. Its records are optional.
</>
),
type: 'Type',
name: 'Name',
value: 'Value',
Expand Down
7 changes: 6 additions & 1 deletion src/Frontend/src/locales/es/editor/domain.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,12 @@ export const domain: EditorMessages['domain'] = {
point: 'Apunta el dominio a este servidor',
check: 'Volver a comprobar',
records:
'En el proveedor que gestiona el DNS del dominio, añade estos dos registros. Omite el registro AAAA si prefieres no ser accesible por IPv6.',
'En el proveedor que gestiona el DNS del dominio, añade estos registros. Omite los registros AAAA si prefieres no ser accesible por IPv6.',
www: (www, domain) => (
<>
Los visitantes que escriban {www} serán redirigidos a {domain}, así los buscadores mantienen una sola dirección. Sus registros son opcionales.
</>
),
type: 'Tipo',
name: 'Nombre',
value: 'Valor',
Expand Down
7 changes: 6 additions & 1 deletion src/Frontend/src/locales/fr/editor/domain.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,12 @@ export const domain: EditorMessages['domain'] = {
point: 'Faites pointer le domaine vers ce serveur',
check: 'Revérifier',
records:
'Chez le gestionnaire DNS du domaine, ajoutez ces deux enregistrements. Laissez de côté l’enregistrement AAAA si vous ne voulez pas être joignable en IPv6.',
'Chez le gestionnaire DNS du domaine, ajoutez ces enregistrements. Laissez de côté les enregistrements AAAA si vous ne voulez pas être joignable en IPv6.',
www: (www, domain) => (
<>
Les visiteurs qui saisissent {www} sont redirigés vers {domain}, afin que les moteurs de recherche ne retiennent qu’une seule adresse. Ses enregistrements sont facultatifs.
</>
),
type: 'Type',
name: 'Nom',
value: 'Valeur',
Expand Down
7 changes: 6 additions & 1 deletion src/Frontend/src/locales/id/editor/domain.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,12 @@ export const domain: EditorMessages['domain'] = {
point: 'Arahkan domain ke server ini',
check: 'Cek lagi',
records:
'Di penyedia DNS domain Anda, tambahkan dua record ini. Lewati record AAAA kalau Anda tidak ingin bisa diakses lewat IPv6.',
'Di penyedia DNS domain Anda, tambahkan record ini. Lewati record AAAA kalau Anda tidak ingin bisa diakses lewat IPv6.',
www: (www, domain) => (
<>
Pengunjung yang mengetik {www} akan diarahkan ke {domain}, sehingga mesin pencari hanya mengenal satu alamat. Record-nya bersifat opsional.
</>
),
type: 'Tipe',
name: 'Nama',
value: 'Nilai',
Expand Down
7 changes: 6 additions & 1 deletion src/Frontend/src/locales/it/editor/domain.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,12 @@ export const domain: EditorMessages['domain'] = {
point: 'Punta il dominio su questo server',
check: 'Controlla di nuovo',
records:
'Aggiungi questi due record dove gestisci il DNS del dominio. Salta il record AAAA se preferisci non essere raggiungibile via IPv6.',
'Aggiungi questi record dove gestisci il DNS del dominio. Salta i record AAAA se preferisci non essere raggiungibile via IPv6.',
www: (www, domain) => (
<>
Chi digita {www} viene reindirizzato a {domain}, così i motori di ricerca mantengono un solo indirizzo. I record per questo nome sono facoltativi.
</>
),
type: 'Tipo',
name: 'Nome',
value: 'Valore',
Expand Down
7 changes: 6 additions & 1 deletion src/Frontend/src/locales/ja/editor/domain.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,12 @@ export const domain: EditorMessages['domain'] = {
point: 'ドメインをこのサーバーに向ける',
check: '再確認',
records:
'ドメインのDNSを管理しているサービスで、次の2つのレコードを追加してください。IPv6で接続できなくてもよければ、AAAAレコードは省いてかまいません。',
'ドメインのDNSを管理しているサービスで、次のレコードを追加してください。IPv6で接続できなくてもよければ、AAAAレコードは省いてかまいません。',
www: (www, domain) => (
<>
{www}にアクセスした訪問者は{domain}に転送されるため、検索エンジンにはアドレスが1つだけ残ります。こちらのレコードは任意です。
</>
),
type: 'タイプ',
name: '名前',
value: '値',
Expand Down
7 changes: 6 additions & 1 deletion src/Frontend/src/locales/ko/editor/domain.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,12 @@ export const domain: EditorMessages['domain'] = {
point: '도메인이 이 서버를 가리키게 하기',
check: '다시 확인',
records:
'도메인의 DNS를 관리하는 곳에서 아래 두 레코드를 추가하세요. IPv6로 접속되지 않길 원하면 AAAA 레코드는 빼도 돼요.',
'도메인의 DNS를 관리하는 곳에서 아래 레코드를 추가하세요. IPv6로 접속되지 않길 원하면 AAAA 레코드는 빼도 돼요.',
www: (www, domain) => (
<>
{www}를 입력해서 들어온 방문자는 {domain}(으)로 자동 이동해요. 그래서 검색 엔진에는 주소가 하나만 남아요. 이 이름의 레코드는 선택 사항이에요.
</>
),
type: '유형',
name: '이름',
value: '값',
Expand Down
7 changes: 6 additions & 1 deletion src/Frontend/src/locales/nl/editor/domain.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,12 @@ export const domain: EditorMessages['domain'] = {
point: 'Laat het domein naar deze server wijzen',
check: 'Opnieuw controleren',
records:
'Voeg deze twee records toe bij de partij die de DNS van het domein beheert. Laat het AAAA-record weg als je liever niet via IPv6 bereikbaar bent.',
'Voeg deze records toe bij de partij die de DNS van het domein beheert. Laat de AAAA-records weg als je liever niet via IPv6 bereikbaar bent.',
www: (www, domain) => (
<>
Bezoekers die {www} intypen, worden doorgestuurd naar {domain}, zodat zoekmachines één adres houden. De bijbehorende records zijn optioneel.
</>
),
type: 'Type',
name: 'Naam',
value: 'Waarde',
Expand Down
7 changes: 6 additions & 1 deletion src/Frontend/src/locales/pl/editor/domain.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,12 @@ export const domain: EditorMessages['domain'] = {
point: 'Skieruj domenę na ten serwer',
check: 'Sprawdź ponownie',
records:
'U dostawcy DNS domeny dodaj te dwa rekordy. Pomiń rekord AAAA, jeśli nie chcesz, żeby domena była dostępna przez IPv6.',
'U dostawcy DNS domeny dodaj te rekordy. Pomiń rekordy AAAA, jeśli nie chcesz, żeby domena była dostępna przez IPv6.',
www: (www, domain) => (
<>
Osoby, które wpiszą {www}, są przekierowywane na {domain}, dzięki czemu wyszukiwarki widzą jeden adres. Rekordy dla {www} są opcjonalne.
</>
),
type: 'Typ',
name: 'Nazwa',
value: 'Wartość',
Expand Down
7 changes: 6 additions & 1 deletion src/Frontend/src/locales/pt-pt/editor/domain.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,12 @@ export const domain: EditorMessages['domain'] = {
point: 'Aponta o domínio para este servidor',
check: 'Verificar outra vez',
records:
'No serviço que gere o DNS do domínio, adiciona estes dois registos. Deixa de fora o registo AAAA se preferires não estar acessível por IPv6.',
'No serviço que gere o DNS do domínio, adiciona estes registos. Deixa de fora os registos AAAA se preferires não estar acessível por IPv6.',
www: (www, domain) => (
<>
Os visitantes que escreverem {www} são reencaminhados para {domain}, para que os motores de pesquisa mantenham um único endereço. Os registos deste nome são opcionais.
</>
),
type: 'Tipo',
name: 'Nome',
value: 'Valor',
Expand Down
7 changes: 6 additions & 1 deletion src/Frontend/src/locales/pt/editor/domain.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,12 @@ export const domain: EditorMessages['domain'] = {
point: 'Aponte o domínio para este servidor',
check: 'Verificar de novo',
records:
'No provedor que gerencia o DNS do domínio, adicione estes dois registros. Deixe de fora o registro AAAA se preferir não ficar acessível via IPv6.',
'No provedor que gerencia o DNS do domínio, adicione estes registros. Deixe de fora os registros AAAA se preferir não ficar acessível via IPv6.',
www: (www, domain) => (
<>
Os visitantes que digitarem {www} são redirecionados para {domain}, para que os buscadores mantenham um único endereço. Os registros dele são opcionais.
</>
),
type: 'Tipo',
name: 'Nome',
value: 'Valor',
Expand Down
7 changes: 6 additions & 1 deletion src/Frontend/src/locales/tr/editor/domain.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,12 @@ export const domain: EditorMessages['domain'] = {
point: 'Alan adını bu sunucuya yönlendirin',
check: 'Tekrar kontrol et',
records:
'Alan adınızın DNS kayıtlarını yönettiğiniz yerde şu iki kaydı ekleyin. IPv6 üzerinden erişilebilir olmak istemiyorsanız AAAA kaydını eklemeyin.',
'Alan adınızın DNS kayıtlarını yönettiğiniz yerde şu kayıtları ekleyin. IPv6 üzerinden erişilebilir olmak istemiyorsanız AAAA kayıtlarını eklemeyin.',
www: (www, domain) => (
<>
{www} yazan ziyaretçiler {domain} adresine yönlendirilir, böylece arama motorları tek bir adres görür. Bunun kayıtları isteğe bağlıdır.
</>
),
type: 'Tür',
name: 'Ad',
value: 'Değer',
Expand Down
42 changes: 33 additions & 9 deletions src/GenHTTP.Lambda/Services/Hosting/DomainRegistry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ namespace GenHTTP.Lambda.Services.Hosting;

/// <summary>
/// Which hosts the server answers for on behalf of a lambda, and which lambda
/// each of them belongs to: the domains of their own, and the subdomains of
/// the hosting domain.
/// each of them belongs to: the domains of their own, www in front of them,
/// and the subdomains of the hosting domain.
/// </summary>
/// <remarks>
/// Asked on every request the server receives, so the domains are held in
Expand All @@ -29,7 +29,7 @@ namespace GenHTTP.Lambda.Services.Hosting;
public sealed class DomainRegistry(IDbContextFactory<LambdaDbContext> databases, LambdaAddresses addresses, ILogger<DomainRegistry> logger)
: IDomainRegistry
{
private volatile Served _served = new(FrozenDictionary<string, long>.Empty, FrozenDictionary<string, string>.Empty);
private volatile Served _served = new(FrozenDictionary<string, long>.Empty, FrozenDictionary<string, string>.Empty, FrozenDictionary<string, string>.Empty);

/// <summary>
/// One reload at a time: two running side by side could finish in either
Expand All @@ -49,7 +49,21 @@ public sealed class DomainRegistry(IDbContextFactory<LambdaDbContext> databases,
#region Functionality

public LambdaHost? Find(string host)
=> _served.Domains.TryGetValue(host, out var lambdaId) ? new CustomDomain(host, lambdaId) : addresses.Resolve(host);
{
var served = _served;

if (served.Domains.TryGetValue(host, out var lambdaId))
{
return new CustomDomain(host, lambdaId);
}

if (served.Www.TryGetValue(host, out var domain))
{
return new WwwOfDomain(host, domain);
}

return addresses.Resolve(host);
}

public string? DomainOf(string publicKey) => _served.ByKey.GetValueOrDefault(publicKey);

Expand All @@ -71,8 +85,17 @@ public void Reload()

var previous = _served.Domains.Count;

_served = new Served(served.ToFrozenDictionary(l => l.Domain, l => l.Id, StringComparer.Ordinal),
served.ToFrozenDictionary(l => l.PublicKey, l => l.Domain, StringComparer.Ordinal));
var domains = served.ToFrozenDictionary(l => l.Domain, l => l.Id, StringComparer.Ordinal);

// a domain entered without www is also typed with it, the way
// sites used to be found; one entered with it is the owner's
// choice, and so is a www that is a domain of its own
var www = served.Where(l => !l.Domain.StartsWith("www.", StringComparison.Ordinal))
.Select(l => (Name: "www." + l.Domain, l.Domain))
.Where(w => !domains.ContainsKey(w.Name) && !addresses.Owns(w.Name))
.ToFrozenDictionary(w => w.Name, w => w.Domain, StringComparer.Ordinal);

_served = new Served(domains, served.ToFrozenDictionary(l => l.PublicKey, l => l.Domain, StringComparer.Ordinal), www);

if (previous != _served.Domains.Count)
{
Expand All @@ -86,10 +109,11 @@ public void Reload()
#region Types

/// <summary>
/// The served domains, by name and by the key of their lambda - swapped
/// together, so the two never tell different stories.
/// The served domains, by name and by the key of their lambda, and the
/// domain each www in front of one is sent on to - swapped together, so
/// they never tell different stories.
/// </summary>
private sealed record Served(FrozenDictionary<string, long> Domains, FrozenDictionary<string, string> ByKey);
private sealed record Served(FrozenDictionary<string, long> Domains, FrozenDictionary<string, string> ByKey, FrozenDictionary<string, string> Www);

#endregion

Expand Down
7 changes: 7 additions & 0 deletions src/GenHTTP.Lambda/Services/Hosting/DomainRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,13 @@ public abstract record LambdaHost(string Name);
/// </summary>
public sealed record CustomDomain(string Name, long LambdaId) : LambdaHost(Name);

/// <summary>
/// <c>www.</c> in front of a domain of its own that its owner entered
/// without it, which sends its visitors on to that domain.
/// </summary>
/// <param name="Domain">The domain it is in front of, the one the lambda answers at</param>
public sealed record WwwOfDomain(string Name, string Domain) : LambdaHost(Name);

/// <summary>
/// A subdomain of the hosting domain, which is the address of the lambda
/// whose public key it is named after - if there is one.
Expand Down
3 changes: 3 additions & 0 deletions src/GenHTTP.Lambda/Services/Hosting/DomainRouter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ public async ValueTask PrepareAsync(IServer server)
public ValueTask<IResponse?> HandleAsync(IRequest request) => request.ResolveHost(registry) switch
{
CustomDomain => domains.HandleAsync(request),
// sent on to the domain before it gets here; were it not, it is still
// somebody's site and never the platform
WwwOfDomain => domains.HandleAsync(request),
HostedLambda => hosted.HandleAsync(request),
HostingHome => home.HandleAsync(request),
_ => platform.HandleAsync(request)
Expand Down
5 changes: 3 additions & 2 deletions src/GenHTTP.Lambda/Services/Hosting/IDomainRegistry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ public interface IDomainRegistry
int Count { get; }

/// <summary>
/// What a host belongs to: a lambda's domain, a subdomain of the hosting
/// domain or that domain itself - or nothing, for the platform.
/// What a host belongs to: a lambda's domain, www in front of one, a
/// subdomain of the hosting domain or that domain itself - or nothing,
/// for the platform.
/// </summary>
/// <param name="host">A host in its normalized form, see <see cref="DomainNames.Normalize"/></param>
LambdaHost? Find(string host);
Expand Down
Loading
Loading