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:
|
||||
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
|
||||
|
|
11
CHANGELOG.md
11
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.
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "maj"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
authors = ["Christine Dodrill <me@christine.website>"]
|
||||
edition = "2018"
|
||||
license = "0BSD"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "majsite"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
authors = ["Christine Dodrill <me@christine.website>"]
|
||||
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 = ".." }
|
|
@ -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<Vec<Certificate>> {
|
||||
|
@ -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<maj::Response, maj::server::Error> {
|
||||
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]
|
||||
impl maj::server::Handler for Handler {
|
||||
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() {
|
||||
"/" | "" => 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![],
|
||||
}),
|
||||
}
|
||||
|
|
|
@ -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 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<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
|
||||
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 fut = async {
|
||||
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 result = acceptor.accept(stream).await;
|
||||
|
||||
if result.is_err() {
|
||||
return;
|
||||
}
|
||||
|
||||
let u = Url::parse(&u)?;
|
||||
match h
|
||||
.handle(Request {
|
||||
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(),
|
||||
})
|
||||
.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) => {
|
||||
stream
|
||||
.write(format!("{} {}\r\n", resp.status as u8, resp.meta).as_bytes())
|
||||
.await?;
|
||||
stream.write(&resp.body).await?;
|
||||
.await
|
||||
.unwrap();
|
||||
stream.write(&resp.body).await.unwrap();
|
||||
log::info!("{}: {} {:?}", addr, u, resp.status);
|
||||
}
|
||||
Err(why) => {
|
||||
stream
|
||||
.write(format!("{} {:?}\r\n", StatusCode::PermanentFailure as u8, why).as_bytes())
|
||||
.await?;
|
||||
.await
|
||||
.unwrap();
|
||||
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(())
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue