Compare commits

..

37 Commits

Author SHA1 Message Date
f668a07a8b Update build screipt with conditional steps
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-04-28 22:14:46 +00:00
637054ed29 Attempt building with a static openssl
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-04-28 22:05:29 +00:00
d41a21dbe9 Try and verify successful CLI buld
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-04-28 21:53:51 +00:00
0ee3555bb5 Update build script for the binary cli
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-04-28 21:43:10 +00:00
ce7bcf0908 Update capsule to refer to captown CLI 0.2
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-04-28 21:20:57 +00:00
77e5271a40 Add support for specifying the number of logs to return
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-04-28 20:49:43 +00:00
921e410c9b Fix DB mapping
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-04-28 20:06:40 +00:00
1da463facd Added basic log-retrieval for the CLI 2023-04-28 19:08:26 +00:00
e61014fb4f Revert to Rocket 4 for the API
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-04-28 18:09:39 +00:00
aa0812677c Attempt build with rustix feature flag
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2023-04-28 17:39:44 +00:00
ea0084c02c Attempt API build
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2023-04-28 16:33:06 +00:00
9a7abea7ab Minor tweaks to home capsule
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-04-27 08:55:23 +00:00
8062033f8e Update homepage text
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2023-04-26 14:05:48 +00:00
bae265caed Update rust image
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2023-04-25 17:25:32 +00:00
b759b8e00b Update gitignore
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2023-04-25 17:16:02 +00:00
803ffae22c Tidy up for production 2023-04-25 17:15:07 +00:00
49a56f13ba Update API paths to DB and capsules 2023-04-25 17:10:44 +00:00
07f7917152 Add function to return view logs from API 2023-04-25 17:06:38 +00:00
35ca28316d Upgraded rocket 2023-04-25 16:35:46 +00:00
debc8625fc Add support for basic access logs
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-04-25 13:25:22 +00:00
ad543edf31 Remove CLI build for now
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-04-22 16:15:46 +00:00
3722998218 Attempt CLI build
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2023-04-22 15:32:00 +00:00
2a6b8c046a Slight amendment to root capsule
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-04-22 15:18:18 +00:00
add2d5b41c Add woodpecker build file
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-04-22 15:16:35 +00:00
d91881ec9b Update deps for new build 2022-05-16 22:04:25 +00:00
a640349ed7 Updated getting started page 2022-05-13 22:11:45 +00:00
d4215034a0 added licenses to the api and cli 2021-07-06 19:19:58 +01:00
acbbebcc53 update root capsule's title 2021-05-15 10:35:34 +01:00
649d6a28c6 added arty title 2021-05-14 22:42:24 +01:00
49c1d83104 updated capsule homepage 2021-05-14 21:58:32 +01:00
0c67ccc663 update help guide to include non-interactive publish mechanism 2021-05-03 23:18:42 +01:00
24be312548 add support for passing in the capsule name as an argument to 'publish' 2021-05-03 22:53:17 +01:00
8c37442ef8 updated home capsule 2021-05-03 20:19:43 +01:00
f2c6ed565e update the getting started guide 2021-05-03 19:40:52 +01:00
46830d2b97 add a getting started guide and rules gempage 2021-05-03 18:04:30 +01:00
938386c1d7 added home capsule 2021-05-03 17:01:33 +01:00
12e5d9ca4e added README 2021-05-03 16:39:17 +01:00
19 changed files with 1871 additions and 880 deletions

5
.gitignore vendored
View File

@ -1,6 +1,9 @@
target/
capsule/
cli/capsule/
*.sw*
db.db3
cert.pem
key.rsa
content/
.DS_Store
build/

61
.woodpecker.yml Normal file
View File

