forked from sloum/bombadillo
Compare commits
2 Commits
Author | SHA1 | Date |
---|---|---|
sloum | 7b7505990f | |
sloum | 36eb7565e0 |
52
client.go
52
client.go
|
@ -18,6 +18,7 @@ import (
|
||||||
"tildegit.org/sloum/bombadillo/gopher"
|
"tildegit.org/sloum/bombadillo/gopher"
|
||||||
"tildegit.org/sloum/bombadillo/http"
|
"tildegit.org/sloum/bombadillo/http"
|
||||||
"tildegit.org/sloum/bombadillo/local"
|
"tildegit.org/sloum/bombadillo/local"
|
||||||
|
"tildegit.org/sloum/bombadillo/mercury"
|
||||||
"tildegit.org/sloum/bombadillo/telnet"
|
"tildegit.org/sloum/bombadillo/telnet"
|
||||||
"tildegit.org/sloum/bombadillo/termios"
|
"tildegit.org/sloum/bombadillo/termios"
|
||||||
)
|
)
|
||||||
|
@ -703,7 +704,7 @@ func (c *client) search(query, uri, question string) {
|
||||||
rootUrl = u.Full
|
rootUrl = u.Full
|
||||||
}
|
}
|
||||||
c.Visit(fmt.Sprintf("%s\t%s", rootUrl, entry))
|
c.Visit(fmt.Sprintf("%s\t%s", rootUrl, entry))
|
||||||
case "gemini":
|
case "gemini", "mercury":
|
||||||
if ind := strings.Index(u.Full, "?"); ind >= 0 {
|
if ind := strings.Index(u.Full, "?"); ind >= 0 {
|
||||||
rootUrl = u.Full[:ind]
|
rootUrl = u.Full[:ind]
|
||||||
} else {
|
} else {
|
||||||
|
@ -913,6 +914,8 @@ func (c *client) Visit(url string) {
|
||||||
c.handleGopher(u)
|
c.handleGopher(u)
|
||||||
case "gemini":
|
case "gemini":
|
||||||
c.handleGemini(u)
|
c.handleGemini(u)
|
||||||
|
case "mercury":
|
||||||
|
c.handleMercury(u)
|
||||||
case "telnet":
|
case "telnet":
|
||||||
c.handleTelnet(u)
|
c.handleTelnet(u)
|
||||||
case "http", "https":
|
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) {
|
func (c *client) handleTelnet(u Url) {
|
||||||
c.SetMessage("Attempting to start telnet session", false)
|
c.SetMessage("Attempting to start telnet session", false)
|
||||||
c.DrawMessage()
|
c.DrawMessage()
|
||||||
|
|
|
@ -61,6 +61,7 @@ func Retrieve(host, port, resource string) ([]byte, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nullRes, err
|
return nullRes, err
|
||||||
}
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
send := resource + "\n"
|
send := resource + "\n"
|
||||||
|
|
||||||
|
|
|
@ -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
2
url.go
|
@ -115,6 +115,8 @@ func MakeUrl(u string) (Url, error) {
|
||||||
out.Port = "443"
|
out.Port = "443"
|
||||||
} else if out.Scheme == "gemini" && out.Port == "" {
|
} else if out.Scheme == "gemini" && out.Port == "" {
|
||||||
out.Port = "1965"
|
out.Port = "1965"
|
||||||
|
} else if out.Scheme == "mercury" && out.Port == "" {
|
||||||
|
out.Port = "1961"
|
||||||
} else if out.Scheme == "telnet" && out.Port == "" {
|
} else if out.Scheme == "telnet" && out.Port == "" {
|
||||||
out.Port = "23"
|
out.Port = "23"
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue