-
Notifications
You must be signed in to change notification settings - Fork 1
/
safelinks.go
191 lines (150 loc) · 4.98 KB
/
safelinks.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
// Copyright 2022 Adam Chalkley
//
// https://github.com/atc0005/safelinks
//
// Licensed under the MIT License. See LICENSE file in the project root for
// full license information.
package safelinks
import (
"bufio"
"flag"
"fmt"
"html"
"io"
"net/url"
"os"
"strings"
)
// ReadURLFromUser attempts to read a given URL pattern from the user via
// stdin prompt.
func ReadURLFromUser() (string, error) {
fmt.Print("Enter URL: ")
// NOTE: fmt.Scanln does not seem to handle the length of the input URL
// properly. We use bufio.NewScanner to work around this.
//
// var input string
// _, err := fmt.Scanln(&input)
scanner := bufio.NewScanner(os.Stdin)
scanner.Scan()
return scanner.Text(), scanner.Err()
}
// ReadURLsFromFile attempts to read URL patterns from a given file
// (io.Reader).
//
// The collection of input URLs is returned or an error if one occurs.
func ReadURLsFromFile(r io.Reader) ([]string, error) {
var inputURLs []string
// Loop over input "reader" and attempt to collect each item.
scanner := bufio.NewScanner((r))
for scanner.Scan() {
txt := scanner.Text()
if strings.TrimSpace(txt) == "" {
continue
}
inputURLs = append(inputURLs, txt)
}
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("error reading URLs: %w", err)
}
if len(inputURLs) == 0 {
return nil, ErrInvalidURL
}
return inputURLs, nil
}
// ProcessInputAsURL processes a given input string as a URL value. This
// input string represents a single URL given via CLI flag.
//
// If an input string is not provided, this function will attempt to read
// input URLs from stdin. Each input URL is unescaped and quoting removed.
//
// The collection of input URLs is returned or an error if one occurs.
func ProcessInputAsURL(inputURL string) ([]string, error) {
var inputURLs []string
// https://stackoverflow.com/questions/22744443/check-if-there-is-something-to-read-on-stdin-in-golang
// https://stackoverflow.com/a/26567513/903870
// stat, _ := os.Stdin.Stat()
// if (stat.Mode() & os.ModeCharDevice) == 0 {
// fmt.Println("data is being piped to stdin")
// } else {
// fmt.Println("stdin is from a terminal")
// }
stat, _ := os.Stdin.Stat()
switch {
// We received one or more URLs via standard input.
case (stat.Mode() & os.ModeCharDevice) == 0:
// fmt.Fprintln(os.Stderr, "Received URL via standard input")
return ReadURLsFromFile(os.Stdin)
// We received a URL via positional argument. We ignore all but the first
// one.
case len(flag.Args()) > 0:
// fmt.Fprintln(os.Stderr, "Received URL via positional argument")
if strings.TrimSpace(flag.Args()[0]) == "" {
return nil, ErrInvalidURL
}
inputURLs = append(inputURLs, cleanURL(flag.Args()[0]))
// We received a URL via flag.
case inputURL != "":
// fmt.Fprintln(os.Stderr, "Received URL via flag")
inputURLs = append(inputURLs, cleanURL(inputURL))
// Input URL not given via positional argument, not given via flag either.
// We prompt the user for a single input value.
default:
// fmt.Fprintln(os.Stderr, "default switch case triggered")
input, err := ReadURLFromUser()
if err != nil {
return nil, fmt.Errorf("error reading URL: %w", err)
}
if strings.TrimSpace(input) == "" {
return nil, ErrInvalidURL
}
inputURLs = append(inputURLs, cleanURL(input))
}
return inputURLs, nil
}
// cleanURL strips away quoting or escaping of characters in a given URL.
func cleanURL(s string) string {
// Strip off any quoting that may be present.
s = strings.ReplaceAll(s, `'`, "")
s = strings.ReplaceAll(s, `"`, "")
// Replace escaped ampersands with literal ampersands.
// inputURL = strings.ReplaceAll(flag.Args()[1], "&", "&")
// Use html package to handle ampersand escaping *and* any edge cases that
// I may be unaware of.
s = html.UnescapeString(s)
return s
}
// assertValidURLParameter requires that the given url.URL contains a
// non-empty parameter named url.
func assertValidURLParameter(u *url.URL) error {
urlValues := u.Query()
if urlValues.Get("url") == "" {
return ErrOriginalURLNotResolved
}
return nil
}
// ProcessInputURLs processes a given collection of input URL strings and
// emits successful decoding results to the specified results output sink.
// Errors are emitted to the specified error output sink if encountered but
// bulk processing continues until all input URLs have been evaluated.
//
// If requested, decoded URLs are emitted in verbose format.
//
// A boolean value is returned indicating whether any errors occurred.
func ProcessInputURLs(inputURLs []string, okOut io.Writer, errOut io.Writer, verbose bool) bool {
var errEncountered bool
for _, inputURL := range inputURLs {
safelink, err := url.Parse(inputURL)
if err != nil {
fmt.Printf("Failed to parse URL: %v\n", err)
errEncountered = true
continue
}
if err := assertValidURLParameter(safelink); err != nil {
fmt.Fprintf(errOut, "Invalid Safelinks URL %q: %v\n", safelink, err)
errEncountered = true
continue
}
emitOutput(safelink, okOut, verbose)
}
return errEncountered
}