v0.1.0 -- serve gemtext files
This commit is contained in:
commit
53b14ad888
10 changed files with 449 additions and 0 deletions
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
/docs/
|
||||||
|
/lib/
|
||||||
|
/bin/
|
||||||
|
/.shards/
|
||||||
|
*.dwarf
|
168
LICENSE
Normal file
168
LICENSE
Normal 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
11
Makefile
Normal 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
75
README.md
Normal 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
25
contrib/nginx.conf
Normal 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
13
shard.yml
Normal 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
8
src/gemini.cr
Normal 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
48
src/gemini/request.cr
Normal 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
46
src/gemini/response.cr
Normal 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
50
src/gemini/server.cr
Normal 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
|
Loading…
Reference in a new issue