XTQ/Web/DocsDownload

Routes

Every directive that can go in a route or site block, with exact behaviour and an example each.

A route picks part of a site by path and says how to treat it. This page covers every directive that can go in a route. How routes are matched and nested is in Config file.

Directive Where What it does
route <pattern> { } site, route A nested route
strip route Removes the matched prefix from the path
rewrite </path[?query]> site, route Replaces the path, and the query if one is given
header <Name> <value> site, route Sets a response header
header -<Name> site, route Removes a response header
ip <entries>... route The route matches only for these client addresses
not ip <entries>... route The route matches only for other client addresses
auth <backend> </path> site, route Asks another service before the request is handled
compress off site, route Leaves proxied responses uncompressed
challenge [overload|always] site, route The browser check
probes on|off site Answers scanner probes with 404, on by default. See Protection

Plus one handler per block, see Handlers.

route

route <pattern> {
    ...
}

/x matches exactly, /x/* matches everything under /x/, /x* matches everything starting with /x, and * matches every path. A route always needs a block.

site example.com {
    route /health {
        respond 200 "ok"
    }
    route /static/* {
        files /var/www/static
    }
    proxy 127.0.0.1:3000
}

strip

strip

Removes the part of the path the route's pattern matched, before nested routes and the handler see it. It takes no arguments and works only inside a route.

What is removed is the pattern without its trailing * and without a trailing slash. For route /api/* that is /api. The rest of the path keeps its leading slash, and the query string is kept as it is.

With this config:

site example.com {
    route /api/* {
        strip
        proxy 127.0.0.1:3000
    }
}
Client asks for Backend gets, with strip Backend gets, without strip
/api/auth/me /auth/me /api/auth/me
/api/users?page=2 /users?page=2 /api/users?page=2
/api/ / /api/
/API/users /users /API/users
/api The route does not match The route does not match

/api alone does not match /api/*. If clients use it, add an exact route that redirects:

route /api {
    redirect /api/ 301
}

The same rule for the other pattern forms:

Route Removed /api/x becomes
route /api/* /api /x
route /api* /api /x, and /apix becomes /x too
route /api (exact) /api not matched. /api itself becomes /
route * nothing /api/x

Use /api/* rather than /api* with strip, so /api-docs is not turned into /-docs.

When the client's encoding of the prefix matches the pattern, the rest of the path is passed on byte for byte, so /api/v1/a%2Fb arrives as /v1/a%2Fb.

Common mistake

Use strip only when the backend does not know about the prefix. If your backend's own routes already start with /api, for example an Express app with app.get('/api/auth/me') or a Next.js app with a basePath, do not use strip. With strip, the backend gets /auth/me, has no route for it, and answers 404.

# The backend serves /api/... itself: no strip.
route /api/* {
    proxy 127.0.0.1:3000
}

# The backend serves /auth/me, /users, ... at its root: strip.
route /api/* {
    strip
    proxy 127.0.0.1:4000
}

rewrite

rewrite </path[?query]>

Replaces the path. The target must start with /. If it contains a ?, the query string is replaced too. Without a ?, the client's query string is kept. A target ending in ? removes the query string.

Line /old?x=1 becomes
rewrite /new /new?x=1
rewrite /new?view=lb /new?view=lb
rewrite /new? /new

In a block with both, strip runs first and rewrite second, whatever order you write them in. Nested routes and the handler see the rewritten path. Placeholders are not expanded in rewrite.

A dashboard view chosen by query string:

route /lb/* {
    strip
    rewrite /?view=lb
    proxy 127.0.0.1:9090
}

A single page for every path, as a 404:

site * {
    rewrite /index.html
    files /var/www/unassigned {
        status 404
    }
}
header <Name> <value>
header -<Name>

Sets a response header, replacing any value the handler gave it, or removes it with a leading -. Quote values that contain spaces. Several header lines in a block are applied in order.

site example.com {
    header X-Robots-Tag "noindex, nofollow"
    header -X-Debug
    route /live.json {
        header Cache-Control no-store
        files /var/www/live
    }
    proxy 127.0.0.1:3000
}

header lines apply to whatever answered within the site: the handler, an auth service's reply, the browser check page, and the 404 when no route matched. They do not apply to scanner probe replies or to the 404 for an unknown hostname.

The Server and X-Powered-By headers are set after header lines, on every response. To control them, use the XTQW_SERVER_HEADER and XTQW_POWERED_BY environment variables. See Operations.

header changes the response to the client. To change the request sent to a backend, use header_up in a proxy block.

ip and not ip

ip <entry>...
not ip <entry>...

Limits a route to some client addresses. With ip, the route matches only for clients in the list. With not ip, it matches only for clients outside it. If the address check fails, the route does not match and the next route is tried.

An entry is one of:

  • an address: 203.0.113.7, 2001:db8::1,
  • a CIDR range: 10.0.0.0/8, 2001:db8::/32,
  • a file: @/etc/xtq-web/admins.list.

List every entry on one line. One ip or not ip per route. ip works only inside a route, so the site's other routes stay reachable.

A file holds one address, CIDR range or hostname per line, with # comments:

# office
203.0.113.0/24
# home, dynamic DNS
home.example.com

Hostnames in a file are resolved to every address they have. Files are read again every 60 seconds and hostnames resolved again, so a dynamic DNS name keeps working without a reload. If a later read fails or comes back empty, the previous list is kept.

If a file can't be read when the config loads, xTQ Web logs WARNING: ip list ...; it matches no one until it can be read and keeps going. With ip, that route then matches nobody from that file. With not ip, it matches every client. The next 60-second refresh picks the file up once it is readable. Hostnames can only appear in files, not directly on the ip line.

The address checked is the real client address worked out with trust. If a proxy or CDN is in front of xTQ Web, list it in trust, or every request looks like it comes from the proxy. See HTTPS.

Only a list of addresses may reach an admin area. Everyone else gets a fixed page with status 403:

site admin.example.com {
    route * {
        not ip 203.0.113.7 10.0.0.0/8 @/etc/xtq-web/admins.list
        rewrite /blocked.html
        files /var/www/blocked {
            status 403
        }
    }
    proxy 127.0.0.1:3000
}

The reverse, an area only for some clients:

route /internal/* {
    ip 10.0.0.0/8
    proxy 127.0.0.1:9000
}

Clients outside 10.0.0.0/8 skip this route and fall through to the next one or the site's handler.

auth

auth <backend> </path>

Asks another service whether the request may continue, before the handler runs. The backend is written like a proxy backend: 127.0.0.1:8082, http://auth.internal:80 or unix:/run/auth.sock. The path must start with /.

xTQ Web sends a GET to that path on the auth service. The original query string is appended. The request carries the client's headers, cookies and Authorization included, minus Content-Length and Content-Type, with Host and the X-Forwarded-* headers set as for a proxied request. The auth service does not see the original path or method.

Auth service answers Result
Any 2xx The request continues to the handler. The auth reply is discarded, headers included
444 The connection is closed without a reply
Anything else That reply, status, headers and body, goes to the client. The handler never runs
Not reachable 502 Bad Gateway
No answer within 10 seconds 504 Gateway Timeout

header lines of the route are applied to the auth service's reply too.

An auth in an outer block and another in a nested route both run, outer first. Inside one block only the last auth line counts.

A status page behind a login session:

site status.example.com {
    route /panel/* {
        strip
        auth 127.0.0.1:8082 /auth
        files /var/www/panel
    }
    respond 404
}

The service at 127.0.0.1:8082 checks the session cookie and answers 204 to let the visitor in, 302 with a Location to send them to a login page, or 444 to drop a caller it wants to ignore.

compress

compress off
compress on
compress

Proxied responses are compressed by default when the client accepts it and the content is text. compress off turns that off for the block and the routes inside it. compress on, or compress alone, turns it back on in a nested route.

route /downloads/* {
    compress off
    proxy 127.0.0.1:3000
}

compress affects only the proxy handler. Files and PHP pages have their own rules, see Static files and Handlers. What is compressed and how is in Reverse proxy.

challenge

challenge
challenge overload
challenge always

Puts xTQ Web's browser check in front of the block and every route inside it. A visitor without a valid pass gets a small page that solves a proof-of-work puzzle in the browser, receives a pass cookie and reloads. Clients that don't run JavaScript never get past it.

Mode When the check is shown
challenge or challenge overload Only while the backend that would answer is under heavy load. For proxy, that is 75% of the pool's in-flight cap in use, or any request waiting for a slot, and it stays on until use drops below 50%. For php, while xTQ PHP reports overload. files, redirect, respond and abort are never checked in this mode
challenge always Every request without a pass, whatever the handler

One pass covers the whole site, every route with a challenge. It lasts 24 hours.

site shop.example.com {
    route /checkout/* {
        challenge
        proxy 127.0.0.1:3000
    }
    proxy 127.0.0.1:3000
}

Only a valid pass cookie lets a visitor through. It works over plain HTTP and HTTPS alike, and is marked Secure when the visitor came over HTTPS. Set XTQW_SECRET so passes stay valid across restarts and reloads. The full description, including thresholds and settings, is in Protection.

probes

probes off
probes on

Scanner probes, paths like /.env, /.git/config or /cgi-bin/..., get a short 404 before any route of the site is looked at. That is probes on, the default. probes off lets them through to the site's routes like any other path, for an app that really serves such a path.

site app.example.com {
    probes off
    proxy 127.0.0.1:8080
}

probes goes only directly in a site block and covers the whole site. In a route it is an error (probes goes directly inside a site block). Which paths count as probes is in Protection.