redesign: light project site + live network status from a lusers probe
This commit is contained in:
parent
182f14a7f2
commit
d1ec6962dd
10 changed files with 473 additions and 282 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -717,6 +717,7 @@ version = "1.53.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"libc",
|
||||
"mio",
|
||||
"pin-project-lite",
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ license = "MIT"
|
|||
|
||||
[dependencies]
|
||||
axum = "0.7"
|
||||
tokio = { version = "1", features = ["macros", "rt-multi-thread", "net", "signal"] }
|
||||
tokio = { version = "1", features = ["macros", "rt-multi-thread", "net", "signal", "io-util", "time"] }
|
||||
askama = "0.12"
|
||||
tower-http = { version = "0.6", features = ["trace", "compression-gzip"] }
|
||||
tracing = "0.1"
|
||||
|
|
|
|||
38
src/main.rs
38
src/main.rs
|
|
@ -1,9 +1,11 @@
|
|||
//! The echoIRCd project website: a small Axum server that renders a handful of
|
||||
//! compile-time Askama templates. Templates and static assets are baked into the
|
||||
//! binary, so the deployed artifact is fully self-contained.
|
||||
//! The echoIRCd project website: an Axum server rendering compile-time Askama
|
||||
//! templates, with a small live "network status" pulled from the running ircd.
|
||||
|
||||
mod stats;
|
||||
|
||||
use askama::Template;
|
||||
use axum::{
|
||||
extract::State,
|
||||
http::{header, StatusCode},
|
||||
response::{Html, IntoResponse, Response},
|
||||
routing::get,
|
||||
|
|
@ -12,10 +14,27 @@ use axum::{
|
|||
use std::net::SocketAddr;
|
||||
use tower_http::{compression::CompressionLayer, trace::TraceLayer};
|
||||
|
||||
/// Display-ready snapshot of the live network state for templates.
|
||||
struct Status {
|
||||
online: bool,
|
||||
users: String,
|
||||
version: String,
|
||||
}
|
||||
|
||||
fn view(shared: &stats::Shared) -> Status {
|
||||
let s = shared.read().ok().map(|g| g.clone()).unwrap_or_default();
|
||||
Status {
|
||||
online: s.online,
|
||||
users: s.users.map(|n| n.to_string()).unwrap_or_else(|| "—".into()),
|
||||
version: s.version.unwrap_or_else(|| "5.0.0".into()),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "index.html")]
|
||||
struct IndexTemplate {
|
||||
active: &'static str,
|
||||
status: Status,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
|
|
@ -30,7 +49,6 @@ struct ConnectTemplate {
|
|||
active: &'static str,
|
||||
}
|
||||
|
||||
/// Render a template to an HTML response, or a 500 if rendering fails.
|
||||
fn page<T: Template>(t: T) -> Response {
|
||||
match t.render() {
|
||||
Ok(body) => Html(body).into_response(),
|
||||
|
|
@ -41,8 +59,8 @@ fn page<T: Template>(t: T) -> Response {
|
|||
}
|
||||
}
|
||||
|
||||
async fn index() -> Response {
|
||||
page(IndexTemplate { active: "home" })
|
||||
async fn index(State(shared): State<stats::Shared>) -> Response {
|
||||
page(IndexTemplate { active: "home", status: view(&shared) })
|
||||
}
|
||||
async fn features() -> Response {
|
||||
page(FeaturesTemplate { active: "features" })
|
||||
|
|
@ -66,7 +84,6 @@ async fn favicon() -> impl IntoResponse {
|
|||
async fn health() -> &'static str {
|
||||
"ok"
|
||||
}
|
||||
|
||||
async fn not_found() -> Response {
|
||||
(
|
||||
StatusCode::NOT_FOUND,
|
||||
|
|
@ -84,6 +101,9 @@ async fn main() {
|
|||
)
|
||||
.init();
|
||||
|
||||
let shared: stats::Shared = Default::default();
|
||||
stats::spawn_updater(shared.clone());
|
||||
|
||||
let app = Router::new()
|
||||
.route("/", get(index))
|
||||
.route("/features", get(features))
|
||||
|
|
@ -93,7 +113,8 @@ async fn main() {
|
|||
.route("/health", get(health))
|
||||
.fallback(not_found)
|
||||
.layer(CompressionLayer::new())
|
||||
.layer(TraceLayer::new_for_http());
|
||||
.layer(TraceLayer::new_for_http())
|
||||
.with_state(shared);
|
||||
|
||||
let addr: SocketAddr = std::env::var("ECHO_WEB_ADDR")
|
||||
.unwrap_or_else(|_| "127.0.0.1:8099".into())
|
||||
|
|
@ -111,7 +132,6 @@ async fn main() {
|
|||
.expect("server error");
|
||||
}
|
||||
|
||||
/// Resolve on SIGINT (Ctrl-C) or SIGTERM (systemd stop) for a clean shutdown.
|
||||
async fn shutdown() {
|
||||
use tokio::signal::unix::{signal, SignalKind};
|
||||
let mut term = signal(SignalKind::terminate()).expect("install SIGTERM handler");
|
||||
|
|
|
|||
101
src/stats.rs
Normal file
101
src/stats.rs
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
//! Live network status, polled from the running ircd. A background task connects
|
||||
//! to the local daemon as an ordinary client every minute, runs LUSERS/VERSION,
|
||||
//! parses the reply, and stores it. If the daemon is unreachable the state simply
|
||||
//! reads "offline" — it never blocks or breaks page rendering.
|
||||
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::time::Duration;
|
||||
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::time::{sleep, timeout};
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct NetStats {
|
||||
pub online: bool,
|
||||
pub users: Option<u64>,
|
||||
pub version: Option<String>,
|
||||
}
|
||||
|
||||
pub type Shared = Arc<RwLock<NetStats>>;
|
||||
|
||||
/// Spawn the background poller. Refreshes every 60s.
|
||||
pub fn spawn_updater(shared: Shared) {
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
let next = match timeout(Duration::from_secs(7), probe()).await {
|
||||
Ok(Some(s)) => s,
|
||||
_ => NetStats::default(),
|
||||
};
|
||||
if let Ok(mut g) = shared.write() {
|
||||
*g = next;
|
||||
}
|
||||
sleep(Duration::from_secs(60)).await;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async fn probe() -> Option<NetStats> {
|
||||
let addr = std::env::var("ECHO_IRCD_ADDR").unwrap_or_else(|_| "127.0.0.1:6667".into());
|
||||
let stream = TcpStream::connect(&addr).await.ok()?;
|
||||
let (rd, mut wr) = stream.into_split();
|
||||
wr.write_all(b"NICK echostat\r\nUSER echostat 0 * :echo website status\r\n")
|
||||
.await
|
||||
.ok()?;
|
||||
|
||||
let mut lines = BufReader::new(rd).lines();
|
||||
let (mut users, mut version, mut asked) = (None, None, false);
|
||||
|
||||
for _ in 0..100 {
|
||||
let line = match lines.next_line().await {
|
||||
Ok(Some(l)) => l,
|
||||
_ => break,
|
||||
};
|
||||
let line = line.trim_end_matches('\r');
|
||||
if let Some(rest) = line.strip_prefix("PING ") {
|
||||
let _ = wr.write_all(format!("PONG {rest}\r\n").as_bytes()).await;
|
||||
continue;
|
||||
}
|
||||
match numeric(line) {
|
||||
Some("001") if !asked => {
|
||||
let _ = wr.write_all(b"LUSERS\r\nVERSION\r\n").await;
|
||||
asked = true;
|
||||
}
|
||||
Some("251") => users = users.or_else(|| parse_users(line)),
|
||||
Some("351") => version = version.or_else(|| parse_version(line)),
|
||||
_ => {}
|
||||
}
|
||||
if users.is_some() && version.is_some() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let _ = wr.write_all(b"QUIT :bye\r\n").await;
|
||||
Some(NetStats { online: true, users, version })
|
||||
}
|
||||
|
||||
/// The three-digit numeric of an IRC line (`:src 251 nick …`), if any.
|
||||
fn numeric(line: &str) -> Option<&str> {
|
||||
let mut it = line.split(' ');
|
||||
let first = it.next()?;
|
||||
let code = if first.starts_with(':') { it.next()? } else { first };
|
||||
(code.len() == 3 && code.bytes().all(|b| b.is_ascii_digit())).then_some(code)
|
||||
}
|
||||
|
||||
/// RPL_LUSERCLIENT trailing: "There are 5 users on 1 server" → first number.
|
||||
fn parse_users(line: &str) -> Option<u64> {
|
||||
line.splitn(2, " :")
|
||||
.nth(1)?
|
||||
.split_whitespace()
|
||||
.find_map(|w| w.parse::<u64>().ok())
|
||||
}
|
||||
|
||||
/// RPL_VERSION trailing: "echoircd 5.0.0 · …" → "5.0.0".
|
||||
fn parse_version(line: &str) -> Option<String> {
|
||||
let tail = line.splitn(2, " :").nth(1)?;
|
||||
let mut it = tail.split_whitespace();
|
||||
let a = it.next()?;
|
||||
if a.eq_ignore_ascii_case("echoircd") {
|
||||
it.next().map(|v| v.to_string())
|
||||
} else {
|
||||
Some(a.to_string())
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<rect width="64" height="64" rx="13" fill="#0d0e12"/>
|
||||
<text x="13" y="45" font-family="ui-monospace,monospace" font-size="33" font-weight="700" fill="#e2a25a">›</text>
|
||||
<rect x="37" y="27" width="13" height="20" rx="1" fill="#c8ccd4"/>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<rect width="32" height="32" rx="7" fill="#12a594"/>
|
||||
<circle cx="10" cy="16" r="2.8" fill="#fff"/>
|
||||
<path d="M15.5 10a8 8 0 0 1 0 12" fill="none" stroke="#fff" stroke-width="2.2" stroke-linecap="round"/>
|
||||
<path d="M19.5 6.5a13 13 0 0 1 0 19" fill="none" stroke="#eafffb" stroke-width="2.2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 315 B After Width: | Height: | Size: 389 B |
245
static/style.css
245
static/style.css
|
|
@ -1,126 +1,141 @@
|
|||
/* echoIRCd — the site as an IRC client. Monospace, warm phosphor, real IRC grammar. */
|
||||
/* echoIRCd — a friendly, light project site. Teal is echo's own colour. */
|
||||
:root{
|
||||
--bg:#08090c; --win:#0d0e12; --panel:#14161c; --panel2:#0f1116;
|
||||
--edge:#22252e; --edge2:#2d313c;
|
||||
--fg:#c8ccd4; --dim:#767b85; --dimmer:#4a4e58;
|
||||
--amber:#e2a25a; --green:#5fbf6b; --blue:#5f9bef; --magenta:#c58af0; --cyan:#4fcccf; --red:#ec6d6d;
|
||||
--link:#6aa6f2; --ok:#5fbf6b;
|
||||
--mono:ui-monospace,"SF Mono","JetBrains Mono","Cascadia Code",Menlo,Consolas,"DejaVu Sans Mono",monospace;
|
||||
--teal:#12a594; --teal-d:#0e8577; --teal-dd:#0a6a5f; --teal-t:#e7f6f3;
|
||||
--ink:#1b2733; --muted:#5c6773; --faint:#8b95a0;
|
||||
--line:#e4e8eb; --line2:#d8dee2; --bg:#f5f6f7; --card:#ffffff;
|
||||
--code-bg:#132430; --code-fg:#e7edf0; --cmd:#6fe0cf;
|
||||
--amber:#e8a13c; --green:#22b573; --red:#e0604f;
|
||||
--sans:"Rubik",system-ui,-apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
|
||||
--mono:ui-monospace,"SF Mono",Menlo,Consolas,"DejaVu Sans Mono",monospace;
|
||||
--wrap:1080px;
|
||||
}
|
||||
*{box-sizing:border-box}
|
||||
html{-webkit-text-size-adjust:100%}
|
||||
body{
|
||||
margin:0;background:var(--bg);color:var(--fg);
|
||||
font:14.5px/1.65 var(--mono);
|
||||
background-image:radial-gradient(1200px 600px at 50% -10%,rgba(226,162,90,.05),transparent 70%);
|
||||
margin:0;color:var(--ink);font-family:var(--sans);font-size:16px;line-height:1.65;
|
||||
background-color:var(--bg);
|
||||
background-image:radial-gradient(rgba(18,165,148,.06) 1px,transparent 1px);
|
||||
background-size:24px 24px;background-position:-12px -12px;
|
||||
-webkit-font-smoothing:antialiased;
|
||||
}
|
||||
a{color:var(--link);text-decoration:none}
|
||||
h1,h2,h3{line-height:1.15;margin:0 0 .5rem;color:var(--ink);font-weight:700;letter-spacing:-.01em}
|
||||
p{margin:0 0 1rem}
|
||||
a{color:var(--teal-d);text-decoration:none}
|
||||
a:hover{text-decoration:underline}
|
||||
b{color:var(--fg);font-weight:600}
|
||||
.dim{color:var(--dim)}
|
||||
.kw{color:var(--amber)}
|
||||
.ok{color:var(--ok)}
|
||||
.i{font-style:italic}
|
||||
.n-amber{color:var(--amber)} .n-green{color:var(--green)} .n-blue{color:var(--blue)}
|
||||
.n-magenta{color:var(--magenta)} .n-cyan{color:var(--cyan)} .n-red{color:var(--red)}
|
||||
code{font-family:var(--mono);font-size:.9em;background:rgba(18,165,148,.09);color:var(--teal-dd);padding:.08em .38em;border-radius:5px}
|
||||
.wrap{max-width:var(--wrap);margin:0 auto;padding:0 1.25rem}
|
||||
.mono-line{font-family:var(--mono);color:var(--muted)}
|
||||
|
||||
/* the client window */
|
||||
.client{
|
||||
max-width:1060px;margin:1.6rem auto;background:var(--win);
|
||||
border:1px solid var(--edge);border-radius:9px;overflow:hidden;
|
||||
box-shadow:0 24px 60px -30px rgba(0,0,0,.8),0 0 0 1px rgba(255,255,255,.01);
|
||||
/* buttons */
|
||||
.btn{display:inline-block;font-weight:500;font-size:.98rem;padding:.62rem 1.15rem;border-radius:9px;border:1.5px solid transparent;transition:transform .05s ease}
|
||||
.btn:hover{text-decoration:none;transform:translateY(-1px)}
|
||||
.btn-primary{background:var(--teal);color:#fff;box-shadow:0 8px 18px -8px rgba(18,165,148,.7)}
|
||||
.btn-primary:hover{background:var(--teal-d)}
|
||||
.btn-ghost{border-color:var(--line2);color:var(--ink);background:#fff}
|
||||
.btn-ghost:hover{border-color:var(--teal);color:var(--teal-d)}
|
||||
|
||||
/* top bar */
|
||||
.topbar{background:var(--teal);color:#fff;position:sticky;top:0;z-index:20;box-shadow:0 2px 12px -6px rgba(0,0,0,.35)}
|
||||
.bar-in{max-width:var(--wrap);margin:0 auto;padding:.7rem 1.25rem;display:flex;align-items:center;gap:1.5rem;flex-wrap:wrap}
|
||||
.brand{display:flex;align-items:center;gap:.55rem;color:#fff;font-weight:700;font-size:1.25rem}
|
||||
.brand:hover{text-decoration:none;opacity:.95}
|
||||
.mark{width:26px;height:26px;color:#fff;flex:none}
|
||||
.mainnav{margin-left:auto;display:flex;gap:1.4rem;align-items:center;flex-wrap:wrap}
|
||||
.mainnav a{color:#eafffb;font-weight:500;font-size:.96rem;opacity:.9;padding:.15rem 0;border-bottom:2px solid transparent}
|
||||
.mainnav a:hover{opacity:1;text-decoration:none}
|
||||
.mainnav a.on{opacity:1;border-bottom-color:#fff}
|
||||
.mainnav a.ext{opacity:.8}
|
||||
|
||||
/* hero */
|
||||
.hero{padding:3.4rem 0 2.6rem}
|
||||
.hero-in{max-width:var(--wrap);margin:0 auto;padding:0 1.25rem;display:grid;grid-template-columns:1.4fr 1fr;gap:2.5rem;align-items:center}
|
||||
.eyebrow{color:var(--teal-d);font-weight:600;font-size:.82rem;text-transform:uppercase;letter-spacing:.14em;margin:0 0 .8rem}
|
||||
.hero h1{font-size:clamp(2.4rem,5.4vw,3.7rem);margin-bottom:1rem}
|
||||
.hero-sub{color:var(--muted);font-size:1.12rem;max-width:34rem;margin-bottom:1.6rem}
|
||||
.cta{display:flex;gap:.7rem;flex-wrap:wrap}
|
||||
|
||||
/* status card */
|
||||
.statuscard{background:var(--card);border:1px solid var(--line);border-radius:14px;padding:1.2rem 1.3rem;box-shadow:0 20px 40px -28px rgba(20,40,60,.4)}
|
||||
.sc-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:.7rem}
|
||||
.sc-title{font-size:.72rem;text-transform:uppercase;letter-spacing:.15em;color:var(--faint);font-weight:600}
|
||||
.dot{width:9px;height:9px;border-radius:50%}
|
||||
.dot.on{background:var(--green);box-shadow:0 0 0 0 rgba(34,181,115,.6);animation:pulse 2s infinite}
|
||||
.dot.off{background:var(--faint)}
|
||||
@keyframes pulse{0%{box-shadow:0 0 0 0 rgba(34,181,115,.5)}70%{box-shadow:0 0 0 7px rgba(34,181,115,0)}100%{box-shadow:0 0 0 0 rgba(34,181,115,0)}}
|
||||
.sc-rows{margin:0}
|
||||
.sc-rows>div{display:flex;justify-content:space-between;align-items:baseline;padding:.42rem 0;border-bottom:1px dashed var(--line)}
|
||||
.sc-rows>div:last-child{border-bottom:0}
|
||||
.sc-rows dt{color:var(--muted);font-size:.9rem}
|
||||
.sc-rows dd{margin:0;font-weight:500;font-family:var(--mono);font-size:.9rem;color:var(--ink)}
|
||||
.sc-rows .live{color:var(--green)} .sc-rows .down{color:var(--red)}
|
||||
.sc-foot{margin-top:.7rem;font-size:.78rem;color:var(--faint)}
|
||||
|
||||
/* feature band */
|
||||
.band{background:linear-gradient(180deg,#fff,#fbfcfc);border-top:1px solid var(--line);border-bottom:1px solid var(--line);padding:3.2rem 0}
|
||||
.h-center{text-align:center}
|
||||
.band h2{font-size:1.9rem;margin-bottom:2rem}
|
||||
.cards{display:grid;grid-template-columns:repeat(3,1fr);gap:1.1rem}
|
||||
.fcard{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:1.3rem 1.4rem;transition:border-color .15s,box-shadow .15s}
|
||||
.fcard:hover{border-color:var(--teal);box-shadow:0 16px 30px -24px rgba(18,165,148,.6)}
|
||||
.fcard h3{font-size:1.12rem;color:var(--teal-dd)}
|
||||
.fcard p{margin:0;color:var(--muted);font-size:.95rem}
|
||||
.more{margin-top:1.8rem}.more a{font-weight:600}
|
||||
|
||||
/* latest / news */
|
||||
.latest{padding:3rem 1.25rem}
|
||||
.latest h2{font-size:1.6rem;margin-bottom:1.2rem}
|
||||
.news{list-style:none;margin:0;padding:0}
|
||||
.news li{display:flex;gap:1.2rem;padding:1rem 0;border-top:1px solid var(--line)}
|
||||
.news li:last-child{border-bottom:1px solid var(--line)}
|
||||
.news-date{flex:none;width:6.5rem;color:var(--faint);font-size:.85rem;font-family:var(--mono);padding-top:.15rem}
|
||||
.news div{color:var(--muted)}
|
||||
|
||||
/* cta band */
|
||||
.ctaband{background:var(--teal-t);border-top:1px solid #cdeae4}
|
||||
.ctaband-in{display:flex;align-items:center;justify-content:space-between;gap:1.5rem;padding:2.3rem 1.25rem;flex-wrap:wrap}
|
||||
.ctaband h2{color:var(--teal-dd);margin-bottom:.2rem;font-size:1.5rem}
|
||||
.ctaband .mono-line{color:var(--teal-d)}
|
||||
|
||||
/* page head + doc pages */
|
||||
.pagehead{background:linear-gradient(180deg,#fff,#fbfcfc);border-bottom:1px solid var(--line);padding:2.6rem 0 2rem}
|
||||
.pagehead h1{font-size:2.3rem}
|
||||
.pagehead p{color:var(--muted);margin:0;max-width:44rem}
|
||||
.doc{padding:2.4rem 1.25rem}
|
||||
.doc-block{margin:0 0 2.2rem}
|
||||
.doc-block h2{font-size:1.3rem;color:var(--ink);padding-bottom:.5rem;border-bottom:2px solid var(--teal-t);margin-bottom:.9rem}
|
||||
.doc-block ul{margin:.3rem 0;padding-left:1.2rem}
|
||||
.doc-block li{margin:.4rem 0;color:var(--muted)}
|
||||
.doc-block li b{color:var(--ink)}
|
||||
.chips{display:flex;flex-wrap:wrap;gap:.4rem;margin-top:.6rem}
|
||||
.chips span{font-family:var(--mono);font-size:.8rem;background:#fff;border:1px solid var(--line2);color:var(--muted);border-radius:6px;padding:.22rem .5rem}
|
||||
.detail{border-collapse:collapse;width:100%;max-width:34rem;margin:.4rem 0 1rem}
|
||||
.detail th{text-align:left;font-weight:500;color:var(--muted);padding:.5rem .9rem .5rem 0;border-bottom:1px solid var(--line);white-space:nowrap;width:1%;vertical-align:top}
|
||||
.detail td{padding:.5rem 0;border-bottom:1px solid var(--line)}
|
||||
.note{color:var(--faint);font-size:.9rem}
|
||||
.webchat{margin:.6rem 0 0}
|
||||
.code{background:var(--code-bg);color:var(--code-fg);border-radius:10px;padding:.9rem 1.1rem;overflow-x:auto;
|
||||
font-family:var(--mono);font-size:.88rem;line-height:1.7;white-space:pre;margin:.5rem 0}
|
||||
|
||||
/* footer */
|
||||
.site-foot{background:#0f1a22;color:#aeb8c0;margin-top:1rem}
|
||||
.foot-in{max-width:var(--wrap);margin:0 auto;padding:2rem 1.25rem;display:flex;align-items:center;gap:1rem 1.5rem;flex-wrap:wrap}
|
||||
.foot-brand{display:flex;align-items:center;gap:.5rem;color:#fff;font-weight:700}
|
||||
.foot-brand .mark{width:22px;height:22px;color:var(--teal)}
|
||||
.foot-tag{margin:0;color:#8994a0;font-size:.9rem}
|
||||
.foot-tag code{background:rgba(255,255,255,.08);color:#cfe3dd}
|
||||
.foot-links{margin-left:auto;display:flex;gap:1.2rem;flex-wrap:wrap}
|
||||
.foot-links a{color:#aeb8c0;font-size:.9rem}
|
||||
.foot-links a:hover{color:#fff}
|
||||
|
||||
@media(max-width:820px){
|
||||
.hero-in{grid-template-columns:1fr;gap:1.6rem}
|
||||
.statuscard{max-width:26rem}
|
||||
.cards{grid-template-columns:1fr 1fr}
|
||||
}
|
||||
|
||||
/* title bar */
|
||||
.titlebar{display:flex;align-items:center;gap:.6rem;padding:.55rem .9rem;background:var(--panel);border-bottom:1px solid var(--edge)}
|
||||
.conn{width:9px;height:9px;border-radius:50%;background:var(--green);box-shadow:0 0 8px var(--green);flex:none}
|
||||
.tb-name{color:var(--fg);font-weight:600}
|
||||
.tb-server{margin-left:auto;color:var(--dim);font-size:.82rem}
|
||||
|
||||
/* tabs */
|
||||
.tabs{display:flex;gap:.15rem;padding:0 .5rem;background:var(--panel);border-bottom:1px solid var(--edge);overflow-x:auto}
|
||||
.tab{padding:.5rem .8rem;color:var(--dim);border-bottom:2px solid transparent;white-space:nowrap}
|
||||
.tab:hover{color:var(--fg);text-decoration:none}
|
||||
.tab.on{color:var(--amber);border-bottom-color:var(--amber)}
|
||||
.tab.ext{margin-left:auto;color:var(--dimmer)}
|
||||
|
||||
/* topic */
|
||||
.topic{padding:.5rem .95rem;background:var(--panel2);border-bottom:1px solid var(--edge);color:var(--dim);font-size:.85rem}
|
||||
.topic .tp{color:var(--amber);margin-right:.35rem}
|
||||
|
||||
/* workspace = buffer + nicklist */
|
||||
.workspace{display:flex;align-items:stretch}
|
||||
.buffer{flex:1;min-width:0;padding:1.1rem 1.1rem 1.4rem}
|
||||
.nicklist{width:13.5rem;flex:none;background:var(--panel2);border-left:1px solid var(--edge);padding:1rem .85rem;font-size:.86rem}
|
||||
.nl-head{color:var(--dim);text-transform:uppercase;letter-spacing:.12em;font-size:.72rem;margin-bottom:.6rem}
|
||||
.nicklist ul{list-style:none;margin:0;padding:0}
|
||||
.nicklist li{display:flex;align-items:center;gap:.15rem;padding:.14rem 0;white-space:nowrap}
|
||||
.nicklist .s{display:inline-block;width:1.1em;text-align:center;color:var(--dim)}
|
||||
.bf{margin-left:.35rem;font-size:.62rem;color:var(--dimmer);border:1px solid var(--edge2);border-radius:3px;padding:0 .25em}
|
||||
|
||||
/* buffer lines — [time] [gutter] [message] */
|
||||
.ln{display:grid;grid-template-columns:3.1em 7em 1fr;column-gap:.75em;align-items:baseline;padding:.09rem 0}
|
||||
.ln .t{color:var(--dimmer);font-size:.82em}
|
||||
.ln .g{text-align:right;white-space:nowrap;color:var(--dim)}
|
||||
.ln .m{min-width:0;word-wrap:break-word;overflow-wrap:anywhere}
|
||||
.ln .m a{color:var(--link)}
|
||||
|
||||
/* banner / wordmark */
|
||||
.banner{padding:.6rem 0 1.4rem;border-bottom:1px dashed var(--edge);margin-bottom:1rem}
|
||||
.wordmark{font-size:clamp(2.3rem,7vw,3.7rem);font-weight:700;letter-spacing:-2px;color:var(--fg);line-height:1}
|
||||
.wordmark .w-hi{color:var(--amber)}
|
||||
.w-sub{color:var(--dim);margin-top:.5rem;font-size:.95rem}
|
||||
.cur{display:inline-block;width:.55em;color:var(--amber);animation:blink 1.1s steps(1) infinite}
|
||||
.cur-lg{width:.5em;margin-left:.06em}
|
||||
@keyframes blink{50%{opacity:0}}
|
||||
@media(prefers-reduced-motion:reduce){.cur{animation:none}}
|
||||
|
||||
/* quickfacts */
|
||||
.quickfacts{display:flex;flex-wrap:wrap;gap:.5rem;margin-top:1.3rem}
|
||||
.qf{border:1px solid var(--edge);border-radius:6px;padding:.4rem .7rem;color:var(--dim);background:var(--panel2);font-size:.85rem}
|
||||
.qf:hover{border-color:var(--edge2);color:var(--fg);text-decoration:none}
|
||||
.qf .qf-k{color:var(--amber)}
|
||||
|
||||
/* feature groups */
|
||||
.grp{margin:1.5rem 0}
|
||||
.grp-h{color:var(--amber);font-size:.82rem;text-transform:uppercase;letter-spacing:.14em;padding-bottom:.4rem;margin-bottom:.7rem;border-bottom:1px solid var(--edge);position:relative}
|
||||
.grp-h::before{content:"── ";color:var(--dimmer)}
|
||||
.bul{list-style:none;margin:0;padding:0}
|
||||
.bul li{padding:.22rem 0 .22rem 1.3em;position:relative;color:var(--fg)}
|
||||
.bul li::before{content:"·";position:absolute;left:.3em;color:var(--amber)}
|
||||
.srvline{display:grid;grid-template-columns:7em 1fr;column-gap:.75em;align-items:baseline;margin:.2rem 0 .8rem}
|
||||
.srvline .g{text-align:right}
|
||||
.capline{color:var(--dim);word-break:break-word;line-height:1.7}
|
||||
|
||||
/* connect page: tables + code */
|
||||
.detail{width:100%;border-collapse:collapse;margin:.7rem 0 1rem;font-size:.9rem}
|
||||
.detail th{text-align:left;color:var(--dim);font-weight:400;padding:.4rem .8rem .4rem 0;border-bottom:1px solid var(--edge);white-space:nowrap;vertical-align:top;width:1%}
|
||||
.detail td{padding:.4rem 0;border-bottom:1px solid var(--edge);color:var(--fg)}
|
||||
.note{color:var(--dim);margin:.2rem 0 .6rem;font-size:.9rem}
|
||||
.code{background:var(--panel2);border:1px solid var(--edge);border-left:2px solid var(--amber);border-radius:6px;
|
||||
padding:.8rem 1rem;overflow-x:auto;color:var(--green);font-size:.88rem;line-height:1.75;white-space:pre;margin:.4rem 0}
|
||||
.webchat{display:flex;align-items:center;gap:.8rem;flex-wrap:wrap;margin:1.1rem 0}
|
||||
.btn{display:inline-block;border:1px solid var(--edge2);background:var(--panel);color:var(--fg);padding:.4rem .85rem;border-radius:6px;font-size:.88rem}
|
||||
.btn:hover{border-color:var(--amber);color:var(--amber);text-decoration:none}
|
||||
|
||||
/* input + status bars */
|
||||
.inputbar{display:flex;align-items:baseline;gap:.5rem;padding:.55rem .95rem;background:var(--panel);border-top:1px solid var(--edge);font-size:.9rem;overflow:hidden}
|
||||
.ib-ch{color:var(--dim)}
|
||||
.ib-prompt{color:var(--amber)}
|
||||
.ib-text{color:var(--fg);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.statusbar{display:flex;align-items:center;gap:.8rem;padding:.4rem .95rem;background:var(--panel2);border-top:1px solid var(--edge);font-size:.74rem;color:var(--dim)}
|
||||
.statusbar .sb-m{margin:0 auto;color:var(--amber);opacity:.75}
|
||||
.statusbar a{color:var(--dim)}
|
||||
.statusbar a:hover{color:var(--fg)}
|
||||
|
||||
@media(max-width:720px){
|
||||
.client{margin:0;border:0;border-radius:0;min-height:100vh}
|
||||
.workspace{flex-direction:column}
|
||||
.nicklist{width:auto;border-left:0;border-top:1px solid var(--edge);order:2}
|
||||
.nicklist ul{display:flex;flex-wrap:wrap;gap:.1rem .9rem}
|
||||
.ln{grid-template-columns:5.5em 1fr}
|
||||
.ln .t{display:none}
|
||||
.srvline{grid-template-columns:5.5em 1fr}
|
||||
.tb-server{display:none}
|
||||
@media(max-width:560px){
|
||||
.cards{grid-template-columns:1fr}
|
||||
.mainnav{gap:1rem}
|
||||
.news li{flex-direction:column;gap:.2rem}
|
||||
.news-date{width:auto}
|
||||
.ctaband-in{flex-direction:column;align-items:flex-start}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,67 +3,60 @@
|
|||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="color-scheme" content="dark">
|
||||
<title>{% block title %}echoIRCd{% endblock %}</title>
|
||||
<meta name="description" content="echoIRCd — an IRC network written from scratch in Rust. Full IRCv3, modern TLS, native services.">
|
||||
<title>{% block title %}echoIRCd — a from-scratch IRC server in Rust{% endblock %}</title>
|
||||
<meta name="description" content="echoIRCd is a modern IRC daemon and services suite, written from scratch in Rust — full IRCv3, modern TLS, SASL.">
|
||||
<meta property="og:title" content="echoIRCd">
|
||||
<meta property="og:description" content="An IRC network written from scratch in Rust.">
|
||||
<meta property="og:description" content="A from-scratch IRC server, written in Rust.">
|
||||
<meta property="og:type" content="website">
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Rubik:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="client">
|
||||
|
||||
<div class="titlebar">
|
||||
<span class="conn" title="connected"></span>
|
||||
<span class="tb-name">echoircd.org</span>
|
||||
<span class="tb-server">irc.echoircd.org · +6697 · TLS1.3</span>
|
||||
</div>
|
||||
|
||||
<nav class="tabs">
|
||||
<a href="/" class="tab{% if active == "home" %} on{% endif %}">#home</a>
|
||||
<a href="/features" class="tab{% if active == "features" %} on{% endif %}">#features</a>
|
||||
<a href="/connect" class="tab{% if active == "connect" %} on{% endif %}">#connect</a>
|
||||
<a href="https://git.devtronic.pro/echo/echoIRCd" class="tab ext">git↗</a>
|
||||
<header class="topbar">
|
||||
<div class="bar-in">
|
||||
<a class="brand" href="/">
|
||||
<svg class="mark" viewBox="0 0 32 32" aria-hidden="true">
|
||||
<circle cx="8" cy="16" r="3.1" fill="currentColor"/>
|
||||
<path d="M14 9.5a9 9 0 0 1 0 13" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"/>
|
||||
<path d="M18.5 5.5a15 15 0 0 1 0 21" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"/>
|
||||
</svg>
|
||||
<span class="brand-name">echoIRCd</span>
|
||||
</a>
|
||||
<nav class="mainnav">
|
||||
<a href="/"{% if active == "home" %} class="on"{% endif %}>Home</a>
|
||||
<a href="/features"{% if active == "features" %} class="on"{% endif %}>Features</a>
|
||||
<a href="/connect"{% if active == "connect" %} class="on"{% endif %}>Connect</a>
|
||||
<a href="https://orbit.devtronic.pro">Web client</a>
|
||||
<a href="https://git.devtronic.pro/echo/echoIRCd" class="ext">Source↗</a>
|
||||
</nav>
|
||||
|
||||
<div class="topic"><span class="tp">»</span> {% block topic %}echoIRCd — a native-Rust IRC daemon & services{% endblock %}</div>
|
||||
|
||||
<div class="workspace">
|
||||
<div class="buffer">
|
||||
{% block buffer %}{% endblock %}
|
||||
</div>
|
||||
<aside class="nicklist">
|
||||
{% block nicklist %}
|
||||
<div class="nl-head">names · 9</div>
|
||||
<ul class="nl">
|
||||
<li><span class="s n-amber">~</span><span class="n-amber">echoircd</span></li>
|
||||
<li><span class="s n-green">@</span><span class="n-green">NickServ</span><span class="bf">B</span></li>
|
||||
<li><span class="s n-blue">@</span><span class="n-blue">ChanServ</span><span class="bf">B</span></li>
|
||||
<li><span class="s n-magenta">@</span><span class="n-magenta">OperServ</span><span class="bf">B</span></li>
|
||||
<li><span class="s n-cyan">@</span><span class="n-cyan">MemoServ</span><span class="bf">B</span></li>
|
||||
<li><span class="s n-blue">+</span><span>TLS1.3</span></li>
|
||||
<li><span class="s n-blue">+</span><span>IRCv3</span></li>
|
||||
<li><span class="s n-blue">+</span><span>SASL</span></li>
|
||||
<li><span class="s dim">·</span><span class="dim">you</span></li>
|
||||
</ul>
|
||||
{% endblock %}
|
||||
</aside>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="inputbar">
|
||||
<span class="ib-ch">[{% block chan %}#home{% endblock %}]</span>
|
||||
<span class="ib-prompt">›</span>
|
||||
<span class="ib-text">{% block cmd %}/connect{% endblock %}</span><span class="cur">▋</span>
|
||||
</div>
|
||||
<main>
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
|
||||
<div class="statusbar">
|
||||
<span class="sb-l">echoIRCd 5.0.0</span>
|
||||
<span class="sb-m">#![forbid(unsafe_code)]</span>
|
||||
<span class="sb-r"><a href="https://git.devtronic.pro/echo/echo">services</a> · <a href="https://git.devtronic.pro/echo/website">website</a></span>
|
||||
<footer class="site-foot">
|
||||
<div class="foot-in">
|
||||
<div class="foot-brand">
|
||||
<svg class="mark" viewBox="0 0 32 32" aria-hidden="true">
|
||||
<circle cx="8" cy="16" r="3.1" fill="currentColor"/>
|
||||
<path d="M14 9.5a9 9 0 0 1 0 13" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"/>
|
||||
<path d="M18.5 5.5a15 15 0 0 1 0 21" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"/>
|
||||
</svg>
|
||||
<span>echoIRCd</span>
|
||||
</div>
|
||||
|
||||
<p class="foot-tag">A native-Rust IRC daemon & services. No forks, no C, no <code>unsafe</code>.</p>
|
||||
<nav class="foot-links">
|
||||
<a href="https://git.devtronic.pro/echo/echoIRCd">echoIRCd</a>
|
||||
<a href="https://git.devtronic.pro/echo/echo">services</a>
|
||||
<a href="https://git.devtronic.pro/echo/website">website</a>
|
||||
<a href="https://orbit.devtronic.pro">web client</a>
|
||||
</nav>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -1,54 +1,55 @@
|
|||
{% extends "base.html" %}
|
||||
{% block title %}#connect — echoIRCd{% endblock %}
|
||||
{% block topic %}#connect — point any client at irc.echoircd.org, or use the browser{% endblock %}
|
||||
{% block chan %}#connect{% endblock %}
|
||||
{% block cmd %}/server irc.echoircd.org +6697{% endblock %}
|
||||
{% block buffer %}
|
||||
<div class="ln"><span class="t">19:33</span><span class="g n-cyan"><you></span><span class="m">/server irc.echoircd.org +6697</span></div>
|
||||
<div class="ln"><span class="t">19:33</span><span class="g dim">-!-</span><span class="m dim">connecting to irc.echoircd.org:6697 … TLS1.3 up (X25519MLKEM768)</span></div>
|
||||
<div class="ln"><span class="t">19:33</span><span class="g dim">-!-</span><span class="m dim">Welcome to the echoiRCd IRC Network</span></div>
|
||||
{% block title %}Connect — echoIRCd{% endblock %}
|
||||
{% block content %}
|
||||
<section class="pagehead">
|
||||
<div class="wrap">
|
||||
<h1>Connect</h1>
|
||||
<p>Point any IRC client at the network — or open it right in your browser.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="wrap doc">
|
||||
<div class="doc-block">
|
||||
<table class="detail">
|
||||
<tr><th>server</th><td>irc.echoircd.org</td></tr>
|
||||
<tr><th>tls (recommended)</th><td>6697</td></tr>
|
||||
<tr><th>plaintext</th><td>6667</td></tr>
|
||||
<tr><th>network</th><td>echoiRCd</td></tr>
|
||||
<tr><th>Server</th><td><code>irc.echoircd.org</code></td></tr>
|
||||
<tr><th>TLS (recommended)</th><td><code>6697</code></td></tr>
|
||||
<tr><th>Plaintext</th><td><code>6667</code></td></tr>
|
||||
<tr><th>Network</th><td>echoiRCd</td></tr>
|
||||
</table>
|
||||
|
||||
<div class="webchat">
|
||||
<span class="dim">prefer the browser?</span>
|
||||
<a class="btn" href="https://orbit.devtronic.pro">open the web client ↗</a>
|
||||
<p class="webchat"><a class="btn btn-ghost" href="https://orbit.devtronic.pro">Open the web client ↗</a></p>
|
||||
</div>
|
||||
|
||||
<div class="grp">
|
||||
<div class="grp-h">client quick-start</div>
|
||||
<p class="note">most clients take a one-line server string; the leading <span class="kw">+</span> means TLS.</p>
|
||||
<div class="doc-block">
|
||||
<h2>Client quick-start</h2>
|
||||
<p>Most clients take a one-line server string; the leading <code>+</code> on the port means TLS.</p>
|
||||
<pre class="code">/server irc.echoircd.org +6697
|
||||
/join #echoircd</pre>
|
||||
<table class="detail">
|
||||
<tr><th>HexChat</th><td>add irc.echoircd.org/+6697 · tick “Use SSL”</td></tr>
|
||||
<tr><th>WeeChat</th><td>/server add echo irc.echoircd.org/6697 -tls</td></tr>
|
||||
<tr><th>irssi</th><td>/connect -tls irc.echoircd.org 6697</td></tr>
|
||||
<tr><th>mIRC</th><td>/server irc.echoircd.org +6697</td></tr>
|
||||
<tr><th>HexChat</th><td>add <code>irc.echoircd.org/+6697</code> · tick “Use SSL”</td></tr>
|
||||
<tr><th>WeeChat</th><td><code>/server add echo irc.echoircd.org/6697 -tls</code></td></tr>
|
||||
<tr><th>irssi</th><td><code>/connect -tls irc.echoircd.org 6697</code></td></tr>
|
||||
<tr><th>mIRC</th><td><code>/server irc.echoircd.org +6697</code></td></tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="grp">
|
||||
<div class="grp-h">register your nick</div>
|
||||
<p class="note">keep your nick, found channels, and log in with SASL.</p>
|
||||
<div class="doc-block">
|
||||
<h2>Register your nick</h2>
|
||||
<p>Registering lets you keep your nickname, found channels, and log in with SASL.</p>
|
||||
<pre class="code">/msg NickServ REGISTER <password> <email>
|
||||
/msg NickServ IDENTIFY <password></pre>
|
||||
</div>
|
||||
|
||||
<div class="grp">
|
||||
<div class="grp-h">log in with a key (ecdsa)</div>
|
||||
<p class="note">sign a server challenge with a NIST P-256 key instead of sending a password — nothing secret crosses the wire.</p>
|
||||
<div class="doc-block">
|
||||
<h2>Log in with a key (ECDSA)</h2>
|
||||
<p>Sign a server challenge with a NIST P-256 key instead of sending a password — nothing
|
||||
secret crosses the wire.</p>
|
||||
<pre class="code"># generate a key, read its public half
|
||||
ecdsatool keygen ~/.ecdsa.pem
|
||||
ecdsatool pubkey ~/.ecdsa.pem
|
||||
|
||||
# store it, then pick ECDSA-NIST256P-CHALLENGE for SASL
|
||||
/msg NickServ SET PUBKEY <printed-public-key></pre>
|
||||
<p class="note">point your client's SASL settings at the same key file.</p>
|
||||
<p class="note">Point your client's SASL settings at the same key file.</p>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -1,66 +1,85 @@
|
|||
{% extends "base.html" %}
|
||||
{% block title %}#features — echoIRCd{% endblock %}
|
||||
{% block topic %}#features — what's inside the daemon & services{% endblock %}
|
||||
{% block chan %}#features{% endblock %}
|
||||
{% block cmd %}/msg echoircd help{% endblock %}
|
||||
{% block buffer %}
|
||||
<div class="ln"><span class="t">19:31</span><span class="g n-cyan"><you></span><span class="m">/msg echoircd what can you do</span></div>
|
||||
<div class="ln"><span class="t">19:31</span><span class="g n-amber"><echoircd></span><span class="m">plenty — here's the short tour. all of it is original, native Rust.</span></div>
|
||||
{% block title %}Features — echoIRCd{% endblock %}
|
||||
{% block content %}
|
||||
<section class="pagehead">
|
||||
<div class="wrap">
|
||||
<h1>Features</h1>
|
||||
<p>Everything below is original, native Rust. The InspIRCd protocol is a reference for wire
|
||||
compatibility — never a source of code.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="grp">
|
||||
<div class="grp-h">the daemon</div>
|
||||
<ul class="bul">
|
||||
<li><b>safe by construction</b> — <span class="kw">#![forbid(unsafe_code)]</span> across the tree, enforced in CI</li>
|
||||
<li><b>reactor core</b> — single core thread + mio/epoll pool, zero-copy broadcast, parallel channel fan-out</li>
|
||||
<li><b>full mode set</b> — every standard channel/user mode, host cloaking, custom prefixes</li>
|
||||
<li><b>operators</b> — a privilege model: command grants, named privs, per-type mode allow-list</li>
|
||||
<li><b>the edge</b> — connection classes, per-class flood/fakelag, RFC1413 ident, PROXY v1/v2</li>
|
||||
<li><b>observability</b> — a metrics endpoint, JSON logging, native syslog</li>
|
||||
<section class="wrap doc">
|
||||
<div class="doc-block">
|
||||
<h2>The daemon</h2>
|
||||
<ul>
|
||||
<li><b>Safe by construction</b> — <code>#![forbid(unsafe_code)]</code> across the tree, enforced in CI.</li>
|
||||
<li><b>Reactor core</b> — a single core thread with a mio/epoll pool; zero-copy broadcast and parallel channel fan-out.</li>
|
||||
<li><b>Full mode set</b> — every standard channel and user mode, host cloaking, custom prefixes.</li>
|
||||
<li><b>Operators</b> — a privilege model: command grants, named privileges, and a per-type mode allow-list.</li>
|
||||
<li><b>The edge</b> — connection classes, per-class flood/fakelag, RFC 1413 ident, PROXY protocol v1/v2.</li>
|
||||
<li><b>Observability</b> — a metrics endpoint, structured JSON logging, and native syslog.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="grp">
|
||||
<div class="grp-h">transport & tls</div>
|
||||
<ul class="bul">
|
||||
<li><b>6697</b> direct TLS · <b>7799</b> WebSocket (wss) · <b>6667</b> plaintext</li>
|
||||
<li>two backends — <b>OpenSSL</b> and <b>rustls</b> — both serving TLS 1.3</li>
|
||||
<li>with OpenSSL 3.5 the handshake negotiates post-quantum <span class="kw">X25519MLKEM768</span></li>
|
||||
<li>per-host <b>SNI</b> certificates, reloaded live on rehash</li>
|
||||
<div class="doc-block">
|
||||
<h2>Transport & TLS</h2>
|
||||
<ul>
|
||||
<li><b>6697</b> direct TLS · <b>7799</b> WebSocket (wss) · <b>6667</b> plaintext.</li>
|
||||
<li>Two interchangeable backends — <b>OpenSSL</b> and <b>rustls</b> — both serving TLS 1.3.</li>
|
||||
<li>With OpenSSL 3.5 the handshake negotiates post-quantum <code>X25519MLKEM768</code>.</li>
|
||||
<li>Per-host <b>SNI</b> certificates, reloaded live on rehash.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="grp">
|
||||
<div class="grp-h">ircv3</div>
|
||||
<div class="srvline"><span class="g n-amber"><echoircd></span><span class="capline">CAP * LS :server-time message-tags account-tag account-notify extended-join chghost multi-prefix away-notify invite-notify setname echo-message userhost-in-names batch labeled-response standard-replies extended-monitor draft/chathistory draft/event-playback draft/message-redaction draft/multiline draft/metadata-2 draft/read-marker draft/webpush draft/account-registration sts</span></div>
|
||||
<div class="doc-block">
|
||||
<h2>IRCv3</h2>
|
||||
<p>Capabilities advertised to clients:</p>
|
||||
<div class="chips">
|
||||
<span>server-time</span><span>message-tags</span><span>account-tag</span><span>account-notify</span>
|
||||
<span>extended-join</span><span>chghost</span><span>multi-prefix</span><span>away-notify</span>
|
||||
<span>invite-notify</span><span>setname</span><span>echo-message</span><span>userhost-in-names</span>
|
||||
<span>batch</span><span>labeled-response</span><span>standard-replies</span><span>extended-monitor</span>
|
||||
<span>draft/chathistory</span><span>draft/event-playback</span><span>draft/message-redaction</span>
|
||||
<span>draft/multiline</span><span>draft/metadata-2</span><span>draft/read-marker</span>
|
||||
<span>draft/webpush</span><span>draft/account-registration</span><span>sts</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grp">
|
||||
<div class="grp-h">sasl</div>
|
||||
<div class="srvline"><span class="g n-amber"><echoircd></span><span class="capline">sasl=PLAIN,EXTERNAL,SCRAM-SHA-256,ECDSA-NIST256P-CHALLENGE</span></div>
|
||||
<ul class="bul">
|
||||
<li><b>EXTERNAL</b> — authenticate by your TLS client-certificate fingerprint</li>
|
||||
<li><b>SCRAM-SHA-256</b> & <b>ECDSA-NIST256P-CHALLENGE</b> — challenge/response, no secret on the wire</li>
|
||||
<div class="doc-block">
|
||||
<h2>SASL</h2>
|
||||
<ul>
|
||||
<li><b>PLAIN</b> — classic username / password.</li>
|
||||
<li><b>EXTERNAL</b> — authenticate by your TLS client-certificate fingerprint.</li>
|
||||
<li><b>SCRAM-SHA-256</b> — challenge / response, no password on the wire.</li>
|
||||
<li><b>ECDSA-NIST256P-CHALLENGE</b> — sign a challenge with a NIST P-256 key; the private key never leaves your client.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="grp">
|
||||
<div class="grp-h">services</div>
|
||||
<ul class="bul">
|
||||
<li><b>NickServ</b> — registration, grouped nicks, certs, public keys, vhosts, profiles</li>
|
||||
<li><b>ChanServ</b> — founder/access, auto-op, akick, topic & mode locks</li>
|
||||
<li><b>OperServ · MemoServ · more</b> — network admin, offline messages, games</li>
|
||||
<li><b>event-sourced</b> store; SASL relayed to services mechanism-agnostically</li>
|
||||
<div class="doc-block">
|
||||
<h2>Services</h2>
|
||||
<ul>
|
||||
<li><b>NickServ</b> — registration, grouped nicks, certificates, public keys, vhosts, profiles.</li>
|
||||
<li><b>ChanServ</b> — founder/access, auto-op, akick, topic and mode locks.</li>
|
||||
<li><b>OperServ · MemoServ · more</b> — network administration, offline messaging, and games.</li>
|
||||
<li><b>Event-sourced</b> store; SASL relayed to services mechanism-agnostically.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="grp">
|
||||
<div class="grp-h">security</div>
|
||||
<ul class="bul">
|
||||
<li>a native anti-abuse engine in the core — connection/nick-flood and mass-join screening</li>
|
||||
<li>behavioral and content heuristics with computed-pattern mining</li>
|
||||
<li>a DEFCON state machine plus DNSBL / MX screening via a native async resolver</li>
|
||||
<div class="doc-block">
|
||||
<h2>Security</h2>
|
||||
<ul>
|
||||
<li>A native anti-abuse engine in the core — connection/nick-flood and mass-join screening.</li>
|
||||
<li>Behavioral and content heuristics with computed-pattern mining.</li>
|
||||
<li>A DEFCON state machine plus DNSBL / MX screening via a native async resolver.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="ln"><span class="t">19:32</span><span class="g">*</span><span class="m i">echoircd nods toward <a href="/connect">#connect</a></span></div>
|
||||
<section class="ctaband">
|
||||
<div class="wrap ctaband-in">
|
||||
<div><h2>Try it</h2><p class="mono-line">irc.echoircd.org · +6697 · TLS 1.3</p></div>
|
||||
<a class="btn btn-primary" href="/connect">Connect & register</a>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -1,28 +1,68 @@
|
|||
{% extends "base.html" %}
|
||||
{% block title %}echoIRCd — a native-Rust IRC network{% endblock %}
|
||||
{% block topic %}#home — echoIRCd, a native-Rust IRC daemon & services · no fork · no C · no unsafe{% endblock %}
|
||||
{% block chan %}#home{% endblock %}
|
||||
{% block cmd %}/connect{% endblock %}
|
||||
{% block buffer %}
|
||||
<div class="banner">
|
||||
<div class="wordmark">echo<span class="w-hi">IRCd</span><span class="cur cur-lg">▋</span></div>
|
||||
<div class="w-sub">an IRC network written from scratch in Rust — built, not forked</div>
|
||||
{% block title %}echoIRCd — a from-scratch IRC server in Rust{% endblock %}
|
||||
{% block content %}
|
||||
<section class="hero">
|
||||
<div class="hero-in">
|
||||
<div class="hero-main">
|
||||
<p class="eyebrow">IRC daemon & services · written in Rust</p>
|
||||
<h1>The from-scratch<br>IRC server.</h1>
|
||||
<p class="hero-sub">echoIRCd is a modern IRC daemon and a full services suite, built from the
|
||||
ground up in Rust — full IRCv3, modern TLS, and SASL. No forks. No C. No <code>unsafe</code>.</p>
|
||||
<div class="cta">
|
||||
<a class="btn btn-primary" href="/connect">Connect to the network</a>
|
||||
<a class="btn btn-ghost" href="/features">See the features</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ln"><span class="t">19:28</span><span class="g dim">-!-</span><span class="m dim">Now talking on <b>#home</b></span></div>
|
||||
<div class="ln"><span class="t">19:28</span><span class="g dim">-!-</span><span class="m dim">echoIRCd/5.0.0 · irc.echoircd.org · network echoiRCd · <span class="ok">+6697 TLS1.3</span></span></div>
|
||||
<div class="ln"><span class="t">19:28</span><span class="g n-green">--></span><span class="m dim">you (~guest@echoircd.org) has joined <b>#home</b></span></div>
|
||||
<div class="ln"><span class="t">19:29</span><span class="g n-amber"><echoircd></span><span class="m">hey — you're looking at an IRC network written from the ground up in Rust.</span></div>
|
||||
<div class="ln"><span class="t">19:29</span><span class="g n-amber"><echoircd></span><span class="m">original code: no InspIRCd fork, no C, and <span class="kw">#![forbid(unsafe_code)]</span> across the whole tree.</span></div>
|
||||
<div class="ln"><span class="t">19:29</span><span class="g n-amber"><echoircd></span><span class="m">the daemon speaks full IRCv3 and modern TLS; services handle accounts, channels and SASL.</span></div>
|
||||
<div class="ln"><span class="t">19:30</span><span class="g">*</span><span class="m i">echoircd slides the tabs your way — <b>#features</b> for the tech, <b>#connect</b> to join</span></div>
|
||||
<div class="ln"><span class="t">19:30</span><span class="g n-cyan"><you></span><span class="m">nice. how do I get on?</span></div>
|
||||
<div class="ln"><span class="t">19:30</span><span class="g n-amber"><echoircd></span><span class="m"><a href="/connect">/connect</a> — or open <a href="https://orbit.devtronic.pro">the web client</a> right in your browser.</span></div>
|
||||
|
||||
<div class="quickfacts">
|
||||
<a class="qf" href="/features"><span class="qf-k">tls</span> 1.3 · post-quantum</a>
|
||||
<a class="qf" href="/features"><span class="qf-k">ircv3</span> full cap set</a>
|
||||
<a class="qf" href="/features"><span class="qf-k">sasl</span> incl. ecdsa</a>
|
||||
<a class="qf" href="/features"><span class="qf-k">unsafe</span> 0 lines</a>
|
||||
<aside class="statuscard" aria-label="live network status">
|
||||
<div class="sc-head"><span class="sc-title">network status</span>
|
||||
{% if status.online %}<span class="dot on" title="online"></span>{% else %}<span class="dot off" title="offline"></span>{% endif %}
|
||||
</div>
|
||||
<dl class="sc-rows">
|
||||
<div><dt>state</dt><dd>{% if status.online %}<b class="live">online</b>{% else %}<b class="down">offline</b>{% endif %}</dd></div>
|
||||
<div><dt>users</dt><dd>{{ status.users }}</dd></div>
|
||||
<div><dt>network</dt><dd>echoiRCd</dd></div>
|
||||
<div><dt>version</dt><dd>echoIRCd {{ status.version }}</dd></div>
|
||||
<div><dt>tls</dt><dd>1.3 · X25519MLKEM768</dd></div>
|
||||
</dl>
|
||||
<div class="sc-foot">live from <code>irc.echoircd.org</code></div>
|
||||
</aside>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="band">
|
||||
<div class="wrap">
|
||||
<h2 class="h-center">Why echoIRCd</h2>
|
||||
<div class="cards">
|
||||
<div class="fcard"><h3>Safe by construction</h3><p><code>#![forbid(unsafe_code)]</code> across the whole tree, enforced in CI. Original code — not a fork of anything.</p></div>
|
||||
<div class="fcard"><h3>Full IRCv3</h3><p>server-time, message-tags, account-tag, batch, labeled-response, chathistory, multiline, standard-replies and more.</p></div>
|
||||
<div class="fcard"><h3>Modern TLS</h3><p>Direct TLS and WebSocket, OpenSSL & rustls backends, TLS 1.3 with post-quantum X25519MLKEM768, per-host SNI.</p></div>
|
||||
<div class="fcard"><h3>SASL, incl. ECDSA</h3><p>PLAIN, EXTERNAL, SCRAM-SHA-256, and ECDSA-NIST256P-CHALLENGE — sign a challenge, no password on the wire.</p></div>
|
||||
<div class="fcard"><h3>Native services</h3><p>NickServ, ChanServ, OperServ, MemoServ and more over S2S — accounts, channels, vhosts, an event-sourced store.</p></div>
|
||||
<div class="fcard"><h3>Anti-abuse built in</h3><p>A native security engine: connection & nick-flood detection, behavioral/content heuristics, DEFCON, DNSBL/MX screening.</p></div>
|
||||
</div>
|
||||
<p class="h-center more"><a href="/features">Everything that's inside →</a></p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="wrap latest">
|
||||
<h2>Latest</h2>
|
||||
<ul class="news">
|
||||
<li>
|
||||
<span class="news-date">Aug 2026</span>
|
||||
<div><a href="https://git.devtronic.pro/echo/echoIRCd"><b>echoIRCd 5.0</b></a> — SASL <b>ECDSA-NIST256P-CHALLENGE</b>, post-quantum TLS 1.3, the operator privilege model, and a native anti-abuse engine.</div>
|
||||
</li>
|
||||
<li>
|
||||
<span class="news-date">Aug 2026</span>
|
||||
<div><a href="https://git.devtronic.pro/echo/echo"><b>echo services</b></a> — key-based login (<code>SET PUBKEY</code>), event-sourced accounts, and SASL relayed over the server link.</div>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="ctaband">
|
||||
<div class="wrap ctaband-in">
|
||||
<div><h2>Ready to connect?</h2><p class="mono-line">irc.echoircd.org · +6697 · TLS 1.3</p></div>
|
||||
<a class="btn btn-primary" href="/connect">Get connected</a>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue