forked from cadey/maj
version 0.2.0
This commit is contained in:
parent
a9d7c9b3be
commit
6df5735f7d
50
.drone.yml
50
.drone.yml
|
@ -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
|
|
||||||
|
|
11
CHANGELOG.md
11
CHANGELOG.md
|
@ -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.
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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 = ".." }
|
|
@ -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![],
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
@ -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 acceptor = acceptor.clone();
|
let fut = async {
|
||||||
let mut stream = acceptor.accept(stream).await?;
|
let acceptor = acceptor.clone();
|
||||||
let mut rd = BufReader::new(&mut stream);
|
let result = acceptor.accept(stream).await;
|
||||||
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)?;
|
if result.is_err() {
|
||||||
match h
|
return;
|
||||||
.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);
|
|
||||||
}
|
}
|
||||||
Err(why) => {
|
|
||||||
stream
|
let mut stream = result.unwrap();
|
||||||
.write(format!("{} {:?}\r\n", StatusCode::PermanentFailure as u8, why).as_bytes())
|
|
||||||
.await?;
|
let mut rd = BufReader::new(&mut stream);
|
||||||
log::error!("{}: {}: {:?}", addr, u, why);
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn serve_plain<T>(h: T, host: String, port: u16) -> Result<(), Error>
|
async fn handle<T>(h: &(dyn Handler + Sync), req: Request, stream: &mut T, addr: SocketAddr)
|
||||||
where
|
where
|
||||||
T: Handler,
|
T: AsyncWriteExt + Unpin,
|
||||||
{
|
{
|
||||||
let mut listener = TcpListener::bind(&format!("{}:{}", host, port)).await?;
|
let u = req.url.clone();
|
||||||
let mut incoming = listener.incoming();
|
match h.handle(req).await {
|
||||||
while let Some(stream) = incoming.next().await {
|
Ok(resp) => {
|
||||||
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
|
stream
|
||||||
.write(format!("{} URL too long", StatusCode::BadRequest as u8).as_bytes())
|
.write(format!("{} {}\r\n", resp.status as u8, resp.meta).as_bytes())
|
||||||
.await?;
|
.await
|
||||||
continue;
|
.unwrap();
|
||||||
|
stream.write(&resp.body).await.unwrap();
|
||||||
|
log::info!("{}: {} {:?}", addr, u, resp.status);
|
||||||
}
|
}
|
||||||
|
Err(why) => {
|
||||||
let u = Url::parse(&u)?;
|
stream
|
||||||
match h
|
.write(format!("{} {:?}\r\n", StatusCode::PermanentFailure as u8, why).as_bytes())
|
||||||
.handle(Request {
|
.await
|
||||||
url: u.clone(),
|
.unwrap();
|
||||||
certs: None,
|
log::error!("{}: {}: {:?}", addr, u, why);
|
||||||
})
|
}
|
||||||
.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(())
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue