Skip to content

Runlight 0.1.2

Latest

Choose a tag to compare

@github-actions github-actions released this 12 Oct 15:34
· 25 commits to main since this release

Security

  • Requests to a service someone set up (the AI assistant's API, every HTTP mail service, and a mail webhook) follow redirects by hand, at most five, and check each one against link-local and cloud metadata addresses, so a redirect can no longer lead to a metadata service. A refused address answers address_refused. The public refuseMetadata helper is gone from the assistant in PHP, Python, Ruby, Go, Java, .NET, and Rust, since the fetch checks every hop itself.
  • A fetch pinned to the address Runlight checked never goes out unpinned: TypeScript gives Node's https its own agent, so an environment proxy cannot look the name up again, and PHP's CurlFetcher and the other languages' fetchers refuse a pin they cannot apply, a pin for another host, or a redirect to an unpinned host, and ignore environment proxies for pinned requests. The WordPress, Drupal, and Craft fetchers refuse a transport or proxy that would skip the pin, and follow redirects by hand.
  • The goal picker no longer gives the tracked site's tab a handle on the dashboard, which let any script on the site send the dashboard tab somewhere else. The dashboard opens the site cut off from itself, and the picker sends its choice to a relay page at pick-return under the dashboard's own address, in the fragment so it never reaches a server. The picker's ticket names that page and a nonce, signed, and the page hands the choice to the dashboard tab that made the nonce over a channel only that origin's pages share. A hub picking on a site whose own install is older asks for that install to be updated first.

Fixed

  • Connect works against a Runlight whose one site lists no domains, which counts any host: its consent page offers that site instead of asking to add one.
  • Waiting for another process creating or upgrading the tables has a limit. After 30 seconds a request answers 503 with the code tables_busy and Retry-After: 30, and a tracker hit waits at most 10 seconds for the tables and is then dropped, so requests no longer pile up behind a long index build. On Postgres, an index left unusable is rebuilt under its own name when the table prefix itself starts rl_.
  • A new SQLite file is readable and writable by its owner and group (0660), and a folder Runlight makes for it by its owner and group (0770), in every language and in the WordPress plugin, so a cron job or CLI command running as another user of the same group can still open it. PHP and the WordPress plugin made them 0600 and 0700, and the other languages left them to the umask.
  • Java's app mailer answers an empty via rather than null, Rust refuses mail and app_mailer set together as Go does, and an app mailer in Rust that returns runlight::Error::Mail has its error passed on as it is.
  • Running inside Craft with trustedHosts left at its default, which believes forwarded headers from anyone, the plugin works out each visitor's address itself, as the WordPress plugin does. A site that sets trustedHosts gets Craft's own answer.
  • The dashboard has words for mail_url, mail_port, busy, and tables_busy in every language, and the mail_service refusal says the service is not offered here.
  • The dashboard has words for embed_token, embed_origin, embed_host, and token_embed_only in every language, so a refused dashboard key says what to do in the dashboard's language.
  • The prompt to copy for a custom link domain in Settings, Custom domains covers every language and framework Runlight runs in, from Next.js and Express to Laravel, Django, Rails, Go, Spring Boot, ASP.NET Core, Phoenix, and axum, and says no code is needed for a standalone server.
  • The dashboard's Powered by Runlight link opens in a new tab everywhere, so it works inside a CMS's admin too.
  • Removing a saved mail service in Settings, Email service says why when it fails, and an app's mailer is shown with its sender's name.
  • A broken percent escape in a path, such as %E0, answers as an unknown goal, share, site, link domain, or short link (a 404) instead of an internal error.
  • On a link domain, /go/<name> is a 404 like any other path with no link of that domain's, in the standalone server of every language that has one too, which redirected it to any site's link of that name.
  • In Laravel and Symfony, the /go/{slug} route through HttpFoundation::handle raises the framework's own not-found for a short link nobody has, so the app's 404 page answers it, as it does behind ShortLinks.
  • .NET: MapRunlight() inside UseEndpoints on a Startup-style host answers link domains, which it left to the app before. On an endpoint builder that reaches no middleware at all it refuses to start and says to use UseRunlight().
  • Elixir sorts names the way TypeScript does with punctuation in them, such as a-b and a_b, and reads a whole number past 2^53 in a location database as its decimal text, as the other languages do.

Added

  • The WordPress plugin runs Runlight inside WordPress: activate it, choose that on its setup screen, and the site is counted, with the dashboard at /runlight on the site for anyone who can manage options, AI agents recorded on the server, and the hourly check on WordPress's cron. The numbers go in WordPress's own MySQL 8.0.20 or MariaDB 11.4 database, through $wpdb, in tables named after WordPress's table prefix such as wp_rl_events, unless tables named that way are already there and are not Runlight's, or else in a SQLite file in uploads, which each site in a network always uses. Deleting the plugin drops those tables by name, and on a network removes what it kept for every site. Countries can come from DB-IP's free data, downloaded once a month when turned on. Sending to a Runlight elsewhere is the other choice in its settings. The plugin carries the PHP package at vendor/runlight/runlight, as Composer lays it out, leaving out the parts WordPress never runs, so it passes the plugin directory's checks, and running it this way needs PHP 8.2 with the mbstring and openssl extensions and, without MySQL, sqlite3.
  • Running inside WordPress, the plugin works out each visitor's address itself and hands it to Runlight, with trustProxy off: the connection's address, unless that is this server or a private network, where the nearest public address in X-Forwarded-For counts, and Cloudflare's CF-Connecting-IP only from Cloudflare's own addresses. The runlight_client_ip filter can change it. It hands Runlight the request without the slashes WordPress adds to $_SERVER. It offers no SMTP server in Settings, Email service, since an SMTP plugin covers it and Runlight sends through that plugin. It keeps rate counts and the site's icon in WordPress's object cache, or else in transients, where a minute's counts and the day's key take one each, so nothing piles up in wp_options even with WordPress's cron off. It downloads, unpacks, and reads the location data through WordPress's HTTP and filesystem APIs. With plain permalinks it answers at /index.php/runlight, which works on every server, and its settings page says so, and says when the hourly check has not run for a day. When wp-admin is served from another address than the site, its Runlight page opens the dashboard in a new tab, since only the site's own address may frame it.
  • The Drupal module and the Craft plugin run Runlight inside the CMS too: choose that on their setup screen, and the site is counted, with the dashboard at /runlight on the site and under Reports, Runlight in Drupal or the Runlight item in Craft's Control Panel, and short links at /go/<name>. The numbers go in the CMS's own Postgres, MySQL 8.0.20, or MariaDB 11.4 database, in tables named after its table prefix such as craft_rl_events and marked as that site's, unless tables named that way are already there and are not its own, or else in a SQLite file in Drupal's private files or Craft's storage, never in public files. Uninstalling drops those tables by name, and only that site's, even when Craft's storage folder is gone. Drupal opens Runlight's connection through its own database driver, so TLS options and init_commands apply, and Craft reads the choice from the database, so it holds on every server, and derives Runlight's secret from its security key. Drupal's Administer Runlight permission owns Runlight and a new View Runlight permission reads it; in Craft, admins and a new Manage Runlight permission own it and See Runlight's numbers reads it. Changes to Runlight are taken only from the site's own pages. Each visitor is counted by the address the CMS works out (Drupal's reverse_proxy settings, Craft's trustedHosts and ipHeaders), with trustProxy off, rate counts go in the CMS's cache, notes go to its log, and no environment variable is read. Drupal's cron, or Craft's queue or php craft runlight/check, runs the hourly check, and the settings say when it has not run for a day. Both need PHP 8.2, and Composer installs the PHP package with them.
  • Drupal keeps where Runlight runs, its address, and both keys in state rather than exported configuration, with settings.php overrides still winning, and Craft's Connect writes the keys into .env and saves the settings as $RUNLIGHT_AGENTS_KEY and $RUNLIGHT_DASHBOARD_KEY, so no key reaches a repository. Connect in every plugin shows why a Runlight could not be reached, and treats an address with its default port, such as https://stats.example.com:443, as the same address.
  • The WordPress, Drupal, and Craft plugins connect to a Runlight with a Connect button: enter its address, allow the connection on its consent page, and the plugin saves the site's id, agents key, and dashboard key itself. This works through a new cms OAuth scope, whose token is the site's dashboard key and whose answer also carries the site's agents key. When no site counts the plugin's domain, the consent page offers to add one, where sites are managed in the dashboard.
  • A cms option on the routes names the CMS that adds the script, such as WordPress, when Runlight runs inside one. The dashboard then opens on its numbers even before the first visit, in place of the welcome with the steps to add the script, Settings, Install says the plugin adds it, and pages on the same origin, the CMS's admin, may show the whole dashboard in a frame. Framed there, every button that opens a modal, sheet, or drawer, such as settings, the assistant, live visitors, a box's full list, map, or journeys, and a link's or goal's report, opens the dashboard in a new tab with it open, on the same site, range, and filters, where there is more room. Filters still open in the frame.
  • Settings, Install has a WordPress, Drupal, and Craft section listing every setting the plugins ask for, under the names they use, each with a copy button.
  • mail can be a mailer of the app's own, { send, from, fromName, via } (in PHP, send is a callable taking the message array), such as a CMS's. Unlike a service in code, it is a default: Settings, Email service says reports are sent through it, with a button to use another service instead, and a service saved there takes its place until it is removed. Reports, test emails, invites, and sign-in links go through it, and GET /api/mail answers source: "app" with an app object (via, from, fromName, replaced) whenever the app has one. Every language takes it. Go and Rust take it as a field of its own, AppMailer and app_mailer, and Ruby and Elixir name the sender from_name.
  • Running inside WordPress, Drupal, or Craft, Runlight sends email through the CMS's own mailer when one is set up: with wp_mail when an SMTP or mail plugin such as WP Mail SMTP or FluentSMTP is active, through Drupal's mail manager when its mailer is not plain PHP mail(), or through Craft's mailer when its transport is not Sendmail. Without one, the dashboard asks for a mail service as before.
  • A mailServices option lists the mail services Settings offers and saves, such as every one but SMTP; GET /api/mail lists only those, saving another is refused with mail_service, and a service saved earlier that is not in the list counts as none, so the app's mailer sends instead. Every service by default. Every language takes it, as mail_services in Python, Ruby, Elixir, and Rust and MailServices in Go and .NET.
  • PHP: a cache option takes a Runlight\Cache of the app's own for the small values requests share (rate counts and their daily key, the site's icon, and the once-a-day proxy warning) in place of APCu and files, and Server\DbIp takes a DbIpFiles that works its folder, so a CMS can keep both in its own storage. PHP's file functions and curl, the defaults, moved to Scratch, MmdbFile, and Server\DbIpDisk, and SMTP's client is loaded only for SMTP.
  • Every store, in every language, takes a table prefix in place of rl_ (prefix, or the language's spelling: Go's Prefixed() and NewStoreWithPrefix(), Rust's *_with_prefix() functions, and Java's prefix overloads), so two apps sharing a database each keep their own tables, and a store's tables() lists them by name. The prefix is up to 32 letters, digits, and underscores, not starting with a digit.
  • PHP: a store takes a table prefix in place of rl_, as prefix in the options of Stores::sqlite(), postgres(), and mysql(), or the second argument of new SqlStore() and Stores::fromDb(), so two apps sharing a database each keep their own tables, as the WordPress plugin does with WordPress's prefix. Every table and index name, schema check, and upgrade follows it, and SqlStore::tables() lists the tables by name for an app removing Runlight. Without one, every name is rl_ as before.
  • PHP: Runlight\Cms\Connect is the half of a CMS plugin's Connect button that every plugin shares (the OAuth metadata, the endpoint check, registration, PKCE, the consent page's address, and the token swap), given the CMS's own HTTP client as a Fetcher and its words for each outcome, and Runlight\Cms\Frame is the script for the dashboard's frame on an admin page, told the admin's bars by data attributes. The WordPress, Drupal, and Craft plugins use both.
  • PHP: Runlight\Log::to() sends Runlight's notes, such as a report that could not be sent, somewhere of the app's own in place of PHP's error log, and Request::fromServer() reads a request from an array shaped like $_SERVER, such as a CMS's unslashed copy of it. Request::fromGlobals() reads the superglobals as before.

