1
2
3
4
5
6
7
8
9
10 package web
11
12 import (
13 "bytes"
14 "fmt"
15 "io"
16 "io/fs"
17 "net/url"
18 "strings"
19 "unicode"
20 "unicode/utf8"
21 )
22
23
24
25
26 type SecurityMode int
27
28 const (
29 SecureOnly SecurityMode = iota
30 DefaultSecurity
31 Insecure
32 )
33
34
35 type HTTPError struct {
36 URL string
37 Status string
38 StatusCode int
39 Err error
40 Detail string
41 }
42
43 const (
44 maxErrorDetailLines = 8
45 maxErrorDetailBytes = maxErrorDetailLines * 81
46 )
47
48 func (e *HTTPError) Error() string {
49 if e.Detail != "" {
50 detailSep := " "
51 if strings.ContainsRune(e.Detail, '\n') {
52 detailSep = "\n\t"
53 }
54 return fmt.Sprintf("reading %s: %v\n\tserver response:%s%s", e.URL, e.Status, detailSep, e.Detail)
55 }
56
57 if err := e.Err; err != nil {
58 if pErr, ok := e.Err.(*fs.PathError); ok && strings.HasSuffix(e.URL, pErr.Path) {
59
60 err = pErr.Err
61 }
62 return fmt.Sprintf("reading %s: %v", e.URL, err)
63 }
64
65 return fmt.Sprintf("reading %s: %v", e.URL, e.Status)
66 }
67
68 func (e *HTTPError) Is(target error) bool {
69 return target == fs.ErrNotExist && (e.StatusCode == 404 || e.StatusCode == 410)
70 }
71
72 func (e *HTTPError) Unwrap() error {
73 return e.Err
74 }
75
76
77
78
79
80 func GetBytes(u *url.URL) ([]byte, error) {
81 resp, err := Get(DefaultSecurity, u)
82 if err != nil {
83 return nil, err
84 }
85 defer resp.Body.Close()
86 if err := resp.Err(); err != nil {
87 return nil, err
88 }
89 b, err := io.ReadAll(resp.Body)
90 if err != nil {
91 return nil, fmt.Errorf("reading %s: %v", u.Redacted(), err)
92 }
93 return b, nil
94 }
95
96 type Response struct {
97 URL string
98 Status string
99 StatusCode int
100 Header map[string][]string
101 Body io.ReadCloser
102
103 fileErr error
104 errorDetail errorDetailBuffer
105 }
106
107
108
109
110 func (r *Response) Err() error {
111 if r.StatusCode == 200 || r.StatusCode == 0 {
112 return nil
113 }
114
115 return &HTTPError{
116 URL: r.URL,
117 Status: r.Status,
118 StatusCode: r.StatusCode,
119 Err: r.fileErr,
120 Detail: r.formatErrorDetail(),
121 }
122 }
123
124
125
126 func (r *Response) formatErrorDetail() string {
127 if r.Body != &r.errorDetail {
128 return ""
129 }
130
131
132 _, _ = io.Copy(io.Discard, r.Body)
133
134 s := r.errorDetail.buf.String()
135 if !utf8.ValidString(s) {
136 return ""
137 }
138 for _, r := range s {
139 if !unicode.IsGraphic(r) && !unicode.IsSpace(r) {
140 return ""
141 }
142 }
143
144 var detail strings.Builder
145 for i, line := range strings.Split(s, "\n") {
146 if strings.TrimSpace(line) == "" {
147 break
148 }
149 if i > 0 {
150 detail.WriteString("\n\t")
151 }
152 if i >= maxErrorDetailLines {
153 detail.WriteString("[Truncated: too many lines.]")
154 break
155 }
156 if detail.Len()+len(line) > maxErrorDetailBytes {
157 detail.WriteString("[Truncated: too long.]")
158 break
159 }
160 detail.WriteString(line)
161 }
162
163 return detail.String()
164 }
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181 func Get(security SecurityMode, u *url.URL) (*Response, error) {
182 return get(security, u)
183 }
184
185
186 func OpenBrowser(url string) (opened bool) {
187 return openBrowser(url)
188 }
189
190
191
192 func Join(u *url.URL, path string) *url.URL {
193 j := *u
194 if path == "" {
195 return &j
196 }
197 j.Path = strings.TrimSuffix(u.Path, "/") + "/" + strings.TrimPrefix(path, "/")
198 j.RawPath = strings.TrimSuffix(u.RawPath, "/") + "/" + strings.TrimPrefix(path, "/")
199 return &j
200 }
201
202
203
204 type errorDetailBuffer struct {
205 r io.ReadCloser
206 buf strings.Builder
207 bufLines int
208 }
209
210 func (b *errorDetailBuffer) Close() error {
211 return b.r.Close()
212 }
213
214 func (b *errorDetailBuffer) Read(p []byte) (n int, err error) {
215 n, err = b.r.Read(p)
216
217
218
219
220
221
222
223 if b.bufLines <= maxErrorDetailLines {
224 for _, line := range bytes.SplitAfterN(p[:n], []byte("\n"), maxErrorDetailLines-b.bufLines) {
225 b.buf.Write(line)
226 if len(line) > 0 && line[len(line)-1] == '\n' {
227 b.bufLines++
228 if b.bufLines > maxErrorDetailLines {
229 break
230 }
231 }
232 }
233 }
234
235 return n, err
236 }
237
View as plain text