diff --git a/display/handlers.go b/display/handlers.go index aee0149..5f99eeb 100644 --- a/display/handlers.go +++ b/display/handlers.go @@ -406,7 +406,7 @@ func handleURL(t *tab, u string, numRedirects int) (string, bool) { return ret(u, true) } // Not displayable - // Could be a non 20 (or 21) status code, or a different kind of document + // Could be a non 20 status code, or a different kind of document // Handle each status code switch res.Status { @@ -414,7 +414,6 @@ func handleURL(t *tab, u string, numRedirects int) (string, bool) { userInput, ok := Input(res.Meta) if ok { // Make another request with the query string added - // + chars are replaced because PathEscape doesn't do that parsed.RawQuery = gemini.QueryEscape(userInput) if len(parsed.String()) > gemini.URLMaxLength { Error("Input Error", "URL for that input would be too long.") diff --git a/subscriptions/subscriptions.go b/subscriptions/subscriptions.go index 757310f..a879d95 100644 --- a/subscriptions/subscriptions.go +++ b/subscriptions/subscriptions.go @@ -8,6 +8,7 @@ import ( "io" "io/ioutil" "mime" + urlPkg "net/url" "os" "path" "reflect" @@ -23,9 +24,10 @@ import ( ) var ( - ErrSaving = errors.New("couldn't save JSON to disk") - ErrNotSuccess = errors.New("status 20 not returned") - ErrNotFeed = errors.New("not a valid feed") + ErrSaving = errors.New("couldn't save JSON to disk") + ErrNotSuccess = errors.New("status 20 not returned") + ErrNotFeed = errors.New("not a valid feed") + ErrTooManyRedirects = errors.New("redirected more than 5 times") ) var writeMu = sync.Mutex{} // Prevent concurrent writes to subscriptions.json file @@ -115,7 +117,8 @@ func GetFeed(mediatype, filename string, r io.Reader) (*gofeed.Feed, bool) { // Check mediatype and filename if mediatype != "application/atom+xml" && mediatype != "application/rss+xml" && mediatype != "application/json+feed" && filename != "atom.xml" && filename != "feed.xml" && filename != "feed.json" && - !strings.HasSuffix(filename, ".atom") && !strings.HasSuffix(filename, ".rss") && !strings.HasSuffix(filename, ".xml") { + !strings.HasSuffix(filename, ".atom") && !strings.HasSuffix(filename, ".rss") && + !strings.HasSuffix(filename, ".xml") { // No part of the above is true return nil, false } @@ -229,46 +232,133 @@ func AddPage(url string, r io.Reader) error { return nil } -func updateFeed(url string) error { +// getResource returns a URL and Response for the given URL. +// It will follow up to 5 redirects, and if there is a permanent +// redirect it will return the new URL. Otherwise the URL will +// stay the same. THe returned URL will never be empty. +// +// If there is over 5 redirects the error will be ErrTooManyRedirects. +// ErrNotSuccess, as well as other fetch errors will also be returned. +func getResource(url string) (string, *gemini.Response, error) { res, err := client.Fetch(url) if err != nil { if res != nil { res.Body.Close() } - return err + return url, nil, err } - defer res.Body.Close() - if res.Status != gemini.StatusSuccess { - return ErrNotSuccess + if res.Status == gemini.StatusSuccess { + // No redirects + return url, res, nil } - mediatype, _, err := mime.ParseMediaType(res.Meta) + + parsed, err := urlPkg.Parse(url) if err != nil { - return err + return url, nil, err } - filename := path.Base(url) - feed, ok := GetFeed(mediatype, filename, res.Body) - if !ok { - return ErrNotFeed + + i := 0 + redirs := make([]int, 0) + urls := make([]*urlPkg.URL, 0) + + // Loop through redirects + for (res.Status == gemini.StatusRedirectPermanent || res.Status == gemini.StatusRedirectTemporary) && i < 5 { + redirs = append(redirs, res.Status) + urls = append(urls, parsed) + + tmp, err := parsed.Parse(res.Meta) + if err != nil { + // Redirect URL returned by the server is invalid + return url, nil, err + } + parsed = tmp + + // Make the new request + res, err := client.Fetch(parsed.String()) + if err != nil { + if res != nil { + res.Body.Close() + } + return url, nil, err + } + + i++ } - return AddFeed(url, feed) + + // Two possible options here: + // - Never redirected, got error on start + // - No more redirects, other status code + // - Too many redirects + + if i == 0 { + // Never redirected or succeeded + return url, res, ErrNotSuccess + } + + if i < 5 { + // The server stopped redirecting after <5 redirects + + if res.Status == gemini.StatusSuccess { + // It ended by succeeding + + for j := range redirs { + if redirs[j] == gemini.StatusRedirectTemporary { + if j == 0 { + // First redirect is temporary + return url, res, nil + } + // There were permanent redirects before this one + // Return the URL of the latest permanent redirect + return urls[j-1].String(), res, nil + } + } + // They were all permanent redirects + return urls[len(urls)-1].String(), res, nil + } + + // It stopped because there was a non-redirect, non-success response + return url, res, ErrNotSuccess + } + + // Too many redirects, return original + return url, nil, ErrTooManyRedirects } -func updatePage(url string) error { - res, err := client.Fetch(url) +func updateFeed(url string) { + newURL, res, err := getResource(url) if err != nil { - if res != nil { - res.Body.Close() - } - return err - } - defer res.Body.Close() - - if res.Status != gemini.StatusSuccess { - return ErrNotSuccess + return } - return AddPage(url, res.Body) + mediatype, _, err := mime.ParseMediaType(res.Meta) + if err != nil { + return + } + filename := path.Base(newURL) + feed, ok := GetFeed(mediatype, filename, res.Body) + if !ok { + return + } + + AddFeed(newURL, feed) + if url != newURL { + // URL has changed, remove old one + Remove(url) + } +} + +func updatePage(url string) { + newURL, res, err := getResource(url) + if err != nil { + return + } + + AddPage(newURL, res.Body) + if url != newURL { + // URL has changed, remove old one + Remove(url) + } } // updateAll updates all subscriptions using workers.