From 53b14ad8885b9973f1ccc2f8c0b20b650fb74f5a Mon Sep 17 00:00:00 2001 From: f Date: Wed, 5 Jan 2022 17:02:05 -0300 Subject: [PATCH] v0.1.0 -- serve gemtext files --- .gitignore | 5 ++ LICENSE | 168 +++++++++++++++++++++++++++++++++++++++++ Makefile | 11 +++ README.md | 75 ++++++++++++++++++ contrib/nginx.conf | 25 ++++++ shard.yml | 13 ++++ src/gemini.cr | 8 ++ src/gemini/request.cr | 48 ++++++++++++ src/gemini/response.cr | 46 +++++++++++ src/gemini/server.cr | 50 ++++++++++++ 10 files changed, 449 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100644 contrib/nginx.conf create mode 100644 shard.yml create mode 100644 src/gemini.cr create mode 100644 src/gemini/request.cr create mode 100644 src/gemini/response.cr create mode 100644 src/gemini/server.cr diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0bb75ea --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/docs/ +/lib/ +/bin/ +/.shards/ +*.dwarf diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4208b12 --- /dev/null +++ b/LICENSE @@ -0,0 +1,168 @@ +Copyright (c) 2022 Sutty + +The following license is modified from the MIT license and downloaded +from 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. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..4075a51 --- /dev/null +++ b/Makefile @@ -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/$< diff --git a/README.md b/README.md new file mode 100644 index 0000000..e07161f --- /dev/null +++ b/README.md @@ -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 + +### 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 +``` diff --git a/contrib/nginx.conf b/contrib/nginx.conf new file mode 100644 index 0000000..1d70d6e --- /dev/null +++ b/contrib/nginx.conf @@ -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; + } +} diff --git a/shard.yml b/shard.yml new file mode 100644 index 0000000..b276579 --- /dev/null +++ b/shard.yml @@ -0,0 +1,13 @@ +name: gemini +version: 0.1.0 + +authors: +- f + +targets: + gemini: + main: src/gemini.cr + +crystal: 1.2.2 + +license: MIT-Antifa diff --git a/src/gemini.cr b/src/gemini.cr new file mode 100644 index 0000000..6949e32 --- /dev/null +++ b/src/gemini.cr @@ -0,0 +1,8 @@ +require "./gemini/server" + +module Gemini + class Error < ::ArgumentError; end +end + +gemini = Gemini::Server.new(19650) +gemini.start diff --git a/src/gemini/request.cr b/src/gemini/request.cr new file mode 100644 index 0000000..b49823e --- /dev/null +++ b/src/gemini/request.cr @@ -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 diff --git a/src/gemini/response.cr b/src/gemini/response.cr new file mode 100644 index 0000000..f9bef49 --- /dev/null +++ b/src/gemini/response.cr @@ -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 diff --git a/src/gemini/server.cr b/src/gemini/server.cr new file mode 100644 index 0000000..8bc80f9 --- /dev/null +++ b/src/gemini/server.cr @@ -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