diff --git a/.drone.yml b/.drone.yml index 0602263..2d505aa 100644 --- a/.drone.yml +++ b/.drone.yml @@ -41,53 +41,3 @@ steps: when: event: - tag - ---- - -kind: pipeline -name: docker -steps: - - name: build docker image - image: "monacoremo/nix:2020-04-05-05f09348-circleci" - environment: - USER: root - commands: - - true # cachix use xe - - nix-build -A majsite docker.nix - - cp $(readlink result) /result/site.tgz - - nix-build -A majc docker.nix - - cp $(readlink result) /result/majc.tgz - volumes: - - name: image - path: /result - when: - event: - - tag - - - name: push docker image - image: docker:dind - volumes: - - name: image - path: /result - - name: dockersock - path: /var/run/docker.sock - commands: - - docker load -i /result/site.tgz - - docker load -i /result/majc.tgz - - echo $DOCKER_PASSWORD | docker login -u $DOCKER_USERNAME --password-stdin - - docker push xena/majsite - - docker push xena/majc - environment: - DOCKER_USERNAME: xena - DOCKER_PASSWORD: - from_secret: DOCKER_PASSWORD - when: - event: - - tag - -volumes: - - name: image - temp: {} - - name: dockersock - host: - path: /var/run/docker.sock diff --git a/CHANGELOG.md b/CHANGELOG.md index 607b1f2..bd9b1b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## 0.2.0 + +### maj + +- `maj::server` now multithreads correctly +- maj now passes most of the gemini tests + +### majsite + +- add `/majc` page with magc's help.gmi file + ## 0.1.0 All major functionality has been added. diff --git a/Cargo.toml b/Cargo.toml index c8d0764..6b7faa4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "maj" -version = "0.1.0" +version = "0.2.0" authors = ["Christine Dodrill "] edition = "2018" license = "0BSD" diff --git a/VERSION b/VERSION index 6e8bf73..0ea3a94 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.1.0 +0.2.0 diff --git a/site/Cargo.toml b/site/Cargo.toml index 7529324..cb22765 100644 --- a/site/Cargo.toml +++ b/site/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "majsite" -version = "0.1.0" +version = "0.2.0" authors = ["Christine Dodrill "] edition = "2018" @@ -12,5 +12,7 @@ tokio = { version = "0.2", features = ["full"] } tokio-rustls = { version = "0.14", features = ["dangerous_configuration"] } async-trait = "0" pretty_env_logger = "0.4" +log = "0" +anyhow = "1" maj = { path = ".." } \ No newline at end of file diff --git a/site/src/main.rs b/site/src/main.rs index 7759ccd..e96d57a 100644 --- a/site/src/main.rs +++ b/site/src/main.rs @@ -22,6 +22,14 @@ struct Options { /// key file #[structopt(short = "k", long = "key", env = "KEY_FILE")] key: PathBuf, + + /// server hostname + #[structopt( + long = "hostname", + env = "SERVER_HOSTNAME", + default_value = "maj.kahless.cetacean.club" + )] + hostname: String, } fn load_certs(path: &Path) -> io::Result> { @@ -41,17 +49,29 @@ async fn main() -> Result<(), maj::server::Error> { let certs = load_certs(&opts.cert)?; let mut keys = load_keys(&opts.key)?; + log::info!("{:?}", opts); + let mut config = ServerConfig::new(NoClientAuth::new()); config .set_single_cert(certs, keys.remove(0)) .map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?; - maj::server::serve(Handler{}, config, opts.host, opts.port).await?; + maj::server::serve( + &Handler { + hostname: opts.hostname, + }, + config, + opts.host, + opts.port, + ) + .await?; Ok(()) } -struct Handler {} +struct Handler { + hostname: String, +} fn index() -> Result { let msg = include_bytes!("index.gmi"); @@ -63,14 +83,38 @@ fn index() -> Result { }) } +fn majc() -> Result { + let msg = include_bytes!("majc.gmi"); + + Ok(maj::Response { + status: maj::StatusCode::Success, + meta: "text/gemini".to_string(), + body: msg.to_vec(), + }) +} + #[async_trait::async_trait] impl maj::server::Handler for Handler { async fn handle(&self, r: maj::server::Request) -> Result { + if r.url.has_host() && r.url.host_str().unwrap().to_string() != self.hostname { + return Ok(maj::Response { + status: maj::StatusCode::ProxyRequestRefused, + meta: "Wrong host".to_string(), + body: vec![], + }); + } + match r.url.path() { - "/" | "" => index(), + "" => Ok(maj::Response { + status: maj::StatusCode::PermanentRedirect, + meta: format!("gemini://{}/", self.hostname), + body: vec![], + }), + "/" => index(), + "/majc" => majc(), _ => Ok(maj::Response { status: maj::StatusCode::NotFound, - meta: "".to_string(), + meta: "Not found".to_string(), body: vec![], }), } diff --git a/site/src/majc.gmi b/site/src/majc.gmi new file mode 100644 index 0000000..344cae7 --- /dev/null +++ b/site/src/majc.gmi @@ -0,0 +1,26 @@ +# majc +``` + __ + _____ _____ |__| ____ + / \ \__ \ | |_/ ___\ +| Y Y \ / __ \_ | |\ \___ +|__|_| /(____ //\__| | \___ > + \/ \/ \______| \/ +``` + +A curses client for Gemini! + +=> gemini://gemini.circumlunar.space/ Gemini homepage + +## Homepage +The main homepage for majc is on tulpa.dev: + +=> https://tulpa.dev/cadey/maj + +## Important Keys +: opens the menubar +c: closes the active window +o: prompts to open a URL +q: quits majc +?: shows this screen +~: toggles the debug logging pane diff --git a/src/server/mod.rs b/src/server/mod.rs index cbb1394..e6fad5f 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -1,7 +1,7 @@ use crate::{Response, StatusCode}; use async_trait::async_trait; use rustls::{Certificate, Session}; -use std::{error::Error as StdError, sync::Arc}; +use std::{error::Error as StdError, net::SocketAddr, sync::Arc}; use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; use tokio::{net::TcpListener, stream::StreamExt}; use tokio_rustls::TlsAcceptor; @@ -25,9 +25,13 @@ pub trait Handler { async fn handle(&self, r: Request) -> Result; } -pub async fn serve(h: T, cfg: rustls::ServerConfig, host: String, port: u16) -> Result<(), Error> +pub async fn serve( + h: &(dyn Handler + Sync), + cfg: rustls::ServerConfig, + host: String, + port: u16, +) -> Result<(), Error> where - T: Handler, { let cfg = Arc::new(cfg); let mut listener = TcpListener::bind(&format!("{}:{}", host, port)).await?; @@ -37,86 +41,114 @@ where while let Some(stream) = incoming.next().await { let stream = stream?; let addr = stream.peer_addr().unwrap(); - let acceptor = acceptor.clone(); - let mut stream = acceptor.accept(stream).await?; - let mut rd = BufReader::new(&mut stream); - let mut u = String::new(); - rd.read_line(&mut u).await?; - if u.len() > 1025 { - stream - .write(format!("{} URL too long", StatusCode::BadRequest as u8).as_bytes()) - .await?; - continue; - } + let fut = async { + let acceptor = acceptor.clone(); + let result = acceptor.accept(stream).await; - let u = Url::parse(&u)?; - match h - .handle(Request { - url: u.clone(), - certs: stream.get_ref().1.get_peer_certificates(), - }) - .await - { - Ok(resp) => { - stream - .write(format!("{} {}\r\n", resp.status as u8, resp.meta).as_bytes()) - .await?; - stream.write(&resp.body).await?; - log::info!("{}: {} {:?}", addr, u, resp.status); + if result.is_err() { + return; } - Err(why) => { - stream - .write(format!("{} {:?}\r\n", StatusCode::PermanentFailure as u8, why).as_bytes()) - .await?; - log::error!("{}: {}: {:?}", addr, u, why); + + let mut stream = result.unwrap(); + + let mut rd = BufReader::new(&mut stream); + let mut u = String::new(); + if let Err(why) = rd.read_line(&mut u).await { + log::error!("can't read request from {}: {:?}", addr, why); + let _ = stream + .write(format!("{} Invalid URL", StatusCode::BadRequest as u8).as_bytes()) + .await; + return; + } + u = u.trim().to_string(); + if u.len() >= 1025 { + let _ = stream + .write(format!("{} URL too long", StatusCode::BadRequest as u8).as_bytes()) + .await; + return; + } + + if u.starts_with("//") { + u = format!("gemini:{}", u); + } + + match Url::parse(&u) { + Err(why) => { + let _ = stream + .write( + format!("{} bad URL: {:?}", StatusCode::BadRequest as u8, why) + .as_bytes(), + ) + .await; + } + Ok(u) => { + if u.scheme() != "gemini" { + let _ = stream + .write( + format!( + "{} Cannot handle that kind of url", + StatusCode::ProxyRequestRefused as u8 + ) + .as_bytes(), + ) + .await; + return; + } + + if let Some(u_port) = u.port() { + if port != u_port { + let _ = stream + .write( + format!( + "{} Cannot handle that kind of url", + StatusCode::ProxyRequestRefused as u8 + ) + .as_bytes(), + ) + .await; + return; + } + } + + tokio::join!(handle( + h, + Request { + url: u.clone(), + certs: stream.get_ref().1.get_peer_certificates(), + }, + &mut stream, + addr, + )); + } } }; + + tokio::join!(fut); } Ok(()) } -pub async fn serve_plain(h: T, host: String, port: u16) -> Result<(), Error> +async fn handle(h: &(dyn Handler + Sync), req: Request, stream: &mut T, addr: SocketAddr) where - T: Handler, + T: AsyncWriteExt + Unpin, { - let mut listener = TcpListener::bind(&format!("{}:{}", host, port)).await?; - let mut incoming = listener.incoming(); - while let Some(stream) = incoming.next().await { - let mut stream = stream?; - let mut rd = BufReader::new(&mut stream); - let mut u = String::new(); - rd.read_line(&mut u).await?; - if u.len() > 1025 { + let u = req.url.clone(); + match h.handle(req).await { + Ok(resp) => { stream - .write(format!("{} URL too long", StatusCode::BadRequest as u8).as_bytes()) - .await?; - continue; + .write(format!("{} {}\r\n", resp.status as u8, resp.meta).as_bytes()) + .await + .unwrap(); + stream.write(&resp.body).await.unwrap(); + log::info!("{}: {} {:?}", addr, u, resp.status); } - - let u = Url::parse(&u)?; - match h - .handle(Request { - url: u.clone(), - certs: None, - }) - .await - { - Ok(resp) => { - stream - .write(format!("{} {}", resp.status as u8, resp.meta).as_bytes()) - .await?; - stream.write(&resp.body).await?; - log::info!("{}: {} {:?}", stream.peer_addr().unwrap(), u, resp.status); - } - Err(why) => { - stream - .write(format!("{} {:?}", StatusCode::PermanentFailure as u8, why).as_bytes()) - .await?; - log::error!("{}: {}: {:?}", stream.peer_addr().unwrap(), u, why); - } - }; - } - - Ok(()) + Err(why) => { + stream + .write(format!("{} {:?}\r\n", StatusCode::PermanentFailure as u8, why).as_bytes()) + .await + .unwrap(); + log::error!("{}: {}: {:?}", addr, u, why); + } + }; }