486 lines
12 KiB
Go
486 lines
12 KiB
Go
/*
|
|
MastoGem, A Mastodon proxy for Gemini
|
|
Copyright (C) 2021 Romain de Laage
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU Affero General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU Affero General Public License for more details.
|
|
|
|
You should have received a copy of the GNU Affero General Public License
|
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
*/
|
|
package main
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"net/url"
|
|
"log"
|
|
"net"
|
|
"bufio"
|
|
"strconv"
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
func listen(address, certFile, keyFile string) net.Listener {
|
|
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
|
|
if err != nil {
|
|
log.Fatalln("loadkeys: %s", err)
|
|
}
|
|
config := &tls.Config{
|
|
ClientAuth: tls.RequestClientCert,
|
|
Certificates: []tls.Certificate{cert},
|
|
MinVersion: tls.VersionTLS12,
|
|
InsecureSkipVerify: true,
|
|
}
|
|
|
|
listener, err := tls.Listen("tcp", address, config)
|
|
if err != nil {
|
|
log.Fatalln("failed to listen on 0.0.0.0:1965: %s", err)
|
|
}
|
|
|
|
return listener
|
|
}
|
|
|
|
func serve(listener net.Listener, baseURL, title, home_message string, rateLimit int) {
|
|
for {
|
|
conn, err := listener.Accept()
|
|
if err != nil {
|
|
log.Println(err)
|
|
}
|
|
|
|
go handleConn(conn.(*tls.Conn), baseURL, title, home_message, rateLimit)
|
|
}
|
|
}
|
|
|
|
func getRawURL(conn *tls.Conn) (string, error) {
|
|
scanner := bufio.NewScanner(conn)
|
|
if ok := scanner.Scan(); !ok {
|
|
return "", scanner.Err()
|
|
}
|
|
|
|
rawURL := scanner.Text()
|
|
if strings.Contains(rawURL, "://") {
|
|
return rawURL, nil
|
|
}
|
|
|
|
return fmt.Sprintf("gemini://%s", rawURL), nil
|
|
}
|
|
|
|
func getPath(conn *tls.Conn) (string, string, error) {
|
|
rawURL, err := getRawURL(conn)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
|
|
parsedURL, err := url.Parse(rawURL)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
|
|
return parsedURL.Path, parsedURL.RawQuery, nil
|
|
}
|
|
|
|
func handleConn(conn *tls.Conn, baseURL, title, home_message string, rateLimit int) {
|
|
defer conn.Close()
|
|
|
|
if !rateIsOk(rateMap, strings.Split(conn.RemoteAddr().String(), ":")[0], rateLimit) {
|
|
log.Printf("Too many requests for %s\n", conn.RemoteAddr().String())
|
|
_, err := fmt.Fprintf(conn, "44 60\r\n")
|
|
if err != nil {
|
|
log.Println("send error: %s", err)
|
|
return
|
|
}
|
|
return
|
|
}
|
|
|
|
path, query, err := getPath(conn)
|
|
if err != nil {
|
|
log.Println("get url: %s", err)
|
|
_, err = fmt.Fprintf(conn, "59 Can't parse request\r\n")
|
|
if err != nil {
|
|
log.Println("send error: %s", err)
|
|
return
|
|
}
|
|
return
|
|
}
|
|
|
|
// home
|
|
if path == "" || path == "/" {
|
|
log.Println("Received request for home page")
|
|
|
|
_, err = fmt.Fprintf(conn, "20 text/gemini\r\n# %s\n\n%s\n\n=> /tag Search for a tag\n=> /about About MastoGem", title, home_message)
|
|
if err != nil {
|
|
log.Println("send error: %s", err)
|
|
return
|
|
}
|
|
return
|
|
}
|
|
|
|
// profile
|
|
if strings.HasPrefix(path, "/profile/") {
|
|
if strings.HasSuffix(path, "/reblog") {
|
|
// skip prefix and suffix
|
|
path = path[9:len(path)-len("/reblog")]
|
|
_, err = strconv.ParseUint(path, 10, 64)
|
|
if err != nil {
|
|
log.Println("invalid request: %s", err)
|
|
_, err = fmt.Fprintf(conn, "59 Can't parse request\r\n")
|
|
if err != nil {
|
|
log.Println("send error: %s", err)
|
|
return
|
|
}
|
|
return
|
|
}
|
|
|
|
log.Println("Received request for account with reblog " + path)
|
|
printProfileWithReblog(conn, baseURL, path)
|
|
} else {
|
|
// skip prefix
|
|
path = path[9:]
|
|
_, err = strconv.ParseUint(path, 10, 64)
|
|
if err != nil {
|
|
log.Println("invalid request: %s", err)
|
|
_, err = fmt.Fprintf(conn, "59 Can't parse request\r\n")
|
|
if err != nil {
|
|
log.Println("send error: %s", err)
|
|
return
|
|
}
|
|
return
|
|
}
|
|
|
|
log.Println("Received request for account " + path)
|
|
printProfile(conn, baseURL, path)
|
|
}
|
|
} /* thread */ else if strings.HasPrefix(path, "/thread/") {
|
|
// skip prefix
|
|
path = path[8:]
|
|
|
|
_, err = strconv.ParseUint(path, 10, 64)
|
|
if err != nil {
|
|
log.Println("invalid request: %s", err)
|
|
_, err = fmt.Fprintf(conn, "59 Can't parse request\r\n")
|
|
if err != nil {
|
|
log.Println("send error: %s", err)
|
|
return
|
|
}
|
|
return
|
|
}
|
|
|
|
log.Println("Received request for thread " + path)
|
|
|
|
printThread(conn, baseURL, path)
|
|
} /* about */ else if strings.HasPrefix(path, "/about") {
|
|
const page = `# About MastoGem
|
|
|
|
This capsule is running MastoGem, a free (as in free speech, not as in free beer) and open source software. It is released under AGPLv3 License (a copy a the license is available below). This software was written by Romain de Laage in 2021 during his free time. You can get a copy of the sources of this software here :
|
|
|
|
=> https://git.rdelaage.ovh/rdelaage/mastoGem Gitea repository (web)
|
|
|
|
Feel free to contribute, send feedback or share ideas.
|
|
|
|
# Mastodon instance
|
|
|
|
This capsule use %s Mastodon instance.
|
|
|
|
# AGPLv3 License
|
|
|
|
=> http://www.gnu.org/licenses/agpl-3.0.txt AGPLv3 License text`
|
|
|
|
log.Println("Received request for about page")
|
|
|
|
_, err = fmt.Fprintf(conn, "20 text/gemini\r\n" + page, baseURL)
|
|
if err != nil {
|
|
log.Println("send: %s", err)
|
|
return
|
|
}
|
|
} /* tag */ else if strings.HasPrefix(path, "/tag") {
|
|
if query == "" {
|
|
_, err = fmt.Fprintf(conn, "10 Enter a tag name\r\n")
|
|
if err != nil {
|
|
log.Println("send: %s", err)
|
|
return
|
|
}
|
|
return
|
|
}
|
|
|
|
log.Println("Received request for tag " + query)
|
|
|
|
printTag(conn, baseURL, query)
|
|
} /* toot */ else if strings.HasPrefix(path, "/toot/") {
|
|
path = path[6:]
|
|
|
|
_, err = strconv.ParseUint(path, 10, 64)
|
|
if err != nil {
|
|
log.Println("invalid request: %s", err)
|
|
_, err = fmt.Fprintf(conn, "59 Can't parse request\r\n")
|
|
if err != nil {
|
|
log.Println("send error: %s", err)
|
|
return
|
|
}
|
|
return
|
|
}
|
|
|
|
log.Println("Received request for toot " + path)
|
|
|
|
printToot(conn, baseURL, path)
|
|
} else {
|
|
_, err = fmt.Fprintf(conn, "59 Invalid request\r\n")
|
|
if err != nil {
|
|
log.Println("send: %s", err)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func printProfile(conn *tls.Conn, baseURL, profileID string) {
|
|
account, err := getAccount(baseURL, profileID)
|
|
if err != nil {
|
|
_, err = fmt.Fprintf(conn, "40 Remote mastodon instance failed\r\n")
|
|
if err != nil {
|
|
log.Println("handleConn: %s", err)
|
|
return
|
|
}
|
|
return
|
|
}
|
|
|
|
blogs, err := getBlog(baseURL, profileID)
|
|
if err != nil {
|
|
_, err = fmt.Fprintf(conn, "40 Remote mastodon instance failed\r\n")
|
|
if err != nil {
|
|
log.Println("handleConn: %s", err)
|
|
return
|
|
}
|
|
return
|
|
}
|
|
|
|
_, err = fmt.Fprintf(conn, "20 text/gemini\r\n# Toots for %s account\n", account.Name)
|
|
if err != nil {
|
|
log.Println("handleConn: %s", err)
|
|
return
|
|
}
|
|
|
|
for _, blog := range blogs {
|
|
_, err = fmt.Fprintf(conn, "\n%s\n=> /thread/%s View the thread\n", formatBlog(blog), blog.Id)
|
|
if err != nil {
|
|
log.Println("read blogs: %s", err)
|
|
return
|
|
}
|
|
}
|
|
|
|
_, err = fmt.Fprintf(conn, "\n=> /profile/%s/reblog This profile with reblog\n=> %s Go to %s account", account.Id, account.Url, account.Name)
|
|
if err != nil {
|
|
log.Println("add link: %s", err)
|
|
return
|
|
}
|
|
}
|
|
|
|
func printProfileWithReblog(conn *tls.Conn, baseURL, profileID string) {
|
|
account, err := getAccount(baseURL, profileID)
|
|
if err != nil {
|
|
_, err = fmt.Fprintf(conn, "40 Remote mastodon instance failed\r\n")
|
|
if err != nil {
|
|
log.Println("handleConn: %s", err)
|
|
return
|
|
}
|
|
return
|
|
}
|
|
|
|
blogs, err := getBlogAndReblog(baseURL, profileID)
|
|
if err != nil {
|
|
_, err = fmt.Fprintf(conn, "40 Remote mastodon instance failed\r\n")
|
|
if err != nil {
|
|
log.Println("handleConn: %s", err)
|
|
return
|
|
}
|
|
return
|
|
}
|
|
|
|
_, err = fmt.Fprintf(conn, "20 text/gemini\r\n# Toots for %s account\n", account.Name)
|
|
if err != nil {
|
|
log.Println("handleConn: %s", err)
|
|
return
|
|
}
|
|
|
|
for _, blog := range blogs {
|
|
_, err = fmt.Fprintf(conn, "\n%s\n=> /thread/%s View the thread\n", formatBlog(blog), blog.Id)
|
|
if err != nil {
|
|
log.Println("read blogs: %s", err)
|
|
return
|
|
}
|
|
}
|
|
|
|
_, err = fmt.Fprintf(conn, "\n=> /profile/%s This profile without reblog\n=> %s Go to %s account", account.Id, account.Url, account.Name)
|
|
if err != nil {
|
|
log.Println("add link: %s", err)
|
|
return
|
|
}
|
|
}
|
|
|
|
func printToot(conn *tls.Conn, baseURL, tootID string) {
|
|
toot, err := getToot(baseURL, tootID)
|
|
if err != nil {
|
|
_, err = fmt.Fprintf(conn, "40 Remote mastodon instance failed\r\n")
|
|
if err != nil {
|
|
log.Println("handleConn: %s", err)
|
|
return
|
|
}
|
|
return
|
|
}
|
|
|
|
// Print header
|
|
_, err = fmt.Fprintf(conn, "20 text/gemini\r\n")
|
|
if err != nil {
|
|
log.Println("handleConn: %s", err)
|
|
return
|
|
}
|
|
|
|
// Print toot
|
|
if toot.Reblog == nil {
|
|
_, err = fmt.Fprintf(conn, "# Toot\n\n%s\n=> /thread/%s View the thread\n=> /profile/%s More toots from %s\n", formatBlog(toot), toot.Id, toot.Author.Id, toot.Author.Name)
|
|
if err != nil {
|
|
log.Println("handleConn: %s", err)
|
|
return
|
|
}
|
|
} else {
|
|
_, err = fmt.Fprintf(conn, "# Toot\n\n%s\n=> /toot/%s Original toot\n=> /thread/%s View the thread\n=> /profile/%s More toots from %s\n=> /profile/%s More toots from %s\n", formatBlog(toot), toot.Reblog.Id, toot.Id, toot.Author.Id, toot.Author.Name, toot.Reblog.Author.Id, toot.Reblog.Author.Name)
|
|
if err != nil {
|
|
log.Println("handleConn: %s", err)
|
|
return
|
|
}
|
|
}
|
|
|
|
// print mentions
|
|
_, err = fmt.Fprintf(conn, "\n# Mentions\n")
|
|
if err != nil {
|
|
log.Println("handleConn: %s", err)
|
|
return
|
|
}
|
|
|
|
for _, mention := range toot.Mentions {
|
|
_, err = fmt.Fprintf(conn, "\n=> /profile/%s View %s profile", mention.Id, mention.Name)
|
|
if err != nil {
|
|
log.Println("handleConn: %s", err)
|
|
return
|
|
}
|
|
}
|
|
// print tags
|
|
_, err = fmt.Fprintf(conn, "\n\n# Tags\n")
|
|
if err != nil {
|
|
log.Println("handleConn: %s", err)
|
|
return
|
|
}
|
|
|
|
for _, tag := range toot.Tags {
|
|
_, err = fmt.Fprintf(conn, "\n=> /tag?%s View %s tag", url.QueryEscape(tag.Name), tag.Name)
|
|
if err != nil {
|
|
log.Println("handleConn: %s", err)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func printThread(conn *tls.Conn, baseURL, tootID string) {
|
|
originalToot, err := getToot(baseURL, tootID)
|
|
if err != nil {
|
|
_, err = fmt.Fprintf(conn, "40 Remote mastodon instance failed\r\n")
|
|
if err != nil {
|
|
log.Println("handleConn: %s", err)
|
|
return
|
|
}
|
|
return
|
|
}
|
|
|
|
thread, err := getThread(baseURL, tootID)
|
|
if err != nil {
|
|
_, err = fmt.Fprintf(conn, "40 Remote mastodon instance failed\r\n")
|
|
if err != nil {
|
|
log.Println("handleConn: %s", err)
|
|
return
|
|
}
|
|
return
|
|
}
|
|
|
|
// Print header
|
|
_, err = fmt.Fprintf(conn, "20 text/gemini\r\n# Ancestors\n")
|
|
if err != nil {
|
|
log.Println("handleConn: %s", err)
|
|
return
|
|
}
|
|
|
|
// Print each anscestor
|
|
for _, toot := range thread.Ancestors {
|
|
_, err = fmt.Fprintf(conn, "\n%s\n=> /profile/%s More toots from %s\n", formatBlog(toot), toot.Author.Id, toot.Author.Name)
|
|
if err != nil {
|
|
log.Println("handleConn: %s", err)
|
|
return
|
|
}
|
|
}
|
|
|
|
// Print original toot
|
|
_, err = fmt.Fprintf(conn, "\n# Toot\n\n%s\n=> /profile/%s More toots from %s\n", formatBlog(originalToot), originalToot.Author.Id, originalToot.Author.Name)
|
|
if err != nil {
|
|
log.Println("handleConn: %s", err)
|
|
return
|
|
}
|
|
|
|
// Print each descendant
|
|
_, err = fmt.Fprintf(conn, "\n# Descendants\n")
|
|
if err != nil {
|
|
log.Println("handleConn: %s", err)
|
|
return
|
|
}
|
|
for _, toot := range thread.Descendants {
|
|
_, err = fmt.Fprintf(conn, "\n%s\n=> /profile/%s More toots from %s\n", formatBlog(toot), toot.Author.Id, toot.Author.Name)
|
|
if err != nil {
|
|
log.Println("handleConn: %s", err)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func printTag(conn *tls.Conn, baseURL, tag string) {
|
|
toots, err := getTag(baseURL, tag)
|
|
if err != nil {
|
|
_, err = fmt.Fprintf(conn, "40 Remote mastodon instance failed\r\n")
|
|
if err != nil {
|
|
log.Println("handleConn: %s", err)
|
|
return
|
|
}
|
|
return
|
|
}
|
|
|
|
//decode tag (url encoded char like space)
|
|
tag, err = url.QueryUnescape(tag)
|
|
if err != nil {
|
|
_, err = fmt.Fprintf(conn, "59 Invalid url encoded tag\r\n")
|
|
if err != nil {
|
|
log.Println("handleConn: %s", err)
|
|
return
|
|
}
|
|
return
|
|
}
|
|
|
|
// Print header
|
|
_, err = fmt.Fprintf(conn, "20 text/gemini\r\n# Toots for %s\n", tag)
|
|
if err != nil {
|
|
log.Println("handleConn: %s", err)
|
|
return
|
|
}
|
|
|
|
// Print toots
|
|
for _, toot := range toots {
|
|
_, err = fmt.Fprintf(conn, "\n%s\n=> /profile/%s More toots from %s\n=> /thread/%s View the thread\n", formatBlog(toot), toot.Author.Id, toot.Author.Name, toot.Id)
|
|
if err != nil {
|
|
log.Println("handleConn: %s", err)
|
|
return
|
|
}
|
|
}
|
|
}
|