/* 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 . */ 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) { for { conn, err := listener.Accept() if err != nil { log.Println(err) } go handleConn(conn.(*tls.Conn), baseURL, title, home_message) } } 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, error) { rawURL, err := getRawURL(conn) if err != nil { return "", err } parsedURL, err := url.Parse(rawURL) if err != nil { return "", err } return parsedURL.Path, nil } func handleConn(conn *tls.Conn, baseURL, title, home_message string) { defer conn.Close() path, 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 == "/" { _, err = fmt.Fprintf(conn, "20 text/gemini\r\n# " + title + "\n\n" + home_message) if err != nil { log.Println("send error: %s", err) return } return } // profile if strings.HasPrefix(path, "/profile/") { // 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) } 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) blogs := getBlog(baseURL, profileID) if err != nil || blogs == 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 " + account.Name + " account\n") if err != nil { log.Println("handleConn: %s", err) return } for _, blog := range blogs { date := "\n```\n* Posted at " + blog.Date + " *\n```\n" text := removeHTMLTags(blog.Content) + "\n" _, err = fmt.Fprintf(conn, date + text + "=> /thread/" + blog.Id + " View the thread\n") if err != nil { log.Println("read blogs: %s", err) return } } _, err = fmt.Fprintf(conn, "=> " + account.Url + " Go to " + account.Name + " account") if err != nil { log.Println("add link: %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```\n* Posted on " + toot.Date + " by " + toot.Author.Name + " *\n```\n" + removeHTMLTags(toot.Content) + "\n=> /profile/" + toot.Author.Id + " More toots from " + toot.Author.Name + "\n") if err != nil { log.Println("handleConn: %s", err) return } } // Print original toot _, err = fmt.Fprintf(conn, "\n# Toot\n\n```\n* Posted on " + originalToot.Date + " by "+ originalToot.Author.Name +" *\n```\n" + removeHTMLTags(originalToot.Content) + "\n=> /profile/" + originalToot.Author.Id + " More toots from " + originalToot.Author.Name + "\n") 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```\n* Posted on " + toot.Date + " by " + toot.Author.Name + " *\n```\n" + removeHTMLTags(toot.Content) + "\n=> /profile/" + toot.Author.Id + " More toots from " + toot.Author.Name + "\n") if err != nil { log.Println("handleConn: %s", err) return } } }