version 0.2.0
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details

This commit is contained in:
Cadey Ratio 2020-07-26 12:21:02 +00:00
parent a9d7c9b3be
commit 6df5735f7d
8 changed files with 194 additions and 129 deletions

View File

@ -41,53 +41,3 @@ steps:
when: when:
event: event:
- tag - 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

View File

@ -1,5 +1,16 @@
# Changelog # 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 ## 0.1.0
All major functionality has been added. All major functionality has been added.

View File

@ -1,6 +1,6 @@
[package] [package]
name = "maj" name = "maj"
version = "0.1.0" version = "0.2.0"
authors = ["Christine Dodrill <me@christine.website>"] authors = ["Christine Dodrill <me@christine.website>"]
edition = "2018" edition = "2018"
license = "0BSD" license = "0BSD"

View File

@ -1 +1 @@
0.1.0 0.2.0

View File

@ -1,6 +1,6 @@
[package] [package]
name = "majsite" name = "majsite"
version = "0.1.0" version = "0.2.0"
authors = ["Christine Dodrill <me@christine.website>"] authors = ["Christine Dodrill <me@christine.website>"]
edition = "2018" edition = "2018"
@ -12,5 +12,7 @@ tokio = { version = "0.2", features = ["full"] }
tokio-rustls = { version = "0.14", features = ["dangerous_configuration"] } tokio-rustls = { version = "0.14", features = ["dangerous_configuration"] }
async-trait = "0" async-trait = "0"
pretty_env_logger = "0.4" pretty_env_logger = "0.4"
log = "0"
anyhow = "1"
maj = { path = ".." } maj = { path = ".." }

View File

@ -22,6 +22,14 @@ struct Options {
/// key file /// key file
#[structopt(short = "k", long = "key", env = "KEY_FILE")] #[structopt(short = "k", long = "key", env = "KEY_FILE")]
key: PathBuf, 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<Vec<Certificate>> { fn load_certs(path: &Path) -> io::Result<Vec<Certificate>> {
@ -41,17 +49,29 @@ async fn main() -> Result<(), maj::server::Error> {
let certs = load_certs(&opts.cert)?; let certs = load_certs(&opts.cert)?;
let mut keys = load_keys(&opts.key)?; let mut keys = load_keys(&opts.key)?;
log::info!("{:?}", opts);
let mut config = ServerConfig::new(NoClientAuth::new()); let mut config = ServerConfig::new(NoClientAuth::new());
config config
.set_single_cert(certs, keys.remove(0)) .set_single_cert(certs, keys.remove(0))
.map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?; .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(()) Ok(())
} }
struct Handler {} struct Handler {
hostname: String,
}
fn index() -> Result<maj::Response, maj::server::Error> { fn index() -> Result<maj::Response, maj::server::Error> {
let msg = include_bytes!("index.gmi"); let msg = include_bytes!("index.gmi");
@ -63,14 +83,38 @@ fn index() -> Result<maj::Response, maj::server::Error> {
}) })
} }
fn majc() -> Result<maj::Response, maj::server::Error> {
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] #[async_trait::async_trait]
impl maj::server::Handler for Handler { impl maj::server::Handler for Handler {
async fn handle(&self, r: maj::server::Request) -> Result<maj::Response, maj::server::Error> { async fn handle(&self, r: maj::server::Request) -> Result<maj::Response, maj::server::Error> {
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() { 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 { _ => Ok(maj::Response {
status: maj::StatusCode::NotFound, status: maj::StatusCode::NotFound,
meta: "".to_string(), meta: "Not found".to_string(),
body: vec![], body: vec![],
}), }),
} }

26
site/src/majc.gmi Normal file
View File

@ -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
<esc>: 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

View File

@ -1,7 +1,7 @@
use crate::{Response, StatusCode}; use crate::{Response, StatusCode};
use async_trait::async_trait; use async_trait::async_trait;
use rustls::{Certificate, Session}; 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::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::{net::TcpListener, stream::StreamExt}; use tokio::{net::TcpListener, stream::StreamExt};
use tokio_rustls::TlsAcceptor; use tokio_rustls::TlsAcceptor;
@ -25,9 +25,13 @@ pub trait Handler {
async fn handle(&self, r: Request) -> Result<Response, Error>; async fn handle(&self, r: Request) -> Result<Response, Error>;
} }
pub async fn serve<T>(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 where
T: Handler,
{ {
let cfg = Arc::new(cfg); let cfg = Arc::new(cfg);
let mut listener = TcpListener::bind(&format!("{}:{}", host, port)).await?; let mut listener = TcpListener::bind(&format!("{}:{}", host, port)).await?;
@ -37,86 +41,114 @@ where
while let Some(stream) = incoming.next().await { while let Some(stream) = incoming.next().await {
let stream = stream?; let stream = stream?;
let addr = stream.peer_addr().unwrap(); let addr = stream.peer_addr().unwrap();
let fut = async {
let acceptor = acceptor.clone(); let acceptor = acceptor.clone();
let mut stream = acceptor.accept(stream).await?; let result = acceptor.accept(stream).await;
let mut rd = BufReader::new(&mut stream);
let mut u = String::new(); if result.is_err() {
rd.read_line(&mut u).await?; return;
if u.len() > 1025 {
stream
.write(format!("{} URL too long", StatusCode::BadRequest as u8).as_bytes())
.await?;
continue;
} }
let u = Url::parse(&u)?; let mut stream = result.unwrap();
match h
.handle(Request { 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(), url: u.clone(),
certs: stream.get_ref().1.get_peer_certificates(), certs: stream.get_ref().1.get_peer_certificates(),
}) },
.await &mut stream,
{ addr,
));
}
}
};
tokio::join!(fut);
}
Ok(())
}
async fn handle<T>(h: &(dyn Handler + Sync), req: Request, stream: &mut T, addr: SocketAddr)
where
T: AsyncWriteExt + Unpin,
{
let u = req.url.clone();
match h.handle(req).await {
Ok(resp) => { Ok(resp) => {
stream stream
.write(format!("{} {}\r\n", resp.status as u8, resp.meta).as_bytes()) .write(format!("{} {}\r\n", resp.status as u8, resp.meta).as_bytes())
.await?; .await
stream.write(&resp.body).await?; .unwrap();
stream.write(&resp.body).await.unwrap();
log::info!("{}: {} {:?}", addr, u, resp.status); log::info!("{}: {} {:?}", addr, u, resp.status);
} }
Err(why) => { Err(why) => {
stream stream
.write(format!("{} {:?}\r\n", StatusCode::PermanentFailure as u8, why).as_bytes()) .write(format!("{} {:?}\r\n", StatusCode::PermanentFailure as u8, why).as_bytes())
.await?; .await
.unwrap();
log::error!("{}: {}: {:?}", addr, u, why); log::error!("{}: {}: {:?}", addr, u, why);
} }
}; };
}
Ok(())
}
pub async fn serve_plain<T>(h: T, host: String, port: u16) -> Result<(), Error>
where
T: Handler,
{
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 {
stream
.write(format!("{} URL too long", StatusCode::BadRequest as u8).as_bytes())
.await?;
continue;
}
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(())
} }