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-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
}
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-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-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" )
2021-03-03 16:30:26 +01:00
_ , 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 )
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-03-01 18:20:11 +01:00
// skip prefix
2021-03-02 14:16:58 +01:00
path = path [ 9 : ]
2021-03-01 18:20:11 +01:00
_ , 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
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 )
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
}
}
}
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-03-03 16:30:26 +01:00
_ , err = fmt . Fprintf ( conn , "\n=> %s Go to %s account" , account . Url , account . Name )
2021-03-01 18:20:11 +01:00
if err != nil {
log . Println ( "add link: %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
_ , 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
}
// 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
}
}
}