1package faker
  2
  3import (
  4	"fmt"
  5	"reflect"
  6	"time"
  7
  8	"github.com/go-faker/faker/v4/pkg/interfaces"
  9)
 10
 11var (
 12	browserTypes          = []string{"chrome", "firefox", "internetExplorer", "msedge", "opera", "safari"}
 13	windowsPlatformTokens = []string{"Windows NT 6.2", "Windows NT 6.1", "Windows NT 6.0", "Windows NT 5.2",
 14		"Windows NT 5.1", "Windows NT 5.01", "Windows NT 5.0", "Windows NT 4.0", "Windows 98; Win 9x 4.90",
 15		"Windows 98", "Windows 95", "Windows CE",
 16	}
 17	linuxProcessor = []string{"i686", "x86_64"}
 18	macProcessor   = []string{"Intel", "PPC", "U; Intel", "U; PPC"}
 19	// langCodes contains language codes extracted from https://gist.github.com/JamieMason/3748498.
 20	langCodes = []string{"af-ZA", "ar-AE", "ar-BH", "ar-DZ", "ar-EG", "ar-IQ", "ar-JO", "ar-KW", "ar-LB", "ar-LY",
 21		"ar-MA", "ar-OM", "ar-QA", "ar-SA", "ar-SY", "ar-TN", "ar-YE", "be-BY", "bg-BG", "ca-ES", "cs-CZ", "cy-GB",
 22		"da-DK", "de-AT", "de-DE", "de-CH", "de-LI", "de-LU", "dv-MV", "el-GR", "en-AU", "en-BZ", "en-CA", "en-GB",
 23		"en-IE", "en-JM", "en-NZ", "en-PH", "en-TT", "en-US", "en-ZA", "en-ZW", "es-AR", "es-BO", "es-CL", "es-CO",
 24		"es-CR", "es-DO", "es-EC", "es-ES", "es-GT", "es-HN", "es-MX", "es-NI", "es-PA", "es-PE", "es-PR", "es-PY",
 25		"es-SV", "es-UY", "es-VE", "et-EE", "eu-ES", "fa-IR", "fi-FI", "fo-FO", "fr-BE", "fr-CA", "fr-FR", "fr-CH",
 26		"fr-LU", "fr-MC", "gl-ES", "gu-IN", "he-IL", "hi-IN", "hr-BA", "hr-HR", "hu-HU", "hy-AM", "id-ID", "is-IS",
 27		"it-CH", "it-IT", "ja-JP", "ka-GE", "kk-KZ", "kn-IN", "ko-KR", "ky-KG", "lt-LT", "lv-LV", "mi-NZ", "mk-MK",
 28		"mn-MN", "mr-IN", "ms-BN", "ms-MY", "mt-MT", "nb-NO", "nl-BE", "nl-NL", "nn-NO", "ns-ZA", "pa-IN", "pl-PL",
 29		"pt-BR", "pt-PT", "ro-RO", "ru-RU", "sa-IN", "se-FI", "se-NO", "se-SE", "sk-SK", "sl-SI", "sq-AL", "sv-FI",
 30		"sv-SE", "sw-KE", "ta-IN", "te-IN", "th-TH", "tn-ZA", "tr-TR", "tt-RU", "uk-UA", "ur-PK", "vi-VN", "xh-ZA",
 31		"zh-CN", "zh-HK", "zh-MO", "zh-SG", "zh-TW", "zu-ZA",
 32	}
 33)
 34
 35func GetUserAgent() UserAgenter {
 36	ua := &UserAgent{}
 37	return ua
 38}
 39
 40type UserAgenter interface {
 41	UserAgent(v reflect.Value) (interface{}, error)
 42}
 43
 44// UserAgent implements logic loosely based on https://fakerphp.org/formatters/user-agent/.
 45type UserAgent struct{}
 46
 47func (ua UserAgent) UserAgent(reflect.Value) (interface{}, error) {
 48	browserType := randomElementFromSliceString(browserTypes)
 49	switch browserType {
 50	case "chrome":
 51		return ua.chrome(), nil
 52	case "firefox":
 53		return ua.firefox(), nil
 54	case "internetExplorer":
 55		return ua.internetExplorer(), nil
 56	case "msedge":
 57		return ua.msEdge(), nil
 58	case "opera":
 59		return ua.opera(), nil
 60	case "safari":
 61		return ua.safari(), nil
 62	}
 63	return nil, fmt.Errorf(`unknown browser type: "%s"`, browserType)
 64}
 65
 66// chrome generates a user agent string that resembles Google Chrome.
 67// Example: "Mozilla/5.0 (X11; Linux i686) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/69.0.3460.334 Safari/532.2"
 68func (ua UserAgent) chrome() string {
 69	chromeVer := fmt.Sprintf("%d.0.%d.%d",
 70		ua.intBetween(36, 127), ua.intBetween(0, 6000), ua.intBetween(0, 400),
 71	)
 72	platformToken := randomElementFromSliceString([]string{
 73		ua.linuxPlatformToken(),
 74		ua.macPlatformToken("_"),
 75		ua.windowsPlatformToken(),
 76	})
 77	safariType := randomElementFromSliceString([]string{"Safari", "Mobile Safari"})
 78	webKitVer := ua.webKitVersion(false)
 79	return fmt.Sprintf("Mozilla/5.0 (%s) AppleWebKit/%s (KHTML, like Gecko) Chrome/%s %s/%s",
 80		platformToken, webKitVer, chromeVer, safariType, webKitVer,
 81	)
 82}
 83
 84// firefox generates a user agent string that resembles Firefox.
 85// Example: "Mozilla/5.0 (X11; Linux x86_64; rv:58.0) Gecko/20130219 Firefox/58.0"
 86func (ua UserAgent) firefox() string {
 87	firefoxVer := fmt.Sprintf("%d.0", ua.intBetween(35, 129))
 88	platformToken := randomElementFromSliceString([]string{
 89		ua.linuxPlatformToken(),
 90		ua.macPlatformToken("."),
 91		ua.windowsPlatformToken(),
 92	})
 93	return fmt.Sprintf("Mozilla/5.0 (%s; rv:%s) Gecko/%s Firefox/%s",
 94		platformToken, firefoxVer, ua.geckoVersion(), firefoxVer,
 95	)
 96}
 97
 98// internetExplorer generates a user agent string that resembles Internet Explorer
 99// Example: "Mozilla/5.0 (compatible; MSIE 9.0; Windows 98; Win 9x 4.90; Trident/5.0)"
