diff --git a/client.go b/client.go index 766bd96..4a49108 100644 --- a/client.go +++ b/client.go @@ -14,7 +14,7 @@ import ( "tildegit.org/sloum/bombadillo/cmdparse" "tildegit.org/sloum/bombadillo/cui" - // "tildegit.org/sloum/bombadillo/gemini" + "tildegit.org/sloum/bombadillo/gemini" "tildegit.org/sloum/bombadillo/gopher" "tildegit.org/sloum/bombadillo/http" "tildegit.org/sloum/bombadillo/telnet" @@ -715,9 +715,35 @@ func (c *client) Visit(url string) { c.SetHeaderUrl() c.Draw() case "gemini": - // TODO send over to gemini request - c.SetMessage("Bombadillo has not mastered Gemini yet, check back soon", false) - c.DrawMessage() + capsule, err := gemini.Visit(u.Host, u.Port, u.Resource) + if err != nil { + c.SetMessage(err.Error(), true) + c.DrawMessage() + return + } + switch capsule.Status { + case 2: + pg := MakePage(u, capsule.Content, capsule.Links) + pg.WrapContent(c.Width) + c.PageState.Add(pg) + c.Scroll(0) + c.ClearMessage() + c.SetHeaderUrl() + c.Draw() + case 3: + c.SetMessage("[3] Redirect. Follow redirect? y or any other key for no", false) + c.DrawMessage() + ch := cui.Getch() + if ch == 'y' || ch == 'Y' { + c.Visit(capsule.Content) + } else { + c.SetMessage("Redirect aborted", false) + c.DrawMessage() + } + } + + // c.SetMessage("Bombadillo has not mastered Gemini yet, check back soon", false) + // c.DrawMessage() case "telnet": c.SetMessage("Attempting to start telnet session", false) c.DrawMessage() diff --git a/gemini/gemini.go b/gemini/gemini.go index d0f3ce9..e8edee0 100644 --- a/gemini/gemini.go +++ b/gemini/gemini.go @@ -3,81 +3,154 @@ package gemini import ( "crypto/tls" "fmt" - "net" "io/ioutil" - // "strings" - "time" + "strconv" + "strings" // "tildegit.org/sloum/mailcap" ) +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) ([]byte, error) { - nullRes := make([]byte, 0) - timeOut := time.Duration(5) * time.Second - +func Retrieve(host, port, resource string) (string, error) { if host == "" || port == "" { - return nullRes, fmt.Errorf("Incomplete request url") + return "", fmt.Errorf("Incomplete request url") } addr := host + ":" + port conf := &tls.Config{ + MinVersion: tls.VersionTLS12, InsecureSkipVerify: true, } - conn, err := net.DialTimeout("tcp", addr, timeOut) + conn, err := tls.Dial("tcp", addr, conf) if err != nil { - return nullRes, err + return "", err } - secureConn := tls.Client(conn, conf) + defer conn.Close() - send := resource + "\n" + send := "gemini://" + addr + "/" + resource + "\r\n" - _, err = secureConn.Write([]byte(send)) + _, err = conn.Write([]byte(send)) if err != nil { - return nullRes, err + return "", err } result, err := ioutil.ReadAll(conn) if err != nil { - return nullRes, err + return "", err } - return result, nil + return string(result), nil } -func Visit(host, port, resource string) (string, []string, error) { - resp, err := Retrieve(host, port, resource) +func Visit(host, port, resource string) (Capsule, error) { + capsule := MakeCapsule() + rawResp, err := Retrieve(host, port, resource) if err != nil { - return "", []string{}, err + 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 { + header = strings.SplitN(resp[0], "\t", 2) + if len([]rune(header[0])) != 2 { + return capsule, fmt.Errorf("Invalid response format from server") + } + } + + body := resp[1] - // TODO break out the header - // header := "" - mime := "" - mimeMaj := mime - mimeMin := mime - // status := "" - content := string(resp) - - if mimeMaj == "text" && mimeMin == "gemini" { - // text := string(resp) - // links := []string{} - - // TODO parse geminimap from 'content' - } else if mimeMaj == "text" { - // TODO just return the text - } else { - // TODO use mailcap to try and open the file + // 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 - return content, []string{}, nil + switch capsule.Status { + case 1: + // handle search + return capsule, fmt.Errorf("Gemini input not yet supported") + 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" { + rootUrl := fmt.Sprintf("gemini://%s:%s", host, port) + capsule.Content, capsule.Links = parseGemini(body, rootUrl) + } 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]) + case 6: + return capsule, fmt.Errorf("[6] Client Certificate Required (Not supported by Bombadillo)") + default: + return capsule, fmt.Errorf("Invalid response status from server") + } +} + +func parseGemini(b, rootUrl string) (string, []string) { + splitContent := strings.Split(b, "\n") + links := make([]string, 0, 10) + + for i, ln := range splitContent { + splitContent[i] = strings.Trim(ln, "\r\n") + if len([]rune(ln)) > 3 && ln[:2] == "=>" { + trimmedSubLn := strings.Trim(ln[2:], "\r\n\t \a") + lineSplit := strings.SplitN(trimmedSubLn, " ", 2) + if len(lineSplit) != 2 { + lineSplit = append(lineSplit, lineSplit[0]) + } + lineSplit[0] = strings.Trim(lineSplit[0], "\t\n\r \a") + lineSplit[1] = strings.Trim(lineSplit[1], "\t\n\r \a") + if len(lineSplit[0]) > 0 && lineSplit[0][0] == '/' { + lineSplit[0] = fmt.Sprintf("%s%s", rootUrl, lineSplit[0]) + } + links = append(links, lineSplit[0]) + linknum := fmt.Sprintf("[%d]", len(links)) + splitContent[i] = fmt.Sprintf("%-5s %s", linknum, lineSplit[1]) + } + } + return strings.Join(splitContent, "\n"), links +} + + +func MakeCapsule() Capsule { + return Capsule{"", "", 0, "", make([]string, 0, 5)} } diff --git a/url.go b/url.go index faedbbb..5babdc8 100644 --- a/url.go +++ b/url.go @@ -97,8 +97,8 @@ func MakeUrl(u string) (Url, error) { out.DownloadOnly = true } } else { - out.Resource = fmt.Sprintf("%s%s", out.Mime, out.Resource) out.Mime = "" + out.Resource = fmt.Sprintf("%s%s", out.Mime, out.Resource) } if out.Scheme == "http" || out.Scheme == "https" {