Mobius
Mobius is a cross-platform command line Hotline server implemented in Golang.
- Project Goal: Make it simple to run a Hotline server on macOS, Linux, and Windows, with full compatibility for all popular Hotline clients.
Installation
Mobius is distributed through a single binary.
Depending on your platform and preferences, you can acquire the binary in the following ways:
Build from source
- Install Go if needed
- Run
make server
Download pre-built release
See Releases page.
Docker
To run a Hotline server with a default, sample configuration with ports forwarded from the host OS to the container:
docker run --rm -p 5500:5500 -p 5501:5501 ghcr.io/jhalter/mobius-hotline-server:latest
You can now connect to localhost:5500 with your favorite Hotline client and play around, but all changes will be lost on container restart.
To serve files from the host OS and persist configuration changes, you'll want to set up a bind mount that maps a directory from the host into the container.
To do this, create a directory in a location of your choice on the host OS. For clarity, we'll assign the path to the HLFILES environment variable and re-use it.
Then run the docker command with -v and -init like so:
export HLFILES=/home/myuser/hotline-files
mdkir $HLFILES
sudo docker run \
--pull=always \
--rm \
-p 5500:5500 \
-p 5501:5501 \
-v $HLFILES:/usr/local/var/mobius/config \
ghcr.io/jhalter/mobius-hotline-server:latest \
-init
It's a good security practice to run your server as a non-root user, which also happens to make editing the configuration files easier from the host OS.
To do this, add the --user flag to the docker run arguments with a user ID and group ID of a user on the host OS.
--user 1001:1001
Homebrew
For macOS the easiest path to installation is through Homebrew, as this works around Apple's notarization requirements for downloaded pre-compiled binaries by compiling the binary on your system during brew installation.
To install the server:
brew install jhalter/mobius-hotline-server/mobius-hotline-server
After installation mobius-hotline-server will be installed at $HOMEBREW_PREFIX/bin/mobius-hotline-server and should be in your $PATH.
The server config directory will be created under $HOMEBREW_PREFIX/var/mobius.
To start the service:
brew services start mobius-hotline-server
Server Configuration
After you have a server binary, the next step is to configure the server.
Configuration directory
Mobius stores its configuration and server state in a directory tree:
config
├── Agreement.txt
├── Files
│ └── hello.txt
├── MessageBoard.txt
├── ThreadedNews.yaml
├── Users
│ ├── admin.yaml
│ └── guest.yaml
├── banner.jpg
└── config.yaml
If you acquired the server binary by downloading it or compiling it, this directory doesn't exist yet! But you can generate it by running the the server with the -init flag:
./mobius-hotline-server -init -config example-config-dir
Brew users can find the config directory in $HOMEBREW_PREFIX/var/mobius.
Within this directory some files are intended to be edited to customize the server, while others are not.
- 🛠️ Edit this file to customize your server.
- ⚠️ Avoid manual edits outside of special circumstances (e.g. remove offending news content).
🛠️ Agreement.text - The server agreement sent to users after they join the server.
🛠️ Files - Home of your warez or any other files you'd like to share.
⚠️ MessageBoard.txt - Plain text file containing the server's message board. No need to edit this.
⚠️ ThreadedNews.yaml - YAML file containing the server's threaded news. No need to edit this.
⚠️ Users - Directory containing user account YAML files. No need to edit this.
🛠️ banner.jpg - Path to server banner image.
🛠️ config.yaml - Edit to set your server name, description, and enable tracker registration.
Server banners
Existing configurations using BannerFile continue to serve one banner. To
choose a random banner for each client session, add BannerDirectory:
BannerFile: "banner.jpg" # Single banner, and fallback for directory mode
BannerDirectory: "banners" # Optional; relative to the configuration directoryCreate the directory and place JPEG or GIF banners inside it. Absolute directory
paths are also accepted. Banners are always read from the local filesystem,
independently of the file library's storage backend. The server loads readable,
nonempty .jpg, .jpeg, and .gif files (case-insensitively) from that directory's
immediate contents. Hidden files, subdirectories, and symlinks are skipped.
Keep each image within the existing 262,140-byte client limit; the standard banner
size is 468 × 60 pixels. Images are served as provided, without conversion or
additional image-content validation. Banner display is only known to work in
Hotline 1.8 clients.
When BannerDirectory is set, it takes precedence over BannerFile. Each session
gets an equally likely choice and keeps that image for repeat downloads. Different
sessions can receive the same image, and JPEG and GIF banners can be mixed. Images
are cached in memory, so directory size determines the pool's memory use.
If the directory is missing, unreadable, or contains no usable images, the server
logs a warning and uses BannerFile. BannerFile can be omitted in directory mode
if no fallback is wanted. If neither source can be loaded, startup fails; a failed
reload retains the previous working images.
After adding, removing, or replacing images, send SIGHUP or use
POST /api/v1/reload. New sessions use the refreshed pool; existing directory-mode
sessions retain their selected image, including a fallback image. Single-file
mode continues to use the refreshed banner for subsequent download requests.
Downloads already requested always finish with the bytes and size captured for
that transfer. Restart after changing BannerDirectory or BannerFile in
config.yaml. There is no automatic directory watching or timed rotation.
Feed-backed threaded news
An advanced NewsFeeds configuration can import RSS or Atom entries into an
existing ordinary news category, including Sparkle appcasts and GitHub release
feeds:
See Feed-backed threaded news for the complete administrator guide, including configuration rules, retention behavior, durable state, operational limits, and troubleshooting.
NewsFeeds:
- CategoryPath: ["Software Updates", "Afterglow"]
URL: "https://morphing.cloud/afterglow/appcast.xml"
- CategoryPath: ["Software Updates", "Mobius"]
URL: "https://github.com/jhalter/mobius/releases.atom"The full target path must already exist as an ordinary category. Whenever a
client opens it, Mobius conditionally checks the source and saves new entries
as ordinary root articles in ThreadedNews.yaml. Existing entries are never
rewritten, and deleted imports stay deleted. Feed failures are logged while the
current local articles remain available. RSS and Atom are detected
automatically; changes to NewsFeeds require restarting the server.
User accounts
The default installation includes two users:
- guest (no password)
- admin (default password admin).
User administration should be performed from a Hotline client. Avoid editing the files under the Users directory.
The FileRoot field in an account YAML file is the exception when assigning that account a
separate file library. An empty value inherits the global FileRoot. With the default os file
store, a relative value is resolved from the config directory and an absolute value is used as-is.
With the memory or r2 file store, a relative value remains a path in the store's namespace.
Restart the server after changing this field manually.
Run the server
By default running mobius-hotline-server will listen on ports 5500/5501 of all interfaces with info level logging to STDOUT.
Use the -h or -help flag for a list of options:
$ mobius-hotline-server -h
Usage of mobius-hotline-server:
-bind int
Base Hotline server port. File transfer port is base port + 1. (default 5500)
-config string
Path to config root (default "/usr/local/var/mobius/config/")
-init
Populate the config dir with default configuration
-interface string
IP addr of interface to listen on. Defaults to all interfaces.
-log-file string
Path to log file
-log-level string
Log level (default "info")
-stats-port string
Enable stats HTTP endpoint on address and port
-version
Print version and exit
To run as a systemd service, refer to this sample unit file: mobius-hotline-server.service
Connection logs
Hotline control connections include remote_addr on their connection, authentication,
and request events. Use remote_addr (IP and source port) to correlate events for a connection.
Each connection ends with a Connection closed
summary containing its last stage, handshake_ok, authenticated, requests_received,
requests_handled, duration_ms, and reason. For example, a client that completes the
handshake and disconnects without sending a login produces an INFO summary like:
msg="Connection closed" remote_addr=192.0.2.1:52256 stage=login handshake_ok=true authenticated=false requests_received=0 requests_handled=0 duration_ms=12 reason=peer_closedTruncated transactions are WARN events with buffered_bytes and expected_bytes.
Oversized or malformed transactions have distinct reasons; resets and timeouts retain the
underlying error. Payloads and passwords are not included in these diagnostics.
Authentication succeeded is emitted once when credentials verify. Agreement acceptance is
logged separately as Agreement accepted; authentication alone does not mean a client has
completed session setup or issued any requests.
After login, each request is logged with its operation as the message (for example,
msg="Get user list"), plus transaction_id and transaction_type. This event records receipt
before the handler runs. Panics and unsupported transactions are logged as Request failed
and Request rejected. requests_received counts decoded
post-login transactions, including keepalives and unsupported requests. requests_handled
counts handlers that returned normally, including those generating error replies. Routine
keepalive events are suppressed, but included in these counters.
Neither a request log nor the requests_handled count proves response delivery. The asynchronous
writer logs Transaction write failed with the transaction ID if a socket write fails.
Unexpected write failures are WARN events. Closed-connection errors caused by deliberate
disconnects or server shutdown are DEBUG events.
File Storage
By default the file library is stored on the server's local disk. The -file-store flag selects an alternate backend:
os(default) — local filesystemmemory— in-memory, non-persistent (for testing)r2— Cloudflare R2 object storage
See docs/r2-file-store.md for R2 setup instructions.
HTTP API
The Mobius server includes an optional HTTP API for server administration and user management.
Configuration
To enable the API, use the --api-addr flag with an IP and port:
mobius-hotline-server --api-addr=127.0.0.1:5503Authentication
The API supports optional authentication via API key. Set the --api-key flag:
mobius-hotline-server --api-addr=127.0.0.1:5503 --api-key=your-secret-keyInclude the API key in the X-API-Key header:
curl -H "X-API-Key: your-secret-key" localhost:5503/api/v1/statsAPI Documentation
Complete API documentation is available in the OpenAPI specification.
Endpoints
User Management
GET /api/v1/online- List currently online usersPOST /api/v1/ban- Ban a user by username, nickname, or IPPOST /api/v1/unban- Remove a banGET /api/v1/banned/ips- List banned IP addressesGET /api/v1/banned/usernames- List banned usernamesGET /api/v1/banned/nicknames- List banned nicknames
Server Administration
GET /api/v1/stats- Get server statisticsPOST /api/v1/reload- Reload configurationPOST /api/v1/shutdown- Shutdown server
Examples
Get online users:
curl localhost:5503/api/v1/onlineBan a user:
curl -X POST -H "Content-Type: application/json" \
-d '{"username":"baduser"}' \
localhost:5503/api/v1/banGet server stats:
curl localhost:5503/api/v1/stats | jq .Shutdown server:
curl -X POST -d 'Server maintenance' localhost:5503/api/v1/shutdown