@ -0,0 +1,61 @@
pipeline:
buildApi:
group: build
image: woodpeckerci/plugin-docker-buildx
secrets: [docker_username, docker_password]
when:
path: "api/**/*"
settings:
repo: wilw/capsuletown-api
dockerfile: api/Dockerfile
context: api
buildServer:
group: build
image: woodpeckerci/plugin-docker-buildx
secrets: [docker_username, docker_password]
when:
path: "server/**/*"
settings:
repo: wilw/capsuletown-server
dockerfile: server/Dockerfile
context: server
buildCli:
group: build
image: rust
when:
path: "cli/**/*"
commands:
- cd cli
- cargo build --release
- target/release/captown help
deployRootCapsule:
group: deploy
image: python:3.10
when:
path: "home/**/*"
secrets: [ CAPSULE_TOWN_KEY ]
commands:
- cd home
- CAPSULE=$(tar -czf /tmp/c.tar.gz -C capsule . && cat /tmp/c.tar.gz | base64)
- 'echo "{\"capsuleArchive\": \"$CAPSULE\"}" > /tmp/capsule_file'
- 'curl -X PUT -H "Content-Type: application/json" -H "api-key: $CAPSULE_TOWN_KEY" -d @/tmp/capsule_file https://api.capsule.town/capsule'
deployCli:
group: deploy
image: alpine
when:
path: "cli/**/*"
secrets: [ LINODE_ACCESS_KEY, LINODE_SECRET_ACCESS_KEY, BUNNY_KEY ]
commands:
- cd cli
- apk update
- apk add s3cmd curl
- s3cmd --configure --access_key=$LINODE_ACCESS_KEY --secret_key=$LINODE_SECRET_ACCESS_KEY --host=https://eu-central-1.linodeobjects.com --host-bucket="%(bucket)s.eu-central-1.linodeobjects.com" --dump-config > /root/.s3cfg
- s3cmd -c /root/.s3cfg put target/release/captown s3://download.capsule.town/captown-linux-0.2 --acl-public
- 'curl -X POST -H "AccessKey: $BUNNY_KEY" https://api.bunny.net/pullzone/783552/purgeCache'
branches: main

13
README.md Normal file
View File

@ -0,0 +1,13 @@
# capsule.town
Welcome to the capsule.town repository.
This repository contains the files and source for the client, API, and server that power gemini://capsule.town.
# Learn more
To find out more, visit gemini://capsule.town with a Gemini client.
# Contribute
If you'd like to contribute to this project, please [get in touch](https://wilw.dev).

