2021-03-03 10:02:35 +01:00
/ *
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/>.
* /
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"
2021-02-28 16:30:59 +01:00
"bufio"
"strconv"
2021-02-28 14:41:40 +01:00
"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
}
2021-04-18 10:43:12 +02:00
func serve ( listener net . Listener , baseURL , title , home_message string , rateLimit int ) {
2021-02-28 14:41:40 +01:00
for {
conn , err := listener . Accept ( )
if err != nil {
log . Println ( err )
}
2021-04-18 10:43:12 +02:00
go handleConn ( conn . ( * tls . Conn ) , baseURL , title , home_message , rateLimit )
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
}
2021-03-03 11:23:27 +01:00
func getPath ( conn * tls . Conn ) ( string , string , error ) {
2021-02-28 16:30:59 +01:00
rawURL , err := getRawURL ( conn )
if err != nil {
2021-03-03 11:23:27 +01:00
return "" , "" , err
2021-02-28 16:30:59 +01:00
}
parsedURL , err := url . Parse ( rawURL )
if err != nil {
2021-03-03 11:23:27 +01:00
return "" , "" , err
2021-02-28 16:30:59 +01:00
}
2021-03-03 11:23:27 +01:00
return parsedURL . Path , parsedURL . RawQuery , nil
2021-02-28 16:30:59 +01:00
}
2021-04-18 10:43:12 +02:00
func handleConn ( conn * tls . Conn , baseURL , title , home_message string , rateLimit int ) {
2021-02-28 14:41:40 +01:00
defer conn . Close ( )
2021-04-18 10:43:12 +02:00
if ! rateIsOk ( rateMap , strings . Split ( conn . RemoteAddr ( ) . String ( ) , ":" ) [ 0 ] , rateLimit ) {
2021-04-17 12:10:27 +02:00
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
}
2021-03-03 11:23:27 +01:00
path , query , err := getPath ( conn )
2021-02-28 16:30:59 +01:00
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-03-04 10:13:38 +01:00
log . Println ( "Received request for home page" )
2022-04-29 10:35:43 +02:00
_ , err = fmt . Fprintf ( conn , "20 text/gemini\r\n# %s\n\n%s\n\n=> /timeline View public timeline\n=> /tag Search for a tag\n=> /about About MastoGem" , title , home_message )
2021-02-28 18:41:48 +01:00
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-05-02 19:30:47 +02:00
if strings . HasSuffix ( path , "/reblog" ) {
// skip prefix and suffix
path = path [ 9 : len ( path ) - len ( "/reblog" ) ]
_ , err = strconv . ParseUint ( path , 10 , 64 )
2021-03-01 18:20:11 +01:00
if err != nil {
2021-05-02 19:30:47 +02: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-03-01 18:20:11 +01:00
return
}
2021-02-28 16:30:59 +01:00
2021-05-02 19:30:47 +02:00
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
}
2021-02-28 17:55:27 +01:00
2021-05-02 19:30:47 +02:00
log . Println ( "Received request for account " + path )
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
2021-03-02 14:16:58 +01:00
path = path [ 8 : ]
2021-03-01 18:20:11 +01:00
_ , 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 )
2021-03-03 10:41:25 +01:00
} /* 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 .
2021-03-03 12:03:41 +01:00
# Mastodon instance
This capsule use % s Mastodon instance .
2021-03-03 10:41:25 +01:00
# AGPLv3 License
= > http : //www.gnu.org/licenses/agpl-3.0.txt AGPLv3 License text`
2021-03-04 10:13:38 +01:00
log . Println ( "Received request for about page" )
2021-03-03 12:03:41 +01:00
_ , err = fmt . Fprintf ( conn , "20 text/gemini\r\n" + page , baseURL )
2021-03-03 10:41:25 +01:00
if err != nil {
log . Println ( "send: %s" , err )
return
}
2021-03-03 11:23:27 +01:00
} /* 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
}
2021-03-04 10:13:38 +01:00
log . Println ( "Received request for tag " + query )
2021-03-03 11:23:27 +01:00
printTag ( conn , baseURL , query )
2021-03-04 10:13:38 +01:00
} /* 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 )
2022-04-29 10:35:43 +02:00
} /* timeline */ else if strings . HasPrefix ( path , "/timeline" ) {
log . Println ( "Received request for timeline" )
printTimeline ( conn , baseURL )
2022-04-30 14:32:28 +02:00
} /* media */ else if strings . HasPrefix ( path , "/media" ) {
if query == "" {
_ , err = fmt . Fprintf ( conn , "59 Invalid request\r\n" )
if err != nil {
log . Println ( "send: %s" , err )
return
}
return
}
log . Println ( "Received request for media " + query )
proxyMedia ( conn , baseURL , query )
2021-03-01 18:20:11 +01:00
} else {
_ , err = fmt . Fprintf ( conn , "59 Invalid request\r\n" )
if err != nil {
log . Println ( "send: %s" , err )
return
}
}
}
2022-04-30 14:32:28 +02:00
func proxyMedia ( conn * tls . Conn , baseURL , query string ) {
mediaURL , err := url . QueryUnescape ( query )
if err != nil {
_ , err = fmt . Fprintf ( conn , "59 Invalid url encoded media\r\n" )
if err != nil {
log . Println ( "handleConn: %s" , err )
return
}
return
}
if ! strings . HasPrefix ( mediaURL , baseURL ) {
_ , err = fmt . Fprintf ( conn , "59 Invalid media url\r\n" )
if err != nil {
log . Println ( "handleConn: %s" , err )
return
}
return
}
mime , media , err := getMedia ( mediaURL )
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 %s\r\n" , mime )
if err != nil {
log . Println ( "handleConn: %s" , err )
return
}
_ , err = conn . Write ( media )
if err != nil {
log . Println ( "handleConn: %s" , err )
return
}
}
2021-03-01 18:20:11 +01:00
func printProfile ( conn * tls . Conn , baseURL , profileID string ) {
account , err := getAccount ( baseURL , profileID )
2021-03-05 10:46:31 +01:00
if err != nil {
_ , err = fmt . Fprintf ( conn , "40 Remote mastodon instance failed\r\n" )
if err != nil {
log . Println ( "handleConn: %s" , err )
return
}
return
}
2021-02-28 14:41:40 +01:00
2021-03-05 10:46:31 +01:00
blogs , err := getBlog ( baseURL , profileID )
if err != nil {
2021-02-28 17:51:38 +01:00
_ , err = fmt . Fprintf ( conn , "40 Remote mastodon instance failed\r\n" )
if err != nil {
log . Println ( "handleConn: %s" , err )
return
}
return
}
2021-03-03 16:30:26 +01:00
_ , err = fmt . Fprintf ( conn , "20 text/gemini\r\n# Toots for %s account\n" , account . Name )
2021-02-28 14:41:40 +01:00
if err != nil {
log . Println ( "handleConn: %s" , err )
return
}
for _ , blog := range blogs {
2021-03-03 16:30:26 +01:00
_ , err = fmt . Fprintf ( conn , "\n%s\n=> /thread/%s View the thread\n" , formatBlog ( blog ) , blog . Id )
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
}
2021-05-02 19:30:47 +02:00
_ , 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 )
2021-03-01 18:20:11 +01:00
if err != nil {
log . Println ( "add link: %s" , err )
return
}
}
2022-04-29 10:35:43 +02:00
func printTimeline ( conn * tls . Conn , baseURL string ) {
toots , err := getTimeline ( baseURL )
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 toots
for _ , toot := range toots {
_ , err = fmt . Fprintf ( conn , "\n%s\n=> /thread/%s View the thread\n" , formatBlog ( toot ) , toot . Id )
if err != nil {
log . Println ( "read timeline: %s" , err )
return
}
}
}
2021-03-04 10:13:38 +01:00
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
2021-05-02 19:30:47 +02:00
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
}
2021-03-04 10:13:38 +01:00
}
// 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
2021-03-05 10:48:30 +01:00
_ , err = fmt . Fprintf ( conn , "\n\n# Tags\n" )
2021-03-04 10:13:38 +01:00
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
}
}
}
2021-03-01 18:20:11 +01:00
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-03 16:30:26 +01:00
_ , err = fmt . Fprintf ( conn , "\n%s\n=> /profile/%s More toots from %s\n" , formatBlog ( toot ) , toot . Author . Id , toot . Author . Name )
2021-03-01 18:20:11 +01:00
if err != nil {
log . Println ( "handleConn: %s" , err )
return
}
}
// Print original toot
2021-03-03 16:30:26 +01:00
_ , 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 )
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-03 16:30:26 +01:00
_ , err = fmt . Fprintf ( conn , "\n%s\n=> /profile/%s More toots from %s\n" , formatBlog ( toot ) , toot . Author . Id , toot . Author . Name )
2021-03-01 18:20:11 +01:00
if err != nil {
log . Println ( "handleConn: %s" , err )
return
}
}
}
2021-03-03 11:23:27 +01:00
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
}
2021-03-04 09:27:50 +01:00
//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
}
2021-03-03 11:23:27 +01:00
// Print header
2021-03-03 16:30:26 +01:00
_ , err = fmt . Fprintf ( conn , "20 text/gemini\r\n# Toots for %s\n" , tag )
2021-03-03 11:23:27 +01:00
if err != nil {
log . Println ( "handleConn: %s" , err )
return
}
// Print toots
for _ , toot := range toots {
2021-03-04 10:13:38 +01:00
_ , 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 )
2021-03-03 11:23:27 +01:00
if err != nil {
log . Println ( "handleConn: %s" , err )
return
}
}
}