mastoGem/server.go

294 lines
6.4 KiB
Go
Raw Normal View History

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 {
Id string `json:"id"`
Content string `json:"content"`
Date string `json:"created_at"`
}
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
}
type Account struct {
Id string `json:"id"`
Name string `json:"display_name"`
Url string `json:"url"`
}
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
}
path = path[1:]
2021-02-28 18:41:48 +01:00
if 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
}
2021-02-28 16:30:59 +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
}
2021-02-28 17:55:27 +01:00
log.Println("Received request for account " + path)
account, err := getAccount(baseURL, path)
2021-02-28 16:30:59 +01:00
blogs := getBlog(baseURL, path)
2021-02-28 14:41:40 +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 {
date := "\n```\n* Posted at " + blog.Date + " *\n```\n"
2021-02-28 14:41:40 +01:00
text := blog.Content + "\n"
text = strings.ReplaceAll(text, "<p>", "")
text = strings.ReplaceAll(text, "</p>", "\n\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)
_, err = fmt.Fprintf(conn, date + text)
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
}
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
}
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
}