100func (ua UserAgent) internetExplorer() string {
101	return fmt.Sprintf("Mozilla/5.0 (compatible; MSIE %d.0; %s; Trident/%d.0)",
102		ua.intBetween(5, 11), ua.windowsPlatformToken(), ua.intBetween(3, 7),
103	)
104}
105
106// msEdge generates a user agent string that resembles MS Edge
107// Example: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/533.0 (KHTML, like Gecko) Chrome/86.0.4685.32 Safari/533.0 Edg/86.0.4685.32"
108func (ua UserAgent) msEdge() string {
109	webKitVer := ua.webKitVersion(false)
110	chromiumVer := fmt.Sprintf("%d.0.%d.%d", ua.intBetween(79, 99), ua.intBetween(4000, 4844), ua.intBetween(10, 99))
111	platformToken := randomElementFromSliceString([]string{
112		ua.linuxPlatformToken(),
113		ua.windowsPlatformToken(),
114		ua.macPlatformToken("_"),
115	})
116	return fmt.Sprintf("Mozilla/5.0 (%s) AppleWebKit/%s (KHTML, like Gecko) Chrome/%s Safari/%s Edg/%s",
117		platformToken, webKitVer, chromiumVer, webKitVer, chromiumVer,
118	)
119}
120
121// opera generates a user agent string that resembles Opera.
122// Example: "Opera/8.40 (X11; Linux i686; zh-TW) Presto/2.12.181 Version/10.57"
123func (ua UserAgent) opera() string {
124	platformToken := randomElementFromSliceString([]string{
125		ua.linuxPlatformToken(),
126		ua.windowsPlatformToken(),
127	})
128	operaVer := fmt.Sprintf("%d.%d", ua.intBetween(8, 9), ua.intBetween(10, 99))
129	prestoVer := fmt.Sprintf("2.%d.%d", ua.intBetween(8, 12), ua.intBetween(160, 355))
130	ver := fmt.Sprintf("%d.%d", ua.intBetween(10, 12), ua.intBetween(0, 70))
131	return fmt.Sprintf("Opera/%s (%s; %s) Presto/%s Version/%s",
132		operaVer, platformToken, randomElementFromSliceString(langCodes), prestoVer, ver,
133	)
134}
135
136// safari generates a user agent string that resembles Safari.
137// Example: "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_7_3; bg-BG) AppleWebKit/548.43.16 (KHTML, like Gecko) Version/7.5 Safari/548.43.16"
138func (ua UserAgent) safari() string {
139	webKitVer := ua.webKitVersion(true)
140	safariVer := fmt.Sprintf("%d.%d", ua.intBetween(4, 17), ua.intBetween(0, 5))
141	if ua.intBetween(0, 100) < 50 {
142		safariVer = fmt.Sprintf("%s.%d", safariVer, ua.intBetween(1, 20))
143	}
144	platform := randomElementFromSliceString([]string{
145		fmt.Sprintf("Windows; U; %s; %s", ua.windowsPlatformToken(), randomElementFromSliceString(langCodes)),
146		fmt.Sprintf("%s; %s", ua.macPlatformToken("_"), randomElementFromSliceString(langCodes)),
147	})
148
149	return fmt.Sprintf("Mozilla/5.0 (%s) AppleWebKit/%s (KHTML, like Gecko) Version/%s Safari/%s",
150		platform, webKitVer, safariVer, webKitVer,
151	)
152}
153
154func (ua UserAgent) linuxPlatformToken() string {
155	return fmt.Sprintf("X11; Linux %s", randomElementFromSliceString(linuxProcessor))
156}
157
158func (ua UserAgent) macPlatformToken(sep string) string {
159	return fmt.Sprintf("Macintosh; %s Mac OS X %d%s%d%s%d",
160		randomElementFromSliceString(macProcessor), ua.intBetween(10, 17), sep, ua.intBetween(5, 8), sep, ua.intBetween(0, 9),
161	)
162}
163
164func (ua UserAgent) windowsPlatformToken() string {
165	return randomElementFromSliceString(windowsPlatformTokens)
166}
167
168// webKitVersion generates a random string that resembles an AppleWebKit version string.
169func (ua UserAgent) webKitVersion(withPatchVer bool) string {
170	majorVer := ua.intBetween(531, 618)
171	if withPatchVer {
172		return fmt.Sprintf("%d.%d.%d", majorVer, ua.intBetween(1, 50), ua.intBetween(1, 20))
173	}
174	return fmt.Sprintf("%d.%d", majorVer, ua.intBetween(0, 10))
175}
176
177// geckoVersion generates a random string that resembles a Gecko version string.
178func (ua UserAgent) geckoVersion() string {
179	const YYYYMMDD = "20060102"
180	minDate := time.Date(2010, 1, 1, 0, 0, 0, 0, time.UTC)
181	maxDate := time.Now()
182	randDuration := time.Duration(rand.Int63n(maxDate.Unix()-minDate.Unix())) * time.Second
183	return minDate.Add(randDuration).Format(YYYYMMDD)
184}
185
186// intBetween generates a random integer in the range [min, max].
187//
188//nolint:revive
189func (ua UserAgent) intBetween(min, max int) int {
190	boundary := interfaces.RandomIntegerBoundary{Start: min, End: max + 1}
191	return randomIntegerWithBoundary(boundary)
192}