Changed

  • A short link nobody has on the app's own host is the app's to answer, in every language: middleware hands the request on to the app, and a handler the app routes the link path to answers with the framework's own not found where there is one (chi's NotFound handler, Echo's ErrNotFound, Phoenix's NoRouteError from Runlight.Plug.Links, and a bare 404 from ASP.NET Core's endpoint, for the app's status code pages), so the app's own 404 page shows. A front controller and the standalone server, with no app behind them, still answer the 404 themselves.
  • Go: runlightchi.Mount after the app's own routes panics with the way to fix it, since chi takes no middleware after a route and a link domain's links then answered only on paths the app does not route. runlightchi.MountRoutes adds everything but the link domains, for an app that wraps its router with rl.LinkDomains. LinksHTTP answers link domains first, and LinksHTTPOr hands a slug no link has to a handler of the app's.
  • The key for CMS plugins is now called the agents key, and the key for the dashboard in a CMS the dashboard key, in Runlight and in every plugin. The plugins' Observe key field is now Agents key.
  • Runlight's tables need MySQL 8.0.20 or later, rather than 8.4, in every language and in the WordPress, Drupal, and Craft plugins, and CI runs the tests on MySQL 8.0 as well. MariaDB still needs 11.4.
  • The dashboard key's help says that whoever holds it can read the site's dashboard, so it is kept like a password.
  • Ignoring your own visits moved from Settings, Install to Settings, General.
  • The WordPress plugin's Runlight menu item sits below the core admin items instead of near the top, as the plugin directory asks.
  • In the WordPress, Drupal, and Craft admin, the dashboard's frame grows to the dashboard's full height, so the admin page scrolls like any other instead of the dashboard scrolling inside a frame the size of the window. The dashboard tells the page its height, and the page tells it which part of the frame is on screen, so the filter drawer and other sheets open over that part. A plugin keeps fitting the frame to the window for a Runlight too old to say its height.