package main import ( "crypto/tls" "net/url" "log" "net" "net/http" "bufio" "os" "strconv" "fmt" "encoding/json" "io/ioutil" "strings" "html" "regexp" ) type Blog struct { Id string `json:"id"` Content string `json:"content"` Date string `json:"created_at"` } type Config struct { 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"` } type Account struct { Id string `json:"id"` Name string `json:"display_name"` Url string `json:"url"` } func main() { config := getConfig() listener := listen(config.Listen, config.CertPath, config.KeyPath) log.Println("Server successfully started") log.Println("Server is listening at " + config.Listen) serve(listener, config.BaseURL, config.Title, config.HomeMessage) } 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", 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.", } return config } configFile, err := ioutil.ReadFile(configPath) if err != nil { log.Fatalln("config file: %s", err) } var config Config json.Unmarshal(configFile, &config) return config } 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 } path = path[1:] 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 } _, 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) account, err := getAccount(baseURL, path) blogs := getBlog(baseURL, path) 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 := blog.Content + "\n" text = strings.ReplaceAll(text, "

", "") text = strings.ReplaceAll(text, "

", "\n\n") text = strings.ReplaceAll(text, "
", "\n") text = strings.ReplaceAll(text, "", "") text = strings.ReplaceAll(text, "", "") regexString := "]*)?>" regex, err := regexp.Compile(regexString) if err != nil { log.Println("regex: %s", err) return } text = regex.ReplaceAllLiteralString(text, "") regexString = "]*)?>" 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 } } func getBlog(baseURL, account string) []Blog { if baseURL == "" || account == "" { log.Println("baseURL or account is empty") return nil } resp, err := http.Get(baseURL + "/api/v1/accounts/" + account + "/statuses?exclude_reblogs=true&exlude_replies=true") if err != nil { log.Println("Mastodon API request: %s", err) return nil } defer resp.Body.Close() 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 }