package spartan import ( "bytes" "errors" "io" "net" neturl "net/url" "strconv" "tildegit.org/tjp/sliderule/internal/types" ) // Client is used for sending spartan requests and receiving responses. // // It carries no state and is reusable simultaneously by multiple goroutines. // // The zero value is immediately usabble, but will not follow redirects. type Client struct{ MaxRedirects int } // NewClient creates a spartan Client which will follow DefaultMaxRedirects. func NewClient() Client { return Client{MaxRedirects: DefaultMaxRedirects} } // DefaultMaxRedirects is the number of chained redirects a Client will perform for a // single request by default. This can be changed by altering the MaxRedirects field. const DefaultMaxRedirects int = 2 var ExceededMaxRedirects = errors.New("spartan.Client: exceeded MaxRedirects") // RoundTrip sends a single spartan request and returns its response. func (c Client) RoundTrip(request *types.Request) (*types.Response, error) { if request.Scheme != "spartan" && request.Scheme != "" { return nil, errors.New("non-spartan protocols not supported") } host, port, _ := net.SplitHostPort(request.Host) if port == "" { host = request.Host port = "300" } addr := net.JoinHostPort(host, port) conn, err := net.Dial("tcp", addr) if err != nil { return nil, err } defer conn.Close() request.RemoteAddr = conn.RemoteAddr() var rdr *io.LimitedReader if request.Meta == nil { rdr = &io.LimitedReader{R: devnull{}, N: 0} } else { var ok bool rdr, ok = request.Meta.(*io.LimitedReader) if !ok { return nil, errors.New("request body must be nil or an *io.LimitedReader") } } requestLine := host + " " + request.EscapedPath() + " " + strconv.Itoa(int(rdr.N)) + "\r\n" if _, err := conn.Write([]byte(requestLine)); err != nil { return nil, err } if _, err := io.Copy(conn, rdr); err != nil { return nil, err } response, err := ParseResponse(conn) if err != nil { return nil, err } bodybuf, err := io.ReadAll(response.Body) if err != nil { return nil, err } response.Body = bytes.NewBuffer(bodybuf) response.Request = request return response, nil } // Fetch parses a URL string and fetches the spartan resource. // // It will resolve any redirects along the way, up to client.MaxRedirects. func (c Client) Fetch(url string) (*types.Response, error) { u, err := neturl.Parse(url) if err != nil { return nil, err } for i := 0; i <= c.MaxRedirects; i += 1 { response, err := c.RoundTrip(&types.Request{URL: u}) if err != nil { return nil, err } if !c.IsRedirect(response) { return response, nil } prev := u u, err = neturl.Parse(response.Meta.(string)) if err != nil { return nil, err } u = prev.ResolveReference(u) } return nil, ExceededMaxRedirects } func (c Client) IsRedirect(response *types.Response) bool { return response.Status == StatusRedirect } type devnull struct{} func (_ devnull) Read(p []byte) (int, error) { return 0, nil }