v0.1.0 -- serve gemtext files

This commit is contained in:
f 2022-01-05 17:02:05 -03:00
commit 53b14ad888
10 changed files with 449 additions and 0 deletions

5
.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
/docs/
/lib/
/bin/
/.shards/
*.dwarf

168
LICENSE Normal file
View file

@ -0,0 +1,168 @@
Copyright (c) 2022 Sutty
The following license is modified from the MIT license and downloaded
from <https://github.com/Laurelai/anti-fascist-mit-license> on
2019-07-11.
Anti-Fascist MIT License:
The following conditions must be met by any person obtaining a copy of
this software:
- You MAY NOT be a fascist.
- You MUST not financially support fascists.
- You MUST not intentionally provide or knowingly provide through
inaction a platform for fascists to spread propaganda or organize.
- You MUST not publicly voice support for fascists.
- You MAY NOT be a member of any fascist organization, even if you are a
member to infiltrate for anti-fascist purposes.
"Fascist" can be understood as any group or individual who promotes the
political ideology of fascism.
"Fascism" can be broken down into 11 ideological features as well as 8
tactics that can form a fascist system in varying combinations, for the
sake of simplicity and brevity the individual or organization in
question must match to at least 5 features or tactics or a combination
of the two determined by the individual licencer.
Said licencer may provide a list if an individual or group matches to at
least 5 features upon request from the individual or group in question.
The ideological features are listed below.
1. Hyper-nationalism.
As defined as "The belief in the superiority of one's nation and of the
paramount importance of advancing it."
2. Militarism.
As defined as "Advocating for an increase in military forces beyond what
the real defense of a nation needs, more influence of the military upon
the policies of the civilian government, and a preference for force as a
solution over diplomacy for problems."
3. Glorification of violence and readiness to use it in politics.
As defined as "The belief that violence can be used to cleanse a
tarnished nation, also by using violence to harm, intimidate or kill
political oppoenents."
4. Fetishization of youth.
As defined as "Extolling the virtues of youth and making a special
appeal to young people to join a cause or organization"
5. Fetishization of masculinity.
As defined as "Extolling the virtues of male authority or patriarchy and
making a special appeal to men to be leaders of households and groups"
6. Leader cult.
As defined as "Creating an idealized, heroic, and worshipful image of a
leader, often through unquestioning flattery and praise."
7. Lost-golden-age syndrome.
As defined as "Creating or promoting the idea that a nation had a lost
or stolen golden age in the past that must be returned to"
8. Self-definition by opposition.
As defined as "Creating or promoting the idea that the group or
individual is the only person or way who can fight real or imagined
evils within a society."
9. Mass mobilization and mass party.
As defined as "Creating or promoting the creation of a populist group or
party for the advancment of fascist tactics or features."
10. Hierarchical party structure and tendency to purge the disloyal.
As defined as "Removal of membership from a group for lacking absolute
loyalty or lacking further usefulness to the group. Also having a
hierarchical structure within the group itself."
11. Theatricality.
As defined as "Using spectacle to gain and keep the attention of those
inside and outside of the group using speeches full of absolutes and or
superlatives. Elaborate collective rituals (rallies) meant to reenforce
loyalty within the group."
Fascist tactics include
1) Persecution of national minorities.
2) Persecution of racial minorities.
3) Persecution of religious minorities (Anti-Semitism, Islamophobia and others).
4) Promotion of a type of national purity.
5) Promotion of a state run by ideologically oriented corporate bodies.
6) Persecution of gender or sexual minorities.
7) Persecution of the disabled.
8) Formation of extra-legal forces (brownshirts) to defend fascist values.
Special criteria: Meeting only one point of the special criteria is
enough to consider someone or a group to be fascist for the purposes of
this licence.
1. Promotion of any theories that state members of the jewish ethnicity
or faith control or largely control the world, finance, or other
global major power system.
2. Denial of the holocaust or any other historically proven genocide.
3. Promotion of ethnostates.
4. Advocating for eugenics. Either positive or negative eugenics.
Promotion for the rights of abortion are not considered eugenics.
5. Advocating for the removal of rights or legal protections from a
class or group of people.
Former fascists: People or organizations who used to promote the
political ideology of fascism but no longer do so must meet the
following criterea to be able to use this software.
1. Publicly disavow past fascist deeds and ideologies.
2. Expose any and all known fascists former allies to the public.
A suggested route would be through the one peoples project
(onepeoplesproject.com). If they can confirm you have done so that
will count as meeting condition two.
3. Publicly destroy any and all fascist paraphenelia you have in your
posession including removal of tattoos and body markings
affiliated with fascist groups or gangs.
ANTI-FASCIST-MIT LICENSE:
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.
The above licence agreement conditions are met in full.
The Anti-Fascist MIT License may only be used under the terms of the
Anti-Fascist MIT License.
Any modified versions of this software must also include the
Anti-Fascist MIT Licence.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

