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 eErr := e.Err; eErr != nil {
58 if pErr, ok := e.Err.(*fs.PathError); ok {
59 if u, err := url.Parse(e.URL); err == nil {
60 if fp, err := urlToFilePath(u); err == nil && pErr.Path == fp {
61
62 eErr = pErr.Err
63 }
64 }
65 }
66 return fmt.Sprintf("reading %s: %v", e.URL, eErr)
67 }
68
69 return fmt.Sprintf("reading %s: %v", e.URL, e.Status)
70 }
71
72 func (e *HTTPError) Is(target error) bool {
73 return target == fs.ErrNotExist && (e.StatusCode == 404 || e.StatusCode == 410)
74 }
75
76 func (e *HTTPError) Unwrap() error {
77 return e.Err
78 }
79
80
81
82
83
84 func GetBytes(u *url.URL) ([]byte, error) {
85 resp, err := Get(DefaultSecurity, u)
86 if err != nil {
87 return nil, err
88 }
89 defer resp.Body.Close()
90 if err := resp.Err(); err != nil {
91 return nil, err
92 }
93 b, err := io.ReadAll(resp.Body)
94 if err != nil {
95 return nil, fmt.Errorf("reading %s: %v", u.Redacted(), err)
96 }
97 return b, nil
98 }
99
100 type Response struct {
101 URL string
102 Status string
103 StatusCode int
104 Header map[string][]string
105 Body io.ReadCloser
106
107 fileErr error
108 errorDetail errorDetailBuffer
109 }
110
111
112
113
114 func (r *Response) Err() error {
115 if r.StatusCode == 200 || r.StatusCode == 0 {
116 return nil
117 }
118
119 return &HTTPError{
120 URL: r.URL,
121 Status: r.Status,
122 StatusCode: r.StatusCode,
123 Err: r.fileErr,
124 Detail: r.formatErrorDetail(),
125 }
126 }
127
128
129
130 func (r *Response) formatErrorDetail() string {
131 if r.Body != &r.errorDetail {
132 return ""
133 }
134
135
136 _, _ = io.Copy(io.Discard, r.Body)
137
138 s := r.errorDetail.buf.String()
139 if !utf8.ValidString(s) {
140 return ""
141 }
142 for _, r := range s {
143 if !unicode.IsGraphic(r) && !unicode.IsSpace(r) {
144 return ""
145 }
146 }
147
148 var detail strings.Builder
149 for i, line := range strings.Split(s, "\n") {
150 if strings.TrimSpace(line) == "" {
151 break
152 }
153 if i > 0 {
154 detail.WriteString("\n\t")
155 }
156 if i >= maxErrorDetailLines {
157 detail.WriteString("[Truncated: too many lines.]")
158 break
159 }
160 if detail.Len()+len(line) > maxErrorDetailBytes {
161 detail.WriteString("[Truncated: too long.]")
162 break
163 }
164 detail.WriteString(line)
165 }
166
167 return detail.String()
168 }
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185 func Get(security SecurityMode, u *url.URL) (*Response, error) {
186 return get(security, u)
187 }
188
189
190 func OpenBrowser(url string) (opened bool) {
191 return openBrowser(url)
192 }
193
194
195
196 func Join(u *url.URL, path string) *url.URL {
197 j := *u
198 if path == "" {
199 return &j
200 }
201 j.Path = strings.TrimSuffix(u.Path, "/") + "/" + strings.TrimPrefix(path, "/")
202 j.RawPath = strings.TrimSuffix(u.RawPath, "/") + "/" + strings.TrimPrefix(path, "/")
203 return &j
204 }
205
206
207
208 type errorDetailBuffer struct {
209 r io.ReadCloser
210 buf strings.Builder
211 bufLines int
212 }
213
214 func (b *errorDetailBuffer) Close() error {
215 return b.r.Close()
216 }
217
218 func (b *errorDetailBuffer) Read(p []byte) (n int, err error) {
219 n, err = b.r.Read(p)
220
221
222
223
224
225
226
227 if b.bufLines <= maxErrorDetailLines {
228 for _, line := range bytes.SplitAfterN(p[:n], []byte("\n"), maxErrorDetailLines-b.bufLines) {
229 b.buf.Write(line)
230 if len(line) > 0 && line[len(line)-1] == '\n' {
231 b.bufLines++
232 if b.bufLines > maxErrorDetailLines {
233 break
234 }
235 }
236 }
237 }
238
239 return n, err
240 }
241
242
243
244 func IsLocalHost(u *url.URL) bool {
245 return isLocalHost(u)
246 }
247
View as plain text