Bombabillo is a non-web client for the terminal, supporting Gopher, Gemini and much more. https://bombadillo.colorfield.space
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
bombadillo/gopher/gopher.go

204 lines
4.9 KiB

/*
* Copyright (C) 2022 Brian Evans
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License.
*
* 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// Contains the building blocks of a gopher client: history, url, and view.
// History handles the browsing session and view represents individual
// text based resources, the url represents a parsed url.
package gopher
import (
"errors"
"fmt"
"io/ioutil"
"net"
"strings"
"time"
)
//------------------------------------------------\\
// + + + V A R I A B L E S + + + \\
//--------------------------------------------------\\
// types is a map of gophertypes to a string representing their
// type, to be used when displaying gophermaps
var types = map[string]string{
"0": "TXT",
"1": "MAP",
"3": "ERR",
"4": "BIN",
"5": "DOS",
"6": "UUE",
"7": "FTS",
"8": "TEL",
"9": "BIN",
"g": "GIF",
"G": "GEM",
"h": "HTM",
"I": "IMG",
"p": "PNG",
"s": "SND",
"S": "SSH",
"T": "TEL",
}
var Timeout time.Duration = time.Duration(15) * time.Second
//------------------------------------------------\\
// + + + F U N C T I O N S + + + \\
//--------------------------------------------------\\
// Retrieve makes a request to a Url and resturns
// the response as []byte/error. This function is
// available to use directly, but in most implementations
// using the "Visit" receiver of the History struct will
// be better.
func Retrieve(host, port, resource string) ([]byte, error) {
nullRes := make([]byte, 0)
if host == "" || port == "" {
return nullRes, errors.New("Incomplete request url")
}
addr := host + ":" + port
conn, err := net.DialTimeout("tcp", addr, Timeout)
if err != nil {
return nullRes, err
}
send := resource + "\n"
_, err = conn.Write([]byte(send))
if err != nil {
return nullRes, err
}
result, err := ioutil.ReadAll(conn)
if err != nil {
return nullRes, err
}
return result, nil
}
// Visit handles the making of the request, parsing of maps, and returning
// the correct information to the client
func Visit(gophertype, host, port, resource string) (string, []string, error) {
resp, err := Retrieve(host, port, resource)
if err != nil {
return "", []string{}, err
}
text := string(resp)
links := []string{}
if IsDownloadOnly(gophertype) {
return text, []string{}, nil
}
if gophertype == "1" {
text, links = parseMap(text)
}
return text, links, nil
}
func getType(t string) string {
if val, ok := types[t]; ok {
return val
}
return "???"
}
func isWebLink(resource string) (string, bool) {
split := strings.SplitN(resource, ":", 2)
if first := strings.ToUpper(split[0]); first == "URL" && len(split) > 1 {
return split[1], true
}
return "", false
}
func parseMap(text string) (string, []string) {
splitContent := strings.Split(text, "\n")
links := make([]string, 0, 10)
for i, e := range splitContent {
e = strings.Trim(e, "\r\n")
if e == "." {
splitContent[i] = ""
continue
}
line := strings.Split(e, "\t")
var title string
if len(line[0]) > 1 {
title = line[0][1:]
} else if len(line[0]) == 1 {
title = ""
} else {
title = ""
line[0] = "i"
}
if len(line) < 4 || strings.HasPrefix(line[0], "i") {
splitContent[i] = " " + string(title)
} else {
link := buildLink(line[2], line[3], string(line[0][0]), line[1])
links = append(links, link)
linkNum := fmt.Sprintf("[%d]",len(links))
linktext := fmt.Sprintf("%s %5s %s", getType(string(line[0][0])), linkNum, title)
splitContent[i] = linktext
}
}
return strings.Join(splitContent, "\n"), links
}
// Returns false for all text formats (including html
// even though it may link out. Things like telnet
// should never make it into the retrieve call for
// this module, having been handled in the client
// based on their protocol.
func IsDownloadOnly(gophertype string) bool {
switch gophertype {
case "0", "1", "3", "7", "h":
return false
default:
return true
}
}
func buildLink(host, port, gtype, resource string) string {
switch gtype {
case "8", "T":
return fmt.Sprintf("telnet://%s:%s", host, port)
case "G":
return fmt.Sprintf("gemini://%s:%s%s", host, port, resource)
case "h":
u, tf := isWebLink(resource)
if tf {
if strings.Index(u, "://") > 0 {
return u
} else {
return fmt.Sprintf("http://%s", u)
}
}
return fmt.Sprintf("gopher://%s:%s/h%s", host, port, resource)
default:
return fmt.Sprintf("gopher://%s:%s/%s%s", host, port, gtype, resource)
}
}