650
api/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -4,15 +4,14 @@ version = "0.1.0"
authors = ["Will Webberley <me@wilw.dev>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
rocket = "0.4.6"
rocket_contrib = "0.4.6"
jsonwebtoken = "7.2"
serde = { version = "1.0", features = ["derive"] }
flate2 = "1.0.19"
tar = "0.4.30"
base64 = "0.13.0"
rusqlite = "0.24.2"
uuid = { version = "0.8", features = ["v4"] }
rocket = "0.4.11"
jsonwebtoken = "8.3.0"
serde = { version = "1.0.160", features = ["derive"] }
flate2 = "1.0.26"
tar = "0.4.38"
base64 = "0.21.0"
rusqlite = "0.29.0"
uuid = { version = "1.3.1", features = ["v4"] }
ubyte = "0.10.3"
rocket_contrib = "0.4.11"

26
api/LICENSE Normal file
View File

@ -0,0 +1,26 @@
BSD 2-Clause License
Copyright (c) 2021, Will Webberley
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -1,15 +1,17 @@
// Copyright (c) 2021, Will Webberley. See LICENSE.
#![feature(proc_macro_hygiene, decl_macro)]
#[macro_use] extern crate rocket_contrib;
use std::{env, str, fs::{self, File}, time::{SystemTime, UNIX_EPOCH}, io::Write};
use rocket::{self, routes, get, post, put, config::{Config, Limits}, response::Responder, request::{self, Request, FromRequest}, Outcome, http::Status};
use std::{env, str, fs::{self, File}, time::{SystemTime, UNIX_EPOCH}, io::Write, collections::HashSet};
use rocket::{self, routes, get, post, put, catch, config::{Config, Limits}, response::Responder, request::{self, Request, FromRequest}, Outcome, http::Status};
use rocket_contrib::json::{Json, JsonValue};
use serde::{Serialize, Deserialize};
use jsonwebtoken::{self, encode, decode, Header, Validation, EncodingKey, DecodingKey};
use jsonwebtoken::{self, encode, decode, Header, Validation, EncodingKey, DecodingKey, Algorithm};
use rusqlite::{params, Connection};
use flate2::read::GzDecoder;
use tar::Archive;
use base64;
use base64::{Engine as _, engine::general_purpose};
use uuid::Uuid;
const DB_FILE: &str = "/usr/local/var/capsules_db/db.db3";
@ -71,10 +73,9 @@ fn generate_access_token(capsule_id: &str) -> Result<String> {
// Verify the access JWT token, and checks still valid in DB. Returns the authenticated capsule.
fn validate_access_token(token: &str) -> Result<AuthenticatedCapsule> {
// Validate the token integrity itself
let my_validation = Validation {
validate_exp: false,
..Default::default()
};
let mut my_validation = Validation::new(Algorithm::HS256);
my_validation.validate_exp = false;
my_validation.required_spec_claims = HashSet::new();
let claims = decode::<Claims>(&token, &DecodingKey::from_secret(get_jwt_secret().as_ref()), &my_validation)?.claims;
// Verify the token in the DB and retrieve the capsule info
@ -103,6 +104,11 @@ enum Resp {
NotFound(JsonValue),
}
#[catch(400)]
fn bad_request(_: &Request) -> Resp {
return Resp::BadRequest(json!({"message": "There was a problem with your request"}));
}
#[get("/")]
fn index() -> Resp {
return Resp::NotFound(json!({"message": "There is no resource available at this path."}));
@ -230,7 +236,7 @@ struct CapsuleDeployRequest {
#[put("/capsule", data = "<capsule>")]
fn deploy_capsule(capsule: Json<CapsuleDeployRequest>, authenticated_capsule: AuthenticatedCapsule) -> Resp {
// Decode base64-encoded JSON capsule_archive. This gives a Vec<u8>
let archive_vec: Vec<u8> = match base64::decode(&capsule.capsule_archive) {
let archive_vec: Vec<u8> = match general_purpose::STANDARD.decode(&capsule.capsule_archive) {
Ok(x) => x,
Err(_) => return Resp::BadRequest(json!({"message": "Your capsule archive is not valid"})),
};
@ -265,6 +271,30 @@ fn deploy_capsule(capsule: Json<CapsuleDeployRequest>, authenticated_capsule: Au
return Resp::Ok(json!({"message": "Your capsule was deployed", "url": url}));
}
#[derive(Serialize)]
struct CapsuleViewLog {
path: String,
timestamp: i32,
}
#[get("/logs")]
fn get_logs(authenticated_capsule: AuthenticatedCapsule) -> Resp {
let conn = match get_db() {
Ok(c) => c,
Err(_) => return Resp::InternalServerError(json!({"message": "Database problem"})),
};
let mut stmt = conn
.prepare("SELECT path, timestamp FROM view WHERE capsule_id = ?1 ORDER BY timestamp DESC LIMIT 100")
.unwrap();
let view_results = stmt.query_map(params![authenticated_capsule.id], |row| {
Ok(CapsuleViewLog {
path: row.get(0)?,
timestamp: row.get(1)?,
})
}).unwrap();
let views: Vec<CapsuleViewLog> = view_results.map(|view| view.unwrap()).collect();
return Resp::Ok(json!({"logs": &views}));
}
fn main() {
// Create needed directories
fs::create_dir_all(CAPSULE_DIR).unwrap_or_default();
@ -274,5 +304,5 @@ fn main() {
my_config.set_port(9000);
my_config.set_limits(Limits::new().limit("json", 50 * 1024 * 1024)); // 50MB
let app = rocket::custom(my_config);
app.mount("/", routes![index, create_capsule, deploy_capsule]).launch();
app.mount("/", routes![index, create_capsule, deploy_capsule, get_logs]).launch();
}

1375
cli/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,5 @@
[package]
name = "gem-client"
name = "captown"
version = "0.1.0"
authors = ["Will Webberley <me@wilw.dev>"]
edition = "2018"
@ -10,10 +10,13 @@ edition = "2018"
flate2 = "1.0.19"
tar = "0.4.30"
base64 = "0.13.0"
openssl = "0.10.32"
clap = "2.33.3"
dirs = "3.0.1"
tempfile = "3.2.0"
reqwest = { version = "0.11", features = ["json", "blocking"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde_yaml = "0.8"
chrono = "0.4.24"
prettytable = "0.10.0"
openssl = { version = "0.10", features = ["vendored"] }

26
cli/LICENSE Normal file
View File

@ -0,0 +1,26 @@
BSD 2-Clause License
Copyright (c) 2021, Will Webberley
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

17
cli/README.md Normal file
View File

@ -0,0 +1,17 @@
# capsule.town CLI client
## Building
To cross-compile Linux:
````
docker run --rm --user "$(id -u)":"$(id -g)" -v "$PWD":/usr/src/app -w /usr/src/app rust cargo build --release
```
## Release
Upload to object storage:
```
3cmd put target/release/captown s3://capsuletown/cli/captown-linux-0.1 --acl-public
```

View File

@ -1,7 +1,12 @@
// Copyright (c) 2021, Will Webberley. See LICENSE
use std::{env, str, io::{stdin,stdout,Write}, collections::HashMap, fs::{self, File}, path::{Path, PathBuf}};
use clap::{Arg, App, SubCommand};
use flate2::{Compression, write::GzEncoder};
use serde::{Serialize, Deserialize};
use tempfile::tempdir;
use chrono::prelude::*;
use prettytable::{Table, row};
use dirs;
use base64;
use reqwest;
@ -128,8 +133,11 @@ struct DeployResponse {
url: String,
}
// PUTs the base64-encoded capsule tarball to the server.
fn publish_capsule() -> Result<bool>{
let capsule_name: String = prompt_for("Enter the name for this capsule");
fn publish_capsule(flag_capsule_name: Option<&str>) -> Result<bool>{
let capsule_name: String = match flag_capsule_name {
Some(x) => String::from(x),
None => prompt_for("Enter the name for this capsule"),
};
let capsule_config = read_config_capsule(&capsule_name).expect("The configuration for your specified capsule could not be found.");
let base64_encoded_capsule = match read_directory_as_string() {
@ -199,6 +207,81 @@ fn create_capsule() -> Result<bool>{
}
}
#[derive(Deserialize, Debug)]
struct LogsResponse {
logs: Option<Vec<Log>>,
message: Option<String>,
}
#[derive(Deserialize, Debug)]
struct Log {
path: String,
timestamp: i64,
}
impl Log {
fn time(&self) -> String {
let naive = NaiveDateTime::from_timestamp_opt(self.timestamp, 0).unwrap();
let datetime: DateTime<Utc> = DateTime::from_utc(naive, Utc);
let newdate = format!("{}", datetime.format("%Y-%m-%d %H:%M:%S"));
return newdate;
}
}
// PUTs the base64-encoded capsule tarball to the server.
fn capsule_logs(flag_capsule_name: Option<&str>, flag_number_of_logs: Option<&str>) -> Result<bool>{
let capsule_name: String = match flag_capsule_name {
Some(x) => String::from(x),
None => prompt_for("Which capsule do you want to view logs for?"),
};
let capsule_config = match read_config_capsule(&capsule_name) {
Ok(x) => x,
Err(x) => {
println!("{:?}", x);
return Err(Box::from("Unable to get logs"));
},
};
let number_of_logs: usize = match flag_number_of_logs {
Some(x) => {
let required = match x.parse::<usize>() {
Ok(y) => y,
Err(_) => return Err(Box::from("Log count must be a number")),
};
if required > 100 {
return Err(Box::from("Maximum log count is 100"));
}
required
},
None => 20,
};
let client = reqwest::blocking::Client::new();
let res = client.get(&format!("{}/logs", get_api_url()))
.header("api-key", &capsule_config.access_token).send()?;
let status = res.status();
let response_data = res.json::<LogsResponse>()?;
if status.is_success() {
match response_data.logs {
None => println!("No logs found"),
Some(mut logs) => {
logs.truncate(number_of_logs);
logs.reverse();
let mut table = Table::new();
table.add_row(row!["TIME", "CAPSULE", "PATH"]);
for log in logs {
table.add_row(row![log.time(), capsule_name, log.path]);
}
table.printstd();
},
}
return Ok(true);
} else {
let error_message = match response_data.message {
None => String::from("Unknown error"),
Some(message) => message,
};
println!("Unable to get the logs for your capsule: {}", &error_message);
return Err(Box::from("Unable to get logs"));
}
}
fn main() {
// Setup config
@ -206,24 +289,52 @@ fn main() {
println!("{}", e);
}
// Parse command
let args: Vec<String> = env::args().collect();
if args.len() < 2 {
println!("Please specify a command");
return;
}
let command = &args[1];
// Read command-line commands/args
let app_args = App::new("Capsule.Town")
.version("0.2")
.author("Will W. (wilw.dev)")
.about("Publishes your capsule to capsule.town")
.subcommand(SubCommand::with_name("create")
.about("Creates a new capsule"))
.subcommand(SubCommand::with_name("publish")
.about("Publishes your capsule")
.arg(Arg::with_name("capsule")
.short("c")
.long("capsule")
.takes_value(true)
.help("Name of the capsule to be published")))
.subcommand(SubCommand::with_name("logs")
.about("View access logs for your capsule")
.arg(Arg::with_name("capsule")
.short("c")
.long("capsule")
.takes_value(true)
.help("Name of the capsule to view logs for"))
.arg(Arg::with_name("number")
.short("n")
.takes_value(true)
.help("Number of logs to display (default: 20, maximum: 100)"))
);
let matches = app_args.get_matches();
// Handle command
if let Err(err) = match command.as_str() {
"create" => create_capsule(),
"publish" => publish_capsule(),
// Handle commands
if let Err(err) = match matches.subcommand() {
("create", Some(_)) => create_capsule(),
("publish", Some(sub_match)) => {
let capsule_name = sub_match.value_of("capsule");
publish_capsule(capsule_name)
},
("logs", Some(sub_match)) => {
let capsule_name = sub_match.value_of("capsule");
let number_of_logs = sub_match.value_of("number");
capsule_logs(capsule_name, number_of_logs)
},
_ => {
println!("Unknown command: {}. Available commands: create, publish", &command);
println!("Unknown command. Available commands: create, publish, logs");
Err(Box::from("Unable to continue"))
},
} {
println!("Program is exiting with error. {}", err);
println!("Exiting with error: {}", err);
std::process::exit(1);
}
}

3
home/README.md Normal file
View File

@ -0,0 +1,3 @@
# capsule.town home
This directory contains the Gemini files that form the gemini://capsule.town homepage.

58
home/capsule/index.gmi Normal file
View File

@ -0,0 +1,58 @@
```
_
| |
___ __ _ _ __ ___ _ _| | ___
/ __/ _` | '_ \/ __| | | | |/ _ \
| (_| (_| | |_) \__ | |_| | | __/_
\___\__,_| .__/|___/\__,_|_|\___(_)
| | | |
| |_ _____|_| ___ __
| __/ _ \ \ /\ / | '_ \
| || (_) \ V V /| | | |
\__\___/ \_/\_/ |_| |_|
```
# Welcome to capsule.town!
🌆 capsule.town is a safe and free place for hobbyists, gem-loggers, and everyone else to host their static Gemini capsules and gemlogs.
✨ No sign-up is required and your capsule can be live within seconds.
✨ You get a free Geminispace hosted at <yoursubdomain>.capsule.town.
✨ You can publish entirely anonymously.
🚧 This project is currently in open beta. Feedback, bug-reports, and contributions are welcome. Please see below for ways to get in touch.
=> gemini://capsule.town/start.gmi Get started
This project is open-source, and contributions are welcome. Likewise, you are free to run the code yourself to host your own Gemini constellation.
=> https://git.wilw.dev/wilw/capsule-town Project source
## Why host your content on capsule.town?
capsule.town is free and aims to offer a convenient space for people that would like a Geminispace but don't want to or can't host it themselves.
Think of this service as the Gemini version of static site hosting services like Netlify or Vercel. You can publish your capsule at any time using a simple client without needing to worry about provisioning and maintaining servers.
=> gemini://capsule.town/start.gmi Get started
## Why is it free?
capsule.town does not generate any income, and I pay for the servers and upkeep myself.
This is one way I can give back to the indie and open-source community. Your data is not sold. You can view and evaluate the source yourself (there is a link above).
By taking part you are certainly not the product; you are a member of the community.
## What other options do I have?
We know that this type of service isn't for everybody. If you want a Geminispace but do not want to use capsule.town, there are other options available. For example:
=> gemini://gem.limpet.net/agate Host a space yourself using Agate
=> https://tildeverse.org Many `tildeverse` servers offer Gemini hosting
## Get in touch
Check out my own capsule for ways to get in touch:
=> gemini://wilw.capsule.town My Capsule

27
home/capsule/rules.gmi Normal file
View File

@ -0,0 +1,27 @@
# capsule.town: Rules
=> gemini://capsule.town Return home
This page describes the rules applying to all capsules hosted on capsule.town.
By creating and publishing your capsule, you declare that you agree to these rules.
Please note that capsule.town offers no warranty/SLA or anything else of this type. There is no legally binding agreement between the owners and the users of the service. It is a free service that aims to give back to the community, so treat is as such. The service or its owners are not liable for any loss of time/money/resource that may be incurred from use of the service or lack thereof.
The admins and owners are not responsible for the content generated and posted by its users.
## Rules
capsule.town aims to be a safe space for everyone. Its purpose is to provide a space for people to host constructive, useful content, personal items, portfolios, and so on.
This is a space for free speech, where it is constructive. In general, the "don't be a jerk" rules apply. Also:
* Don't use the service in any way that may disrupt the underlying servers, or the experience for other people.
* Don't post hate-speech or content that aims to discriminate against individuals or groups of people based on their race, ethnicity, gender, sexuality, religion or other characteristics.
* Don't harass or bully others or post content that falsely defames someone or some entity (unless done in a constructive way).
* Don't post content or links to content that is/are illegal in the EU or the US.
The server admins reserve the right to remove any content without warning, and at their sole discretion.
=> gemini://capsule.town Return home
=> gemini://capsule.town/start.gmi Get started

122
home/capsule/start.gmi Normal file
View File

@ -0,0 +1,122 @@
# capsule.town: Getting started
=> gemini://capsule.town Return home
🚀 We're excited for you to join capsule.town!
🚧 Please note that this service is currently in early beta. Please get in touch if you have any feedback, bug reports, or would like to contribute.
## Intro
Publishing your capsule is handled through a HTTP API, however we recommend most people make use of our official wrapper client, which is described below.
🗒 Note: whilst we encourage use of the Gemini protocol for publishing and consuming text content, we do use HTTPS for our management API. The HTTP/S protocol offers the useful tooling required for such management, and it's more about using the right tool for the job 🙂
## Getting started guide
### 1. Download the client
We recommend most people download and make use of the capsule.town client, called `captown`, for publishing their capsule. However, if you'd rather use your own tooling (cURL, or something else) you can interface directly with the service using HTTPS (documentation for this is coming soon).
The current version of the client is: 0.1. Replace the version numbers in commands below to get the latest version.
You can use CURL to get the client, as described below.
For Linux:
```
➡️ curl https://download.capsule.town/captown-linux-0.2 -o captown
```
For Mac:
```
➡️ curl https://download.capsule.town/captown-mac-0.2 -o captown
```
We do not yet have a Windows version, but contributions are welcome.
Once downloaded:
1. Mark the binary as executable: `chmod +x captown`
2. Move the binary to a location of your choosing: e.g. `sudo mv captown /usr/local/bin`.
The rest of this guide assumes the binary is somewhere on your `PATH` and can be invoked using `captown` (as above).
🗒 Note: the source code of the binary can be examined. Please find a link to the source from the project homepage.
### 2. Prepare your capsule
You can now create a new capsule. To do so, create a new project directory (e.g. `~/projects/my_capsule`) and change into it:
```
➡️ mkdir -p ~/projects/my_capsule
➡️ cd ~/projects/my_capsule
```
Now create a `capsule/` directory inside this project - this is where your Gemini files will be stored. The client looks for a `capsule/` directory when publishing your capsule:
```
➡️ mkdir capsule
```
Create a new file called `capsule/index.gmi` and write a welcome message inside:
```
➡️ echo "# Hello, world" > capsule/index.gmi
```
### 3. Create your capsule
By creating and publishing your capsule on capsule.town, you agree to the capsule.town rules.
=> gemini://capsule.town/rules.gmi Check out the rules
The next step is to create your new capsule on capsule.town. To do so, run:
```
➡️ captown create
```
The client will ask for a few details:
* Name: the name of your capsule. This will be used as the subdomain, and will host your capsule at `<name>.capsule.town`. It must only contain letters and numbers and it must be unique.
* Contact details: _this is optional_. If you like, you can leave an email, Telegram handle, or another contact identifier. We only use this when verifying ownership in case you lose your access credentials.
Once done, your capsule will be created and the access key will be written to `~/.capsules.yaml`.
⚠️ **IMPORTANT: Please remember to backup this file (e.g. to your password manager). If you lose your access keys you will not be able to publish any new updates.**
### 4. Publish your capsule
Finally, you can publish your capsule.
Run `captown publish` inside your project directory. The program will ask for your capsule name to create the deployment.
Alternatively you can run the following to publish your capsule non-interactively:
```
➡️ captown publish -c mycapsule
```
Whichever route you choose, if all goes well you'll then be able to visit `gemini://<name>.capsule.town` using your Gemini client to view your new capsule.
🗒 Note: the `publish` command must be run from the root of your project (i.e. the directory containing the `capsule/` sub-directory).
### 5. View your capsule's logs
You can see an access log of your capsule by running the `logs` command:
```
➡️ captown logs -c mycapsule
```
View other options for this command by running `captown help logs`.
## Making updates
When you've made updates that you want to publish, simply run `captown publish` again to make them live.
Side note: we recommend committing your capsule project directory to source control as you would for any other project.
=> gemini://capsule.town Return home

117
server/Cargo.lock generated
View File

@ -1,5 +1,7 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "agate"
version = "2.3.0"
@ -10,12 +12,24 @@ dependencies = [
"mime_guess",
"once_cell",
"percent-encoding",
"rusqlite",
"rustls",
"tokio",
"tokio-rustls",
"url",
]
[[package]]
name = "ahash"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47"
dependencies = [
"getrandom",
"once_cell",
"version_check",
]
[[package]]
name = "atty"
version = "0.2.14"
@ -39,6 +53,12 @@ version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
[[package]]
name = "bitflags"
version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24a6904aef64d73cf10ab17ebace7befb918b82164785cb89907993be7f83813"
[[package]]
name = "bumpalo"
version = "3.4.0"
@ -81,6 +101,18 @@ dependencies = [
"termcolor",
]
[[package]]
name = "fallible-iterator"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7"
[[package]]
name = "fallible-streaming-iterator"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
[[package]]
name = "form_urlencoded"
version = "1.0.0"
@ -100,6 +132,35 @@ dependencies = [
"unicode-width",
]
[[package]]
name = "getrandom"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4"
dependencies = [
"cfg-if 1.0.0",
"libc",
"wasi",
]
[[package]]
name = "hashbrown"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
dependencies = [
"ahash",
]
[[package]]
name = "hashlink"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69fe1fcf8b4278d860ad0548329f892a3631fb63f82574df68275f34cdbe0ffa"
dependencies = [
"hashbrown",
]
[[package]]
name = "hermit-abi"
version = "0.1.17"
@ -143,9 +204,19 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
version = "0.2.82"
version = "0.2.142"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89203f3fba0a3795506acaad8ebce3c80c0af93f994d5a1d7a0b1eeb23271929"
checksum = "6a987beff54b60ffa6d51982e1aa1146bc42f19bd26be28b0586f252fccf5317"
[[package]]
name = "libsqlite3-sys"
version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "afc22eff61b133b115c6e8c74e818c628d6d5e7a502afea6f64dee076dd94326"
dependencies = [
"pkg-config",
"vcpkg",
]
[[package]]
name = "log"
@ -228,9 +299,9 @@ dependencies = [
[[package]]
name = "once_cell"
version = "1.5.2"
version = "1.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13bd41f508810a131401606d54ac32a467c97172d74ba7662562ebba5ad07fa0"
checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3"
[[package]]
name = "percent-encoding"
@ -244,6 +315,12 @@ version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439697af366c49a6d0a010c56a0d97685bc140ce0d377b13a2ea2aa42d64a827"
[[package]]
name = "pkg-config"
version = "0.3.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160"
[[package]]
name = "proc-macro2"
version = "1.0.24"
@ -277,6 +354,20 @@ dependencies = [
"winapi",
]
[[package]]
name = "rusqlite"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "549b9d036d571d42e6e85d1c1425e2ac83491075078ca9a15be021c56b1641f2"
dependencies = [
"bitflags",
"fallible-iterator",
"fallible-streaming-iterator",
"hashlink",
"libsqlite3-sys",
"smallvec",
]
[[package]]
name = "rustls"
version = "0.19.0"
@ -300,6 +391,12 @@ dependencies = [
"untrusted",
]
[[package]]
name = "smallvec"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
[[package]]
name = "socket2"
version = "0.3.19"
@ -435,12 +532,24 @@ dependencies = [
"percent-encoding",
]
[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "version_check"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed"
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasm-bindgen"
version = "0.2.69"

View File

@ -21,6 +21,7 @@ once_cell = "1.4"
percent-encoding = "2.1"
rustls = "0.19.0"
url = "2.1"
rusqlite = "0.29.0"
[profile.release]
lto = true

View File

@ -14,6 +14,7 @@ use {
net::SocketAddr,
path::Path,
sync::Arc,
time::{SystemTime, UNIX_EPOCH},
},
tokio::{
io::{AsyncReadExt, AsyncWriteExt},
@ -22,8 +23,16 @@ use {
},
tokio_rustls::{TlsAcceptor, server::TlsStream},
url::{Host, Url},
rusqlite::{Connection},
};
const DB_FILE: &str = "/usr/local/var/capsules_db/db.db3";
// Utility function to retrieve a connection to the database
fn get_db() -> Result<Connection, rusqlite::Error> {
return Ok(Connection::open(DB_FILE)?);
}
fn main() -> Result {
if !ARGS.silent {
env_logger::Builder::new().parse_filters("info").init();
@ -243,7 +252,7 @@ async fn send_response(url: Url, stream: &mut TlsStream<TcpStream>) -> Result {
}
}
}
// Make sure the file opens successfully before sending the success header.
let mut file = match tokio::fs::File::open(&path).await {
Ok(file) => file,
@ -263,6 +272,26 @@ async fn send_response(url: Url, stream: &mut TlsStream<TcpStream>) -> Result {
// Send body.
tokio::io::copy(&mut file, stream).await?;
// Capture the view
#[derive(Debug)]
struct Capsule {
id: String,
}
let conn = get_db().unwrap();
let mut stmt = conn.prepare(&format!("SELECT id, name FROM capsule WHERE name = '{}'", &subdomain))?;
let mut capsule_cursor = stmt.query_map([], |row| {
Ok(Capsule {
id: row.get(0)?,
})
})?;
let capsule = capsule_cursor.next().unwrap()?;
let timestamp = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
_ = conn.execute("CREATE TABLE IF NOT EXISTS view (capsule_id TEXT, path TEXT, timestamp INTEGER)", ());
conn.execute("INSERT INTO view (capsule_id, path, timestamp) VALUES (?1, ?2, ?3)",
(&capsule.id, &url.path(), timestamp),
)?;
Ok(())
}