Compare commits

...

2 Commits

Author SHA1 Message Date
sloum 7b7505990f Increments version 2020-06-13 14:48:53 -07:00
sloum 36eb7565e0 Working mercury protocol implementation 2020-06-13 11:50:53 -07:00
5 changed files with 276 additions and 2 deletions

View File

@ -1 +1 @@
2.3.1
2.3.2

View File

@ -18,6 +18,7 @@ import (
"tildegit.org/sloum/bombadillo/gopher"
"tildegit.org/sloum/bombadillo/http"
"tildegit.org/sloum/bombadillo/local"
"tildegit.org/sloum/bombadillo/mercury"
"tildegit.org/sloum/bombadillo/telnet"
"tildegit.org/sloum/bombadillo/termios"
)
@ -703,7 +704,7 @@ func (c *client) search(query, uri, question string) {
rootUrl = u.Full
}
c.Visit(fmt.Sprintf("%s\t%s", rootUrl, entry))
case "gemini":
case "gemini", "mercury":
if ind := strings.Index(u.Full, "?"); ind >= 0 {
rootUrl = u.Full[:ind]
} else {
@ -913,6 +914,8 @@ func (c *client) Visit(url string) {
c.handleGopher(u)
case "gemini":
c.handleGemini(u)
case "mercury":
c.handleMercury(u)
case "telnet":
c.handleTelnet(u)
case "http", "https":
@ -1012,6 +1015,53 @@ func (c *client) handleGemini(u Url) {
}
}
func (c *client) handleMercury(u Url) {
capsule, err := mercury.Visit(u.Host, u.Port, u.Resource)
if err != nil {
c.SetMessage(err.Error(), true)
c.DrawMessage()
return
}
switch capsule.Status {
case 1:
// Query
c.search("", u.Full, capsule.Content)
case 2:
// Success
if capsule.MimeMaj == "text" || (c.Options["showimages"] == "true" && capsule.MimeMaj == "image") {
pg := MakePage(u, capsule.Content, capsule.Links)
pg.FileType = capsule.MimeMaj
pg.WrapContent(c.Width-1, (c.Options["theme"] == "color"))
c.PageState.Add(pg)
c.SetPercentRead()
c.ClearMessage()
c.SetHeaderUrl()
c.Draw()
} else {
c.SetMessage("The file is non-text: writing to disk...", false)
c.DrawMessage()
c.saveFileFromData(capsule.Content, filepath.Base(u.Resource))
}
case 3:
// Redirect
lowerRedirect := strings.ToLower(capsule.Content)
lowerOriginal := strings.ToLower(u.Full)
if strings.Replace(lowerRedirect, lowerOriginal, "", 1) == "/" {
c.Visit(capsule.Content)
} else {
c.SetMessage(fmt.Sprintf("Follow redirect (y/n): %s?", capsule.Content), false)
c.DrawMessage()
ch := cui.Getch()
if ch == 'y' || ch == 'Y' {
c.Visit(capsule.Content)
} else {
c.SetMessage("Redirect aborted", false)
c.DrawMessage()
}
}
}
}
func (c *client) handleTelnet(u Url) {
c.SetMessage("Attempting to start telnet session", false)
c.DrawMessage()

View File

@ -61,6 +61,7 @@ func Retrieve(host, port, resource string) ([]byte, error) {
if err != nil {
return nullRes, err
}
defer conn.Close()
send := resource + "\n"

221
mercury/mercury.go Normal file
View File

@ -0,0 +1,221 @@
package mercury
import (
"fmt"
"io/ioutil"
"net"
"net/url"
"strconv"
"strings"
"time"
)
type Capsule struct {
MimeMaj string
MimeMin string
Status int
Content string
Links []string
}
//------------------------------------------------\\
// + + + F U N C T I O N S + + + \\
//--------------------------------------------------\\
func Retrieve(host, port, resource string) (string, error) {
timeOut := time.Duration(5) * time.Second
if host == "" || port == "" {
return "", fmt.Errorf("Incomplete request url")
}
addr := host + ":" + port
conn, err := net.DialTimeout("tcp", addr, timeOut)
if err != nil {
return "", err
}
defer conn.Close()
send := "mercury://" + addr + "/" + resource + "\r\n"
_, err = conn.Write([]byte(send))
if err != nil {
return "", err
}
result, err := ioutil.ReadAll(conn)
if err != nil {
return "", err
}
return string(result), nil
}
func Fetch(host, port, resource string) ([]byte, error) {
rawResp, err := Retrieve(host, port, resource)
if err != nil {
return make([]byte, 0), err
}
resp := strings.SplitN(rawResp, "\r\n", 2)
if len(resp) != 2 {
if err != nil {
return make([]byte, 0), fmt.Errorf("Invalid response from server")
}
}
header := strings.SplitN(resp[0], " ", 2)
if len([]rune(header[0])) != 2 {
header = strings.SplitN(resp[0], "\t", 2)
if len([]rune(header[0])) != 2 {
return make([]byte, 0), fmt.Errorf("Invalid response format from server")
}
}
// Get status code single digit form
status, err := strconv.Atoi(string(header[0][0]))
if err != nil {
return make([]byte, 0), fmt.Errorf("Invalid status response from server")
}
if status != 2 {
switch status {
case 1:
return make([]byte, 0), fmt.Errorf("[1] Queries cannot be saved.")
case 3:
return make([]byte, 0), fmt.Errorf("[3] Redirects cannot be saved.")
case 4:
return make([]byte, 0), fmt.Errorf("[4] Temporary Failure.")
case 5:
return make([]byte, 0), fmt.Errorf("[5] Permanent Failure.")
default:
return make([]byte, 0), fmt.Errorf("Invalid response status from server")
}
}
return []byte(resp[1]), nil
}
func Visit(host, port, resource string) (Capsule, error) {
capsule := MakeCapsule()
rawResp, err := Retrieve(host, port, resource)
if err != nil {
return capsule, err
}
resp := strings.SplitN(rawResp, "\r\n", 2)
if len(resp) != 2 {
if err != nil {
return capsule, fmt.Errorf("Invalid response from server")
}
}
header := strings.SplitN(resp[0], " ", 2)
if len([]rune(header[0])) > 2 {
return capsule, fmt.Errorf("%d: %s", len(header), header[0]) // TODO return this to a regular error message
}
body := resp[1]
// Get status code single digit form
capsule.Status, err = strconv.Atoi(string(header[0][0]))
if err != nil {
return capsule, fmt.Errorf("Invalid status response from server")
}
// Parse the meta as needed
var meta string
switch capsule.Status {
case 1:
capsule.Content = header[1]
return capsule, nil
case 2:
mimeAndCharset := strings.Split(header[1], ";")
meta = mimeAndCharset[0]
minMajMime := strings.Split(meta, "/")
if len(minMajMime) < 2 {
return capsule, fmt.Errorf("Improperly formatted mimetype received from server")
}
capsule.MimeMaj = minMajMime[0]
capsule.MimeMin = minMajMime[1]
if capsule.MimeMaj == "text" && capsule.MimeMin == "gemini" {
if len(resource) > 0 && resource[0] != '/' {
resource = fmt.Sprintf("/%s", resource)
} else if resource == "" {
resource = "/"
}
currentUrl := fmt.Sprintf("mercury://%s:%s%s", host, port, resource)
capsule.Content, capsule.Links = parseMercury(body, currentUrl)
} else {
capsule.Content = body
}
return capsule, nil
case 3:
// The client will handle informing the user of a redirect
// and then request the new url
capsule.Content = header[1]
return capsule, nil
case 4:
return capsule, fmt.Errorf("[4] Temporary Failure. %s", header[1])
case 5:
return capsule, fmt.Errorf("[5] Permanent Failure. %s", header[1])
default:
return capsule, fmt.Errorf("Invalid response status from server")
}
}
func parseMercury(b, currentUrl string) (string, []string) {
splitContent := strings.Split(b, "\n")
links := make([]string, 0, 10)
outputIndex := 0
for i, ln := range splitContent {
splitContent[i] = strings.Trim(ln, "\r\n")
if strings.HasPrefix(ln, "=>") && len(ln) > 2 {
var link, decorator string
subLn := strings.Trim(ln[2:], "\r\n\t \a")
splitPoint := strings.IndexAny(subLn, " \t")
if splitPoint < 0 || len([]rune(subLn))-1 <= splitPoint {
link = subLn
decorator = subLn
} else {
link = strings.Trim(subLn[:splitPoint], "\t\n\r \a")
decorator = strings.Trim(subLn[splitPoint:], "\t\n\r \a")
}
if strings.Index(link, "://") < 0 {
link, _ = handleRelativeUrl(link, currentUrl)
} else if strings.HasPrefix(link, "//") {
link = fmt.Sprintf("mercury:%s", link)
}
links = append(links, link)
linknum := fmt.Sprintf("[%d]", len(links))
splitContent[outputIndex] = fmt.Sprintf("%-5s %s", linknum, decorator)
outputIndex++
} else {
splitContent[outputIndex] = ln
outputIndex++
}
}
return strings.Join(splitContent[:outputIndex], "\n"), links
}
// handleRelativeUrl provides link completion
func handleRelativeUrl(relLink, current string) (string, error) {
base, err := url.Parse(current)
if err != nil {
return relLink, err
}
rel, err := url.Parse(relLink)
if err != nil {
return relLink, err
}
return base.ResolveReference(rel).String(), nil
}
func MakeCapsule() Capsule {
return Capsule{"", "", 0, "", make([]string, 0, 5)}
}

2
url.go
View File

@ -115,6 +115,8 @@ func MakeUrl(u string) (Url, error) {
out.Port = "443"
} else if out.Scheme == "gemini" && out.Port == "" {
out.Port = "1965"
} else if out.Scheme == "mercury" && out.Port == "" {
out.Port = "1961"
} else if out.Scheme == "telnet" && out.Port == "" {
out.Port = "23"
}