2021-02-28 14:41:40 +01:00
package main
import (
"crypto/tls"
2021-02-28 16:30:59 +01:00
"net/url"
2021-02-28 14:41:40 +01:00
"log"
"net"
"net/http"
2021-02-28 16:30:59 +01:00
"bufio"
2021-02-28 15:19:15 +01:00
"os"
2021-02-28 16:30:59 +01:00
"strconv"
2021-02-28 14:41:40 +01:00
"fmt"
"encoding/json"
"io/ioutil"
"strings"
"html"
"regexp"
)
type Blog struct {
2021-03-01 18:32:43 +01:00
Id string ` json:"id" `
Content string ` json:"content" `
Date string ` json:"created_at" `
Author Account ` json:"account" `
2021-02-28 14:41:40 +01:00
}
2021-02-28 15:19:15 +01:00
type Config struct {
2021-02-28 18:41:48 +01:00
Listen string ` json:"listen" `
CertPath string ` json:"cert_path" `
KeyPath string ` json:"key_path" `
BaseURL string ` json:"base_url" `
Title string ` json:"title" `
HomeMessage string ` json:"home_message" `
2021-02-28 15:19:15 +01:00
}
2021-02-28 17:51:38 +01:00
type Account struct {
Id string ` json:"id" `
Name string ` json:"display_name" `
Url string ` json:"url" `
}
2021-03-01 18:20:11 +01:00
type Thread struct {
Ancestors [ ] Blog ` json:"ancestors" `
Descendants [ ] Blog ` json:"descendants" `
}
2021-02-28 14:41:40 +01:00
func main ( ) {
2021-02-28 15:19:15 +01:00
config := getConfig ( )
listener := listen ( config . Listen , config . CertPath , config . KeyPath )
log . Println ( "Server successfully started" )
log . Println ( "Server is listening at " + config . Listen )
2021-02-28 18:41:48 +01:00
serve ( listener , config . BaseURL , config . Title , config . HomeMessage )
2021-02-28 15:19:15 +01:00
}
func getConfig ( ) Config {
configPath := os . Getenv ( "MASTOGEM_CONFIG_PATH" )
if configPath == "" {
log . Println ( "MASTOGEM_CONFIG_PATH was not set, using default settings" )
config := Config {
Listen : "127.0.0.1:1965" ,
CertPath : "cert.pem" ,
KeyPath : "key.rsa" ,
BaseURL : "https://mamot.fr" ,
2021-02-28 18:41:48 +01:00
Title : "MastoGem" ,
HomeMessage : "Welcome on MastoGem, this is a Mastodon proxy for Gemini. You can view the last 20 toots of a Mastodon account by providing its ID." ,
2021-02-28 15:19:15 +01:00
}
return config
}
configFile , err := ioutil . ReadFile ( configPath )
2021-02-28 14:41:40 +01:00
if err != nil {
2021-02-28 15:19:15 +01:00
log . Fatalln ( "config file: %s" , err )
2021-02-28 14:41:40 +01:00
}
2021-02-28 15:19:15 +01:00
var config Config
json . Unmarshal ( configFile , & config )
2021-02-28 14:41:40 +01:00
2021-02-28 15:19:15 +01:00
return config
2021-02-28 14:41:40 +01:00
}
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
}
2021-02-28 18:41:48 +01:00
func serve ( listener net . Listener , baseURL , title , home_message string ) {
2021-02-28 14:41:40 +01:00
for {
conn , err := listener . Accept ( )
if err != nil {
log . Println ( err )
}
2021-02-28 18:41:48 +01:00
go handleConn ( conn . ( * tls . Conn ) , baseURL , title , home_message )
2021-02-28 14:41:40 +01:00
}
}
2021-02-28 16:30:59 +01:00
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
}
2021-02-28 18:41:48 +01:00
func handleConn ( conn * tls . Conn , baseURL , title , home_message string ) {
2021-02-28 14:41:40 +01:00
defer conn . Close ( )
2021-02-28 16:30:59 +01:00
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
}
2021-03-01 18:20:11 +01:00
// home
2021-03-02 14:10:58 +01:00
if path == "" || path == "/" {
2021-02-28 18:41:48 +01:00
_ , 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
}
2021-03-01 18:20:11 +01:00
// profile
2021-03-02 14:10:58 +01:00
if strings . HasPrefix ( path , "/profile/" ) {
2021-03-01 18:20:11 +01:00
// skip prefix
path = path [ 8 : ]
_ , err = strconv . ParseUint ( path , 10 , 64 )
2021-02-28 16:30:59 +01:00
if err != nil {
2021-03-01 18:20:11 +01:00
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
}
2021-02-28 16:30:59 +01:00
return
}
2021-03-01 18:20:11 +01:00
log . Println ( "Received request for account " + path )
2021-02-28 17:55:27 +01:00
2021-03-01 18:20:11 +01:00
printProfile ( conn , baseURL , path )
2021-03-02 14:10:58 +01:00
} /* thread */ else if strings . HasPrefix ( path , "/thread/" ) {
2021-03-01 18:20:11 +01:00
// skip prefix
path = path [ 7 : ]
_ , 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 )
2021-02-28 14:41:40 +01:00
2021-02-28 17:51:38 +01:00
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" )
2021-02-28 14:41:40 +01:00
if err != nil {
log . Println ( "handleConn: %s" , err )
return
}
for _ , blog := range blogs {
2021-02-28 17:51:38 +01:00
date := "\n```\n* Posted at " + blog . Date + " *\n```\n"
2021-02-28 14:41:40 +01:00
2021-03-01 18:20:11 +01:00
text := removeHTMLTags ( blog . Content ) + "\n"
2021-03-01 19:00:22 +01:00
_ , err = fmt . Fprintf ( conn , date + text + "=> /thread/" + blog . Id + " View the thread\n" )
2021-02-28 14:41:40 +01:00
if err != nil {
2021-03-01 18:20:11 +01:00
log . Println ( "read blogs: %s" , err )
2021-02-28 14:41:40 +01:00
return
}
2021-03-01 18:20:11 +01:00
}
_ , 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" )
2021-02-28 14:41:40 +01:00
if err != nil {
2021-03-01 18:20:11 +01:00
log . Println ( "handleConn: %s" , err )
2021-02-28 14:41:40 +01:00
return
}
2021-03-01 18:20:11 +01:00
return
}
2021-02-28 14:41:40 +01:00
2021-03-01 18:20:11 +01:00
thread , err := getThread ( baseURL , tootID )
if err != nil {
_ , err = fmt . Fprintf ( conn , "40 Remote mastodon instance failed\r\n" )
2021-02-28 14:41:40 +01:00
if err != nil {
2021-03-01 18:20:11 +01:00
log . Println ( "handleConn: %s" , err )
2021-02-28 14:41:40 +01:00
return
}
2021-03-01 18:20:11 +01:00
return
2021-02-28 14:41:40 +01:00
}
2021-02-28 17:51:38 +01:00
2021-03-01 18:20:11 +01:00
// Print header
_ , err = fmt . Fprintf ( conn , "20 text/gemini\r\n# Ancestors\n" )
2021-02-28 17:51:38 +01:00
if err != nil {
2021-03-01 18:20:11 +01:00
log . Println ( "handleConn: %s" , err )
return
}
// Print each anscestor
for _ , toot := range thread . Ancestors {
2021-03-01 19:00:22 +01:00
_ , 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" )
2021-03-01 18:20:11 +01:00
if err != nil {
log . Println ( "handleConn: %s" , err )
return
}
}
// Print original toot
2021-03-01 19:00:22 +01:00
_ , 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" )
2021-03-01 18:20:11 +01:00
if err != nil {
log . Println ( "handleConn: %s" , err )
2021-02-28 17:51:38 +01:00
return
}
2021-03-01 18:20:11 +01:00
// Print each descendant
_ , err = fmt . Fprintf ( conn , "\n# Descendants\n" )
if err != nil {
log . Println ( "handleConn: %s" , err )
return
}
for _ , toot := range thread . Descendants {
2021-03-01 19:00:22 +01:00
_ , 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" )
2021-03-01 18:20:11 +01:00
if err != nil {
log . Println ( "handleConn: %s" , err )
return
}
}
}
func removeHTMLTags ( content string ) string {
text := strings . ReplaceAll ( content , "<p>" , "" )
text = strings . ReplaceAll ( text , "</p>" , "\n\n" )
text = strings . ReplaceAll ( text , "<br />" , "\n" )
text = strings . ReplaceAll ( text , "<br>" , "\n" )
text = strings . ReplaceAll ( text , "</a>" , "" )
text = strings . ReplaceAll ( text , "</span>" , "" )
regexString := "<a( [^>]*)?>"
regex , err := regexp . Compile ( regexString )
if err != nil {
log . Println ( "regex: %s" , err )
return ""
}
text = regex . ReplaceAllLiteralString ( text , "" )
regexString = "<span( [^>]*)?>"
regex , err = regexp . Compile ( regexString )
if err != nil {
log . Println ( "regex: %s" , err )
return ""
}
text = regex . ReplaceAllLiteralString ( text , "" )
text = html . UnescapeString ( text )
return text
2021-02-28 14:41:40 +01:00
}
func getBlog ( baseURL , account string ) [ ] Blog {
if baseURL == "" || account == "" {
log . Println ( "baseURL or account is empty" )
return nil
}
2021-02-28 15:44:07 +01:00
resp , err := http . Get ( baseURL + "/api/v1/accounts/" + account + "/statuses?exclude_reblogs=true&exlude_replies=true" )
2021-02-28 14:41:40 +01:00
if err != nil {
log . Println ( "Mastodon API request: %s" , err )
return nil
}
2021-02-28 15:29:11 +01:00
defer resp . Body . Close ( )
2021-02-28 14:41:40 +01:00
if resp . StatusCode != 200 {
log . Println ( "Mastodon API response: %s" , resp . Status )
return nil
}
body , err := ioutil . ReadAll ( resp . Body )
if err != nil {
log . Println ( "Mastodon response body: %s" , err )
}
var blogs [ ] Blog
json . Unmarshal ( body , & blogs )
return blogs
}
2021-02-28 17:51:38 +01:00
func getAccount ( baseURL , accountId string ) ( Account , error ) {
if baseURL == "" || accountId == "" {
log . Println ( "baseURL or account is empty" )
return Account { } , fmt . Errorf ( "baseURL or account is empty" )
}
resp , err := http . Get ( baseURL + "/api/v1/accounts/" + accountId )
if err != nil {
log . Println ( "Mastodon API request: %s" , err )
return Account { } , fmt . Errorf ( "API request failed" )
}
defer resp . Body . Close ( )
if resp . StatusCode != 200 {
log . Println ( "Mastodon API response: %s" , resp . Status )
return Account { } , fmt . Errorf ( "API response is not 200" )
}
body , err := ioutil . ReadAll ( resp . Body )
if err != nil {
log . Println ( "Mastodon response body: %s" , err )
return Account { } , fmt . Errorf ( "Failed to read response" )
}
var account Account
json . Unmarshal ( body , & account )
return account , nil
}
2021-03-01 18:20:11 +01:00
func getToot ( baseURL , tootId string ) ( Blog , error ) {
if baseURL == "" || tootId == "" {
log . Println ( "baseURL or tootID is empty" )
return Blog { } , fmt . Errorf ( "baseURL or tootID is empty" )
}
resp , err := http . Get ( baseURL + "/api/v1/statuses/" + tootId )
if err != nil {
log . Println ( "Mastodon API request: %s" , err )
return Blog { } , fmt . Errorf ( "API request failed" )
}
defer resp . Body . Close ( )
if resp . StatusCode != 200 {
log . Println ( "Mastodon API response: %s" , resp . Status )
return Blog { } , fmt . Errorf ( "API response is not 200" )
}
body , err := ioutil . ReadAll ( resp . Body )
if err != nil {
log . Println ( "Mastodon response body: %s" , err )
return Blog { } , fmt . Errorf ( "Failed to read response" )
}
var toot Blog
json . Unmarshal ( body , & toot )
return toot , nil
}
func getThread ( baseURL , tootId string ) ( Thread , error ) {
if baseURL == "" || tootId == "" {
log . Println ( "baseURL or tootID is empty" )
return Thread { } , fmt . Errorf ( "baseURL or tootID is empty" )
}
resp , err := http . Get ( baseURL + "/api/v1/statuses/" + tootId + "/context" )
if err != nil {
log . Println ( "Mastodon API request: %s" , err )
return Thread { } , fmt . Errorf ( "API request failed" )
}
defer resp . Body . Close ( )
if resp . StatusCode != 200 {
log . Println ( "Mastodon API response: %s" , resp . Status )
return Thread { } , fmt . Errorf ( "API response is not 200" )
}
body , err := ioutil . ReadAll ( resp . Body )
if err != nil {
log . Println ( "Mastodon response body: %s" , err )
return Thread { } , fmt . Errorf ( "Failed to read response" )
}
var thread Thread
json . Unmarshal ( body , & thread )
return thread , nil
}