11
Makefile Normal file
View file

@ -0,0 +1,11 @@
PREFIX ?= /usr/local
entry := src/gemini.cr
sources := $(wildcard src/*/*.cr)
gemini: $(entry) $(sources)
crystal build --release $<
strip --strip-all $@
install: gemini
install -m 755 $< $(PREFIX)/bin/$<

75
README.md Normal file
View file

@ -0,0 +1,75 @@
# gemini
This is a Gemini server for ad-hoc hosting. It depends on a TLS
termination service like Nginx or HAProxy. This allow us to configure
TLS like we need to and offload safety to a better reviewed project.
## Installation
```bash
crystal build --release src/gemini.cr
strip --strip-all ./gemini
install -m 755 ./gemini /usr/local/bin/gemini
# Or
make gemini
sudo make install
```
## Usage
`gemini` runs on port 19650/tcp without security on. Make sure to keep
this port closed on your firewall, but open 1965/tcp for TLS
connections.
`gemini` expects to serve files from the directory it's run from, and
serves several hosts under their own directory.
```bash
# Enter the directory you're hosting capsules from
cd /srv/gemini
# Create a directory for the capsule
install -dm 750 -o user -g gemini gemini.sutty.coop.ar
# Create a Gemtext file
echo "Hi" > gemini.sutty.coop.ar/index.gmi
# Run the server
gemini
```
Configure your Nginx server to proxy Gemini requests to
`gemini`. There's an example configuration on `contrib/`.
To test your setup, visit <gemini://gemini.sutty.coop.ar>
### Self-signed certificates
The example configuration reuses the Let's Encrypt certificates we
already issue at [Sutty](https://sutty.coop.ar/) for our sites. If you
want to use a self-signed certificate, GnuTLS provides the simplest way.
```bash
certtool --generate-privkey --outfile gemini.key
certtool --generate-self-signed --load-privkey gemini.key --outfile gemini.crt
```
The only fields required are `CN` for your domain, and a certification
duration in days, you can press Enter for everything else.
## Development
You'll need to install Crystal ~>1.2.2.
[Gemini Specification v0.16.0](https://gemini.circumlunar.space/docs/specification.gmi)
## Contributing
```bash
# 1. Fork it
git clone https://gitea.sutty.coop.ar/Sutty/gemini.git
cd gemini
git remote rename origin upstream
# 2. Work on your changes
# [...]
# 3. Send patches
git format-patch upstream
git send-email --to=f@sutty.coop.ar *.patch
```

25
contrib/nginx.conf Normal file
View file

@ -0,0 +1,25 @@
# This requires the `stream` module. This sections goes on the main
# nginx.conf or at least outside the `http` section. Run `nginx -t` to
# test changes.
stream {
server {
# Listen on port 1965, with mandatory TLS.
listen 1965 ssl;
# Run only these protocols.
ssl_protocols TLSv1.2 TLSv1.3;
# Other TLS options could go here.
# The variable $ssl_server_name dynamically loads a certificate for
# any domain name that points to this server.
#
# No need to send the full chain since Gemini clients only want to
# validate the CommonName field.
ssl_certificate /etc/letsencrypt/live/$ssl_server_name/cert.pem;
ssl_certificate_key /etc/letsencrypt/live/$ssl_server_name/privkey.pem;
# After TLS session is started, proxy everything to `gemini`.
proxy_pass 127.0.0.1:19650;
}
}

13
shard.yml Normal file
View file

@ -0,0 +1,13 @@
name: gemini
version: 0.1.0
authors:
- f <f@sutty.coop.ar>
targets:
gemini:
main: src/gemini.cr
crystal: 1.2.2
license: MIT-Antifa

8
src/gemini.cr Normal file
View file

@ -0,0 +1,8 @@
require "./gemini/server"
module Gemini
class Error < ::ArgumentError; end
end
gemini = Gemini::Server.new(19650)
gemini.start

48
src/gemini/request.cr Normal file
View file

@ -0,0 +1,48 @@
require "uri"
# Processes a Gemini request
class Gemini::Request
getter server : Server
getter connection : IO
getter uri : URI
getter path : Path
getter host : Path
def initialize(@server, @connection, uri : String)
@uri = URI.parse uri
if @uri.host.nil? || @uri.host.try(&.blank?)
raise Error.new("Host can't be empty")
end
@host = @server.pwd / Path[@uri.host || "doesnt_exist"]
@path = process_path
end
# Validates the request by raising exceptions
def validate!
unless host_exist?
raise Error.new("Missing host directory #{@uri.host} at #{Dir.current}, you may need to create and populate it")
end
if malicious_path?
raise Error.new("#{path} is outside served directory #{@host}")
end
end
def host_exist? : Bool
File.directory? @host
end
# Path must be inside served directory
def malicious_path? : Bool
!path.to_s.starts_with?(@host.to_s)
end
private def process_path : Path
path = (@host / Path[@uri.path]).normalize
path = path / Path["index.gmi"] if File.directory? path
path
end
end

46
src/gemini/response.cr Normal file
View file

@ -0,0 +1,46 @@
# Gemini response
class Gemini::Response
getter server : Server
getter connection : IO
getter request : Request
property status : Int32 = 20
property meta : String = "text/gemini"
def initialize(@server, @connection, @request)
end
# Send the file
def send : Nil
unless exists?
@status = 51
@meta = "Not found"
end
if removed?
@status = 52
@meta = "Gone"
end
@server.puts @connection, "#{@status} #{@meta}"
Log.info { "#{@request.host}: #{@status} #{@meta} #{@request.path}" }
send_file if @status == 20
end
def exists? : Bool
File.exists? @request.path
end
# Files can be specifically removed by appending .remove at the end.
def removed? : Bool
File.exists? "#{@request.path}.remove"
end
# Sends the file by copying its contents
private def send_file : Nil
IO.copy File.open(@request.path), @connection
end
end

50
src/gemini/server.cr Normal file
View file

@ -0,0 +1,50 @@
require "socket"
require "log"
require "./request"
require "./response"
class Gemini::Server
getter server : TCPServer
getter port : Int32
getter pwd : Path
def initialize(@port : Int32 = 1965)
@server = TCPServer.new(@port)
@pwd = Path[Dir.current]
Log.info { "Gemini server started at port #{@port}" }
end
# Runs the server and starts waiting for connections
def start : Nil
while connection = @server.accept?
begin
uri = connection.gets
# Client must send URI first
unless uri.nil? || uri.blank?
request = Request.new(self, connection, uri)
response = Response.new(self, connection, request)
request.validate!
response.send
else
raise Error.new("URI is empty")
end
rescue ex
puts connection, "40 Error"
Log.error(exception: ex) { "Gemini::Error" }
end
# Always close the connection
connection.close
end
end
# Sends a message through the connection
def puts(connection : IO, message : String) : Nil
connection.puts message
connection.puts "\r\n"
end
end