diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml new file mode 100644 index 00000000..73eda828 --- /dev/null +++ b/.github/workflows/continuous-integration.yml @@ -0,0 +1,200 @@ +name: Continuous Integration + +on: + schedule: + - cron: "0 * * * *" + workflow_dispatch: + inputs: + logLevel: + description: "Log level" + required: true + default: "info" + type: choice + options: + - info + - warning + - debug + +env: + TZ: Asia/Jakarta + EMOJI_CHEAT_SHEETS: "👻,😻,💕,🤍,💨,🧚,🧜‍♀️,🧞,💃,🐣,🐉,🦕,🦖,🐳,🐬,🦋,🌻,🌼,🌱,🌿,🍀,🍃,🍻,🛫,🪂,🚀,🌟,✨,⚡,🔥,🎉" + +jobs: + build: + name: Build and Testing + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: "go.mod" + + - name: Build + run: go build -v ./... + + - name: Test + run: go test -v ./... + + prepare: + name: Prepare Branch + runs-on: ubuntu-latest + needs: [build] + + steps: + - name: Checkout main branch + uses: actions/checkout@v4 + with: + ref: main + + - name: Remove ${{ vars.ARCHIVE_BRANCH_NAME }} branch if it exists + run: | + if git ls-remote --exit-code --heads origin ${{ vars.ARCHIVE_BRANCH_NAME }}; then + echo "Branch deleted successfully" + git push https://x-access-token:${{ secrets.WORKFLOW_GITHUB_TOKEN }}@github.com/${{ github.repository }} --delete ${{ vars.ARCHIVE_BRANCH_NAME }} + else + echo "Branch does not exist" + fi + + update: + name: Update Proxies + runs-on: ubuntu-latest + needs: [prepare] + permissions: + contents: write + + steps: + - name: Checkout main branch + uses: actions/checkout@v4 + with: + ref: main + + - name: Configure GIT + run: | + git config --global user.email $(git log --reverse --format='%ae' | head -n 1) + git config --global user.name $(git log --reverse --format='%an' | head -n 1) + + - name: Create ${{ vars.ARCHIVE_BRANCH_NAME }} branch + run: | + git checkout -b ${{ vars.ARCHIVE_BRANCH_NAME }} + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: "go.mod" + + - name: Install dependencies + run: | + go mod tidy + + - name: Run Go program + env: + PROXY_RESOURCES: ${{ secrets.PROXY_RESOURCES }} + run: | + go run ./cmd/main.go + + - name: Check for changes + run: | + if [ "$(git status --porcelain)" ]; then + echo "Changes detected" + echo "CHANGES_EXIST=true" >> $GITHUB_ENV + else + echo "No changes to commit" + echo "CHANGES_EXIST=false" >> $GITHUB_ENV + fi + + - name: Commit files + if: env.CHANGES_EXIST == 'true' + run: | + git add storage + git commit -m "chore(bot): update proxies at $(date '+%a, %d %b %Y %H:%M:%S (GMT+07:00)' | tr '[:upper:]' '[:lower:]') $(echo $EMOJI_CHEAT_SHEETS | tr ',' '\n' | shuf -n 1)" + + - name: Push changes to ${{ vars.ARCHIVE_BRANCH_NAME }} branch + if: env.CHANGES_EXIST == 'true' + uses: ad-m/github-push-action@master + with: + github_token: ${{ secrets.WORKFLOW_GITHUB_TOKEN }} + branch: ${{ vars.ARCHIVE_BRANCH_NAME }} + force: true + + release: + name: Release + runs-on: ubuntu-latest + needs: [update] + permissions: + contents: write + + steps: + - name: Checkout ${{ vars.ARCHIVE_BRANCH_NAME }} branch + uses: actions/checkout@v4 + with: + ref: ${{ vars.ARCHIVE_BRANCH_NAME }} + + - name: Configure GIT + run: | + git config --global user.email $(git log --reverse --format='%ae' | head -n 1) + git config --global user.name $(git log --reverse --format='%an' | head -n 1) + + - name: Get last commit time on ${{ vars.ARCHIVE_BRANCH_NAME }} branch + run: | + echo "UPDATED_AT=$(date -d "$(git log -1 --format=%cd --date=iso-strict)" "+%A, %B %e, %Y at %H:%M:%S (GMT+07:00)" | awk '{$1=$1; print}')" >> $GITHUB_ENV + + - name: Count proxies + run: | + echo "HTTP_PROXY_COUNT=$(grep -v '^$' ./storage/classic/http.txt | wc -l)" >> $GITHUB_ENV + echo "HTTPS_PROXY_COUNT=$(grep -v '^$' ./storage/classic/https.txt | wc -l)" >> $GITHUB_ENV + echo "SOCKS4_PROXY_COUNT=$(grep -v '^$' ./storage/classic/socks4.txt | wc -l)" >> $GITHUB_ENV + echo "SOCKS5_PROXY_COUNT=$(grep -v '^$' ./storage/classic/socks5.txt | wc -l)" >> $GITHUB_ENV + + - name: Extract 10 fresh proxies + run: | + echo "HTTP_PROXIES=$(shuf -n 10 ./storage/classic/http.txt | awk '{printf "%s\\n", $0}')" >> $GITHUB_ENV + echo "HTTPS_PROXIES=$(shuf -n 10 ./storage/classic/https.txt | awk '{printf "%s\\n", $0}')" >> $GITHUB_ENV + echo "SOCKS4_PROXIES=$(shuf -n 10 ./storage/classic/socks4.txt | awk '{printf "%s\\n", $0}')" >> $GITHUB_ENV + echo "SOCKS5_PROXIES=$(shuf -n 10 ./storage/classic/socks5.txt | awk '{printf "%s\\n", $0}')" >> $GITHUB_ENV + + - name: Checkout main branch + uses: actions/checkout@v4 + with: + ref: main + + - name: Update documentation + run: | + sed \ + -e "s/{{UPDATED_AT}}/${UPDATED_AT}/" \ + -e "s/{{HTTP_PROXY_COUNT}}/${HTTP_PROXY_COUNT}/" \ + -e "s/{{HTTPS_PROXY_COUNT}}/${HTTPS_PROXY_COUNT}/" \ + -e "s/{{SOCKS4_PROXY_COUNT}}/${SOCKS4_PROXY_COUNT}/" \ + -e "s/{{SOCKS5_PROXY_COUNT}}/${SOCKS5_PROXY_COUNT}/" \ + -e "s/{{HTTP_PROXIES}}/${HTTP_PROXIES}/" \ + -e "s/{{HTTPS_PROXIES}}/${HTTPS_PROXIES}/" \ + -e "s/{{SOCKS4_PROXIES}}/${SOCKS4_PROXIES}/" \ + -e "s/{{SOCKS5_PROXIES}}/${SOCKS5_PROXIES}/" \ + ./docs/README.template.md > ./README.md + + - name: Check for changes + run: | + if [ "$(git status --porcelain)" ]; then + echo "Changes detected" + echo "CHANGES_EXIST=true" >> $GITHUB_ENV + else + echo "No changes to commit" + echo "CHANGES_EXIST=false" >> $GITHUB_ENV + fi + + - name: Commit changes + if: env.CHANGES_EXIST == 'true' + run: | + git add README.md + git commit -m "docs: release fresh proxy list $(echo $EMOJI_CHEAT_SHEETS | tr ',' '\n' | shuf -n 1)" + + - name: Push changes + if: env.CHANGES_EXIST == 'true' + uses: ad-m/github-push-action@master + with: + github_token: ${{ secrets.WORKFLOW_GITHUB_TOKEN }} + branch: main + force: true diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml new file mode 100644 index 00000000..d2b51ecd --- /dev/null +++ b/.github/workflows/static-analysis.yml @@ -0,0 +1,25 @@ +name: Static Analysis + +on: + push: + pull_request: + +jobs: + build: + name: Build and Testing + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: "go.mod" + + - name: Build + run: go build -v ./... + + - name: Test + run: go test -v ./... diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..27512370 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/.idea +/.vscode +.env +*temp.* \ No newline at end of file diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 00000000..fcb20898 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,19 @@ +The MIT License (MIT) + +Copyright (c) Azis Alvriyanto + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software +and associated documentation files (the "Software"), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons +to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 00000000..61a859a0 --- /dev/null +++ b/README.md @@ -0,0 +1,142 @@ +[donate::shield]: https://img.shields.io/badge/Donate-Saweria-orange.svg +[donate::url]: https://saweria.co/azisalvriyanto +[contributors::shield]: https://img.shields.io/github/contributors/fyvri/fresh-proxy-list?style=flat +[contributors::url]: https://github.com/fyvri/fresh-proxy-list/graphs/contributors +[license::shield]: https://img.shields.io/badge/License-MIT-4b9081?style=flat +[license::url]: https://github.com/fyvri/fresh-proxy-list/blob/HEAD/LICENSE.md +[watchers::shield]: https://img.shields.io/github/watchers/fyvri/fresh-proxy-list?style=flat&logo=github&label=Watchers +[watchers::url]: https://github.com/fyvri/fresh-proxy-list/watchers +[stars::shield]: https://img.shields.io/github/stars/fyvri/fresh-proxy-list?style=flat&logo=github&label=Stars +[stars::url]: https://github.com/fyvri/fresh-proxy-list/stargazers +[forks::shield]: https://img.shields.io/github/forks/fyvri/fresh-proxy-list?style=flat&logo=github&label=Forks +[forks::url]: https://github.com/fyvri/fresh-proxy-list/network/members +[continuous-integration::shield]: https://github.com/fyvri/fresh-proxy-list/actions/workflows/continuous-integration.yml/badge.svg +[continuous-integration::url]: https://github.com/fyvri/fresh-proxy-list/actions/workflows/continuous-integration.yml +[static-analysis::shield]: https://github.com/fyvri/fresh-proxy-list/actions/workflows/static-analysis.yml/badge.svg +[static-analysis::url]: https://github.com/fyvri/fresh-proxy-list/actions/workflows/static-analysis.yml +[last-commit::shield]: https://img.shields.io/github/last-commit/fyvri/fresh-proxy-list?style=flat&logo=github&label=last+update +[last-commit::url]: https://github.com/fyvri/fresh-proxy-list/activity?ref=archive +[commit-activity::shield]: https://img.shields.io/github/commit-activity/w/fyvri/fresh-proxy-list?style=flat&logo=github +[commit-activity::url]: https://github.com/fyvri/fresh-proxy-list/commits/archive +[discussions::shield]: https://img.shields.io/github/discussions/fyvri/fresh-proxy-list?style=flat&logo=github +[discussions::url]: https://github.com/fyvri/fresh-proxy-list/discussions +[issues::shield]: https://img.shields.io/github/issues/fyvri/fresh-proxy-list?style=flat&logo=github +[issues::url]: https://github.com/fyvri/fresh-proxy-list/issues + +
+ +

Fresh Proxy List

+ +[![Donate][donate::shield]][donate::url] +[![License][license::shield]][license::url] +[![Static Analysis][static-analysis::shield]][static-analysis::url] +[![Continuous Integration][continuous-integration::shield]][continuous-integration::url] +
+[![Last Commit][last-commit::shield]][last-commit::url] +[![Commit Activity][commit-activity::shield]][commit-activity::url] +[![Discussions][discussions::shield]][discussions::url] +[![Issues][issues::shield]][issues::url] + +An automatically ⏰ updated list of free `HTTP`, `HTTPS`, `SOCKS4`, and `SOCKS5` proxies, available in multiple formats including `TXT`, `CSV`, `JSON`, `XML`, and `YAML`. The list is refreshed ⚡ **hourly** to provide the most accurate 🎯 and up-to-date information. The current data snapshot was 🚀 last updated on `Friday, December 6, 2024`, ensuring that users have access to the latest and most reliable proxies 🍃 available. + + + HTTP + +  + + HTTPS + +  + + SOCKS4 + +  + + SOCKS5 + + +
+ +## 📃 About + +This repository contains a free list of `HTTP/S` and `SOCKS4/5` proxies. + +- [x] 24/7 hourly updates (Committing since December 2024) +- [x] Supported list formats: `TXT`, `CSV`, `JSON`, `XML`, and `YAML` +- [x] No authentication is required when connecting any of these proxies + +> [!TIP] +> Be sure to read this documentation. + +

[ back to top ]

+ +## 🔗 Proxy List Links + +Duplicated proxies are removed — the only exception is if an IP has a different port open. + +1. Classic View (IP:Port only) + + | Category | Links by File Type | + | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | + | All | [`TXT`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/all.txt), [`CSV`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/all.csv), [`JSON`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/all.json), [`XML`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/all.xml), [`YAML`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/all.yaml) | + | HTTP | [`TXT`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/http.txt), [`CSV`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/http.csv), [`JSON`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/http.json), [`XML`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/http.xml), [`YAML`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/http.yaml) | + | HTTPS | [`TXT`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/https.txt), [`CSV`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/https.csv), [`JSON`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/https.json), [`XML`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/https.xml), [`YAML`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/https.yaml) | + | SOCKS4 | [`TXT`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/socks4.txt), [`CSV`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/socks4.csv), [`JSON`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/socks4.json), [`XML`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/socks4.xml), [`YAML`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/socks4.yaml) | + | SOCKS5 | [`TXT`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/socks5.txt), [`CSV`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/socks5.csv), [`JSON`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/socks5.json), [`XML`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/socks5.xml), [`YAML`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/socks5.yaml) | + +2. Advanced View (With full information) + + _Coming soon_ + +

[ back to top ]

+ +## 🎁 Example Results + +HTTP + +```txt + +``` + +HTTPS + +```txt + +``` + +SOCKS4 + +```txt + +``` + +SOCKS5 + +```txt + +``` + +

[ back to top ]

+ +## 👥 Contributing + +If you have any ideas, [open an issue](https://github.com/fyvri/fresh-proxy-list/issues/new) and tell me what you think. + +Contributions are what make the open-source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**. + +> [!IMPORTANT] +> If you have a suggestion that would make this better, please fork the repo and create a pull request. Don't forget to give the project a star 🌟! I can't stop saying thank you! +> +> 1. Fork this project +> 2. Create your feature branch (`git checkout -b feature/awesome-feature`) +> 3. Commit your changes (`git commit -m "feat: add awesome feature"`) +> 4. Push to the branch (`git push origin feature/awesome-feature`) +> 5. Open a pull request + +

[ back to top ]

+ +## 📜 License + +The MIT License (MIT). Please see [License File](LICENSE.md) for more information. + +

[ back to top ]

diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 00000000..99a759dd --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,127 @@ +package main + +import ( + "io" + "log" + "net/http" + "os" + "slices" + "sync" + "time" + + "github.com/fyvri/fresh-proxy-list/internal/entity" + "github.com/fyvri/fresh-proxy-list/internal/infrastructure/config" + "github.com/fyvri/fresh-proxy-list/internal/infrastructure/repository" + "github.com/fyvri/fresh-proxy-list/internal/service" + "github.com/fyvri/fresh-proxy-list/internal/usecase" + "github.com/fyvri/fresh-proxy-list/pkg/utils" + + "github.com/joho/godotenv" +) + +type Runners struct { + fetcherUtil utils.FetcherUtilInterface + urlParserUtil utils.URLParserUtilInterface + proxyService service.ProxyServiceInterface + sourceRepository repository.SourceRepositoryInterface + proxyRepository repository.ProxyRepositoryInterface + fileRepository repository.FileRepositoryInterface +} + +func main() { + if err := runApplication(); err != nil { + log.Fatalf("Application error: %v", err) + } +} + +func runApplication() error { + loadEnv() + + httpTestingSites := config.HTTPTestingSites + httpsTestingSites := config.HTTPSTestingSites + userAgents := config.UserAgents + + mkdirAll := func(path string, perm os.FileMode) error { + return os.MkdirAll(path, perm) + } + create := func(name string) (io.Writer, error) { + file, err := os.Create(name) + if err != nil { + return nil, err + } + return file, nil + } + + fetcherUtil := utils.NewFetcher(http.DefaultClient, http.NewRequest) + urlParserUtil := utils.NewURLParser() + csvWriterUtil := utils.NewCSVWriter() + proxyService := service.NewProxyService(fetcherUtil, urlParserUtil, httpTestingSites, httpsTestingSites, userAgents) + sourceRepository := repository.NewSourceRepository(os.Getenv("PROXY_RESOURCES")) + proxyRepository := repository.NewProxyRepository() + fileRepository := repository.NewFileRepository(mkdirAll, create, csvWriterUtil) + + runners := Runners{ + fetcherUtil: fetcherUtil, + urlParserUtil: urlParserUtil, + proxyService: proxyService, + sourceRepository: sourceRepository, + proxyRepository: proxyRepository, + fileRepository: fileRepository, + } + + return run(runners) +} + +func loadEnv() error { + return godotenv.Load() +} + +func run(runners Runners) error { + startTime := time.Now() + + sourceUsecase := usecase.NewSourceUsecase(runners.sourceRepository, runners.fetcherUtil) + sources, err := sourceUsecase.LoadSources() + if err != nil { + return err + } + + wg := sync.WaitGroup{} + proxyCategories := config.ProxyCategories + specialIPs := config.SpecialIPs + privateIPs := config.PrivateIPs + proxyUsecase := usecase.NewProxyUsecase(runners.proxyRepository, runners.proxyService, specialIPs, privateIPs) + for i, source := range sources { + if _, found := slices.BinarySearch(proxyCategories, source.Category); found { + wg.Add(1) + go func(source entity.Source) { + defer wg.Done() + + innerWG := sync.WaitGroup{} + proxies, err := sourceUsecase.ProcessSource(&source) + if err != nil { + return + } + + for _, proxy := range proxies { + innerWG.Add(1) + go func(source entity.Source, proxy string) { + defer innerWG.Done() + proxyUsecase.ProcessProxy(source.Category, proxy, source.IsChecked) + }(source, proxy) + } + innerWG.Wait() + }(source) + } else { + log.Printf("Index %v: proxy category not found", i) + } + } + wg.Wait() + + fileOutputExtensions := config.FileOutputExtensions + fileUsecase := usecase.NewFileUsecase(runners.fileRepository, runners.proxyRepository, fileOutputExtensions) + fileUsecase.SaveFiles() + + log.Printf("Number of proxies : %v", len(proxyUsecase.GetAllAdvancedView())) + log.Printf("Time-consuming process: %v", time.Since(startTime)) + return nil +} diff --git a/docs/README.template.md b/docs/README.template.md new file mode 100644 index 00000000..7b937dd0 --- /dev/null +++ b/docs/README.template.md @@ -0,0 +1,142 @@ +[donate::shield]: https://img.shields.io/badge/Donate-Saweria-orange.svg +[donate::url]: https://saweria.co/azisalvriyanto +[contributors::shield]: https://img.shields.io/github/contributors/fyvri/fresh-proxy-list?style=flat +[contributors::url]: https://github.com/fyvri/fresh-proxy-list/graphs/contributors +[license::shield]: https://img.shields.io/badge/License-MIT-4b9081?style=flat +[license::url]: https://github.com/fyvri/fresh-proxy-list/blob/HEAD/LICENSE.md +[watchers::shield]: https://img.shields.io/github/watchers/fyvri/fresh-proxy-list?style=flat&logo=github&label=Watchers +[watchers::url]: https://github.com/fyvri/fresh-proxy-list/watchers +[stars::shield]: https://img.shields.io/github/stars/fyvri/fresh-proxy-list?style=flat&logo=github&label=Stars +[stars::url]: https://github.com/fyvri/fresh-proxy-list/stargazers +[forks::shield]: https://img.shields.io/github/forks/fyvri/fresh-proxy-list?style=flat&logo=github&label=Forks +[forks::url]: https://github.com/fyvri/fresh-proxy-list/network/members +[continuous-integration::shield]: https://github.com/fyvri/fresh-proxy-list/actions/workflows/continuous-integration.yml/badge.svg +[continuous-integration::url]: https://github.com/fyvri/fresh-proxy-list/actions/workflows/continuous-integration.yml +[static-analysis::shield]: https://github.com/fyvri/fresh-proxy-list/actions/workflows/static-analysis.yml/badge.svg +[static-analysis::url]: https://github.com/fyvri/fresh-proxy-list/actions/workflows/static-analysis.yml +[last-commit::shield]: https://img.shields.io/github/last-commit/fyvri/fresh-proxy-list?style=flat&logo=github&label=last+update +[last-commit::url]: https://github.com/fyvri/fresh-proxy-list/activity?ref=archive +[commit-activity::shield]: https://img.shields.io/github/commit-activity/w/fyvri/fresh-proxy-list?style=flat&logo=github +[commit-activity::url]: https://github.com/fyvri/fresh-proxy-list/commits/archive +[discussions::shield]: https://img.shields.io/github/discussions/fyvri/fresh-proxy-list?style=flat&logo=github +[discussions::url]: https://github.com/fyvri/fresh-proxy-list/discussions +[issues::shield]: https://img.shields.io/github/issues/fyvri/fresh-proxy-list?style=flat&logo=github +[issues::url]: https://github.com/fyvri/fresh-proxy-list/issues + +
+ +

Fresh Proxy List

+ +[![Donate][donate::shield]][donate::url] +[![License][license::shield]][license::url] +[![Static Analysis][static-analysis::shield]][static-analysis::url] +[![Continuous Integration][continuous-integration::shield]][continuous-integration::url] +
+[![Last Commit][last-commit::shield]][last-commit::url] +[![Commit Activity][commit-activity::shield]][commit-activity::url] +[![Discussions][discussions::shield]][discussions::url] +[![Issues][issues::shield]][issues::url] + +An automatically ⏰ updated list of free `HTTP`, `HTTPS`, `SOCKS4`, and `SOCKS5` proxies, available in multiple formats including `TXT`, `CSV`, `JSON`, `XML`, and `YAML`. The list is refreshed ⚡ **hourly** to provide the most accurate 🎯 and up-to-date information. The current data snapshot was 🚀 last updated on `{{UPDATED_AT}}`, ensuring that users have access to the latest and most reliable proxies 🍃 available. + + + HTTP + +  + + HTTPS + +  + + SOCKS4 + +  + + SOCKS5 + + +
+ +## 📃 About + +This repository contains a free list of `HTTP/S` and `SOCKS4/5` proxies. + +- [x] 24/7 hourly updates (Committing since December 2024) +- [x] Supported list formats: `TXT`, `CSV`, `JSON`, `XML`, and `YAML` +- [x] No authentication is required when connecting any of these proxies + +> [!TIP] +> Be sure to read this documentation. + +

[ back to top ]

+ +## 🔗 Proxy List Links + +Duplicated proxies are removed — the only exception is if an IP has a different port open. + +1. Classic View (IP:Port only) + + | Category | Links by File Type | + | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | + | All | [`TXT`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/all.txt), [`CSV`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/all.csv), [`JSON`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/all.json), [`XML`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/all.xml), [`YAML`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/all.yaml) | + | HTTP | [`TXT`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/http.txt), [`CSV`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/http.csv), [`JSON`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/http.json), [`XML`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/http.xml), [`YAML`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/http.yaml) | + | HTTPS | [`TXT`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/https.txt), [`CSV`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/https.csv), [`JSON`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/https.json), [`XML`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/https.xml), [`YAML`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/https.yaml) | + | SOCKS4 | [`TXT`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/socks4.txt), [`CSV`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/socks4.csv), [`JSON`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/socks4.json), [`XML`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/socks4.xml), [`YAML`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/socks4.yaml) | + | SOCKS5 | [`TXT`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/socks5.txt), [`CSV`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/socks5.csv), [`JSON`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/socks5.json), [`XML`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/socks5.xml), [`YAML`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/socks5.yaml) | + +2. Advanced View (With full information) + + _Coming soon_ + +

[ back to top ]

+ +## 🎁 Example Results + +HTTP + +```txt +{{HTTP_PROXIES}} +``` + +HTTPS + +```txt +{{HTTPS_PROXIES}} +``` + +SOCKS4 + +```txt +{{SOCKS4_PROXIES}} +``` + +SOCKS5 + +```txt +{{SOCKS5_PROXIES}} +``` + +

[ back to top ]

+ +## 👥 Contributing + +If you have any ideas, [open an issue](https://github.com/fyvri/fresh-proxy-list/issues/new) and tell me what you think. + +Contributions are what make the open-source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**. + +> [!IMPORTANT] +> If you have a suggestion that would make this better, please fork the repo and create a pull request. Don't forget to give the project a star 🌟! I can't stop saying thank you! +> +> 1. Fork this project +> 2. Create your feature branch (`git checkout -b feature/awesome-feature`) +> 3. Commit your changes (`git commit -m "feat: add awesome feature"`) +> 4. Push to the branch (`git push origin feature/awesome-feature`) +> 5. Open a pull request + +

[ back to top ]

+ +## 📜 License + +The MIT License (MIT). Please see [License File](LICENSE.md) for more information. + +

[ back to top ]

diff --git a/env.example b/env.example new file mode 100644 index 00000000..1f2b0adb --- /dev/null +++ b/env.example @@ -0,0 +1 @@ +PROXY_RESOURCES=[{"method":"LIST","category":"HTTP","url":"","is_checked":true},{"method":"LIST","category":"HTTPS","url":"","is_checked":true},{"method":"LIST","category":"SOCKS4","url":"","is_checked":true},{"method":"LIST","category":"SOCKS5","url":"","is_checked":true},{"method":"SCRAP","category":"HTTP","url":"","is_checked":true},{"method":"SCRAP","category":"HTTPS","url":"","is_checked":true},{"method":"SCRAP","category":"SOCKS4","url":"","is_checked":true},{"method":"SCRAP","category":"SOCKS5","url":"","is_checked":true}] \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 00000000..99da4f07 --- /dev/null +++ b/go.mod @@ -0,0 +1,9 @@ +module github.com/fyvri/fresh-proxy-list + +go 1.22.5 + +require ( + github.com/joho/godotenv v1.5.1 + gopkg.in/yaml.v3 v3.0.1 + h12.io/socks v1.0.3 +) diff --git a/go.sum b/go.sum new file mode 100644 index 00000000..1bd955ee --- /dev/null +++ b/go.sum @@ -0,0 +1,12 @@ +github.com/h12w/go-socks5 v0.0.0-20200522160539-76189e178364 h1:5XxdakFhqd9dnXoAZy1Mb2R/DZ6D1e+0bGC/JhucGYI= +github.com/h12w/go-socks5 v0.0.0-20200522160539-76189e178364/go.mod h1:eDJQioIyy4Yn3MVivT7rv/39gAJTrA7lgmYr8EW950c= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 h1:JhzVVoYvbOACxoUmOs6V/G4D5nPVUW73rKvXxP4XUJc= +github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +h12.io/socks v1.0.3 h1:Ka3qaQewws4j4/eDQnOdpr4wXsC//dXtWvftlIcCQUo= +h12.io/socks v1.0.3/go.mod h1:AIhxy1jOId/XCz9BO+EIgNL2rQiPTBNnOfnVnQ+3Eck= diff --git a/internal/entity/proxy.go b/internal/entity/proxy.go new file mode 100644 index 00000000..ff0e043c --- /dev/null +++ b/internal/entity/proxy.go @@ -0,0 +1,19 @@ +package entity + +type Proxy struct { + Category string `json:"category" yaml:"category"` + Proxy string `json:"proxy" yaml:"proxy"` + IP string `json:"ip" yaml:"ip"` + Port string `json:"port" yaml:"port"` + TimeTaken float64 `json:"time_taken" yaml:"time_taken"` + CheckedAt string `json:"checked_at" yaml:"checked_at"` +} + +type AdvancedProxy struct { + Proxy string `json:"proxy" yaml:"proxy"` + IP string `json:"ip" yaml:"ip"` + Port string `json:"port" yaml:"port"` + TimeTaken float64 `json:"time_taken" yaml:"time_taken"` + CheckedAt string `json:"checked_at" yaml:"checked_at"` + Categories []string `json:"categories" yaml:"categories"` +} diff --git a/internal/entity/proxy_xml.go b/internal/entity/proxy_xml.go new file mode 100644 index 00000000..b8e444b2 --- /dev/null +++ b/internal/entity/proxy_xml.go @@ -0,0 +1,18 @@ +package entity + +import "encoding/xml" + +type ProxyXMLClassicView struct { + XMLName xml.Name `xml:"Proxies"` + Proxies []string `xml:"Proxy"` +} + +type ProxyXMLAdvancedView struct { + XMLName xml.Name `xml:"Proxies"` + Proxies []Proxy `xml:"Proxy"` +} + +type ProxyXMLAllAdvancedView struct { + XMLName xml.Name `xml:"Proxies"` + Proxies []AdvancedProxy `xml:"Proxy"` +} diff --git a/internal/entity/source.go b/internal/entity/source.go new file mode 100644 index 00000000..f9a26853 --- /dev/null +++ b/internal/entity/source.go @@ -0,0 +1,32 @@ +package entity + +import "encoding/json" + +type Source struct { + Method string `json:"method"` + Category string `json:"category"` + URL string `json:"url"` + IsChecked bool `json:"is_checked"` +} + +func (s *Source) UnmarshalJSON(data []byte) error { + type Alias Source + alias := &struct { + IsChecked *bool `json:"is_checked"` + *Alias + }{ + Alias: (*Alias)(s), + } + + if err := json.Unmarshal(data, &alias); err != nil { + return err + } + + if alias.IsChecked == nil { + s.IsChecked = true + } else { + s.IsChecked = *alias.IsChecked + } + + return nil +} diff --git a/internal/infrastructure/config/file_output_extensions.go b/internal/infrastructure/config/file_output_extensions.go new file mode 100644 index 00000000..d21f8dfa --- /dev/null +++ b/internal/infrastructure/config/file_output_extensions.go @@ -0,0 +1,8 @@ +package config + +var FileOutputExtensions = []string{ + "csv", + "json", + "xml", + "yaml", +} diff --git a/internal/infrastructure/config/private_ips.go b/internal/infrastructure/config/private_ips.go new file mode 100644 index 00000000..936deca8 --- /dev/null +++ b/internal/infrastructure/config/private_ips.go @@ -0,0 +1,16 @@ +package config + +import ( + "net" +) + +var PrivateIPs = []net.IPNet{ + {IP: net.IP{10, 0, 0, 0}, Mask: net.CIDRMask(8, 32)}, // Private range A + {IP: net.IP{172, 16, 0, 0}, Mask: net.CIDRMask(12, 32)}, // Private range B + {IP: net.IP{192, 168, 0, 0}, Mask: net.CIDRMask(16, 32)}, // Private range C + {IP: net.IP{169, 254, 0, 0}, Mask: net.CIDRMask(16, 32)}, // Link-local addresses + {IP: net.IP{224, 0, 0, 0}, Mask: net.CIDRMask(4, 32)}, // Multicast addresses + {IP: net.IP{240, 0, 0, 0}, Mask: net.CIDRMask(4, 32)}, // Reserved addresses + {IP: net.IP{127, 0, 0, 0}, Mask: net.CIDRMask(8, 32)}, // Loopback addresses + {IP: net.IP{192, 0, 2, 0}, Mask: net.CIDRMask(24, 32)}, // Documentation Network +} diff --git a/internal/infrastructure/config/proxy_categories.go b/internal/infrastructure/config/proxy_categories.go new file mode 100644 index 00000000..b81ff30a --- /dev/null +++ b/internal/infrastructure/config/proxy_categories.go @@ -0,0 +1,8 @@ +package config + +var ProxyCategories = []string{ + "HTTP", + "HTTPS", + "SOCKS4", + "SOCKS5", +} diff --git a/internal/infrastructure/config/special_ips.go b/internal/infrastructure/config/special_ips.go new file mode 100644 index 00000000..e0dad12a --- /dev/null +++ b/internal/infrastructure/config/special_ips.go @@ -0,0 +1,7 @@ +package config + +var SpecialIPs = []string{ + "0.0.0.0", + "127.0.0.1", + "255.255.255.255", +} diff --git a/internal/infrastructure/config/testing_sites.go b/internal/infrastructure/config/testing_sites.go new file mode 100644 index 00000000..50a1810d --- /dev/null +++ b/internal/infrastructure/config/testing_sites.go @@ -0,0 +1,18 @@ +package config + +var HTTPTestingSites = []string{ + "http://ifconfig.me/ip", + "http://api.ipaddress.com/myip", + "http://checkip.amazonaws.com", +} + +var HTTPSTestingSites = []string{ + "https://ifconfig.me/ip", + "https://api.ipaddress.com/myip", + "https://checkip.amazonaws.com", + "https://google.com", + "https://bing.com", + "https://yahoo.com", + "https://api.ipify.org", + "https://ipinfo.io/ip", +} diff --git a/internal/infrastructure/config/user_agents.go b/internal/infrastructure/config/user_agents.go new file mode 100644 index 00000000..39658a63 --- /dev/null +++ b/internal/infrastructure/config/user_agents.go @@ -0,0 +1,1003 @@ +package config + +var UserAgents = []string{ + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Ubuntu Chromium/37.0.2062.94 Chrome/37.0.2062.94 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.85 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko", + "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.0", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/600.8.9 (KHTML, like Gecko) Version/8.0.8 Safari/600.8.9", + "Mozilla/5.0 (iPad; CPU OS 8_4_1 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Version/8.0 Mobile/12H321 Safari/600.1.4", + "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.85 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.85 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/12.10240", + "Mozilla/5.0 (Windows NT 6.3; WOW64; rv:40.0) Gecko/20100101 Firefox/40.0", + "Mozilla/5.0 (Windows NT 6.3; WOW64; Trident/7.0; rv:11.0) like Gecko", + "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.85 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; Trident/7.0; rv:11.0) like Gecko", + "Mozilla/5.0 (Windows NT 10.0; WOW64; rv:40.0) Gecko/20100101 Firefox/40.0", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_4) AppleWebKit/600.7.12 (KHTML, like Gecko) Version/8.0.7 Safari/600.7.12", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.85 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:40.0) Gecko/20100101 Firefox/40.0", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/600.8.9 (KHTML, like Gecko) Version/7.1.8 Safari/537.85.17", + "Mozilla/5.0 (iPad; CPU OS 8_4 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Version/8.0 Mobile/12H143 Safari/600.1.4", + "Mozilla/5.0 (iPad; CPU OS 8_3 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Version/8.0 Mobile/12F69 Safari/600.1.4", + "Mozilla/5.0 (Windows NT 6.1; rv:40.0) Gecko/20100101 Firefox/40.0", + "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; WOW64; Trident/6.0)", + "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0)", + "Mozilla/5.0 (Windows NT 6.3; WOW64; Trident/7.0; Touch; rv:11.0) like Gecko", + "Mozilla/5.0 (Windows NT 5.1; rv:40.0) Gecko/20100101 Firefox/40.0", + "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.85 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3) AppleWebKit/600.6.3 (KHTML, like Gecko) Version/8.0.6 Safari/600.6.3", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3) AppleWebKit/600.5.17 (KHTML, like Gecko) Version/8.0.5 Safari/600.5.17", + "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:38.0) Gecko/20100101 Firefox/38.0", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.157 Safari/537.36", + "Mozilla/5.0 (iPhone; CPU iPhone OS 8_4_1 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Version/8.0 Mobile/12H321 Safari/600.1.4", + "Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; rv:11.0) like Gecko", + "Mozilla/5.0 (iPad; CPU OS 7_1_2 like Mac OS X) AppleWebKit/537.51.2 (KHTML, like Gecko) Version/7.0 Mobile/11D257 Safari/9537.53", + "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.85 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.85 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:40.0) Gecko/20100101 Firefox/40.0", + "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/6.0)", + "Mozilla/5.0 (Windows NT 6.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.85 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.157 Safari/537.36", + "Mozilla/5.0 (X11; CrOS x86_64 7077.134.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.156 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/600.7.12 (KHTML, like Gecko) Version/7.1.7 Safari/537.85.16", + "Mozilla/5.0 (Windows NT 6.0; rv:40.0) Gecko/20100101 Firefox/40.0", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:40.0) Gecko/20100101 Firefox/40.0", + "Mozilla/5.0 (iPad; CPU OS 8_1_3 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Version/8.0 Mobile/12B466 Safari/600.1.4", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_2) AppleWebKit/600.3.18 (KHTML, like Gecko) Version/8.0.3 Safari/600.3.18", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.85 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.85 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; Win64; x64; Trident/7.0; rv:11.0) like Gecko", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.157 Safari/537.36", + "Mozilla/5.0 (iPad; CPU OS 8_1_2 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Version/8.0 Mobile/12B440 Safari/600.1.4", + "Mozilla/5.0 (Linux; U; Android 4.0.3; en-us; KFTT Build/IML74K) AppleWebKit/537.36 (KHTML, like Gecko) Silk/3.68 like Chrome/39.0.2171.93 Safari/537.36", + "Mozilla/5.0 (iPad; CPU OS 8_2 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Version/8.0 Mobile/12D508 Safari/600.1.4", + "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:39.0) Gecko/20100101 Firefox/39.0", + "Mozilla/5.0 (iPad; CPU OS 7_1_1 like Mac OS X) AppleWebKit/537.51.2 (KHTML, like Gecko) Version/7.0 Mobile/11D201 Safari/9537.53", + "Mozilla/5.0 (Linux; U; Android 4.4.3; en-us; KFTHWI Build/KTU84M) AppleWebKit/537.36 (KHTML, like Gecko) Silk/3.68 like Chrome/39.0.2171.93 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/600.6.3 (KHTML, like Gecko) Version/7.1.6 Safari/537.85.15", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_2) AppleWebKit/600.4.10 (KHTML, like Gecko) Version/8.0.4 Safari/600.4.10", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.7; rv:40.0) Gecko/20100101 Firefox/40.0", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.78.2 (KHTML, like Gecko) Version/7.0.6 Safari/537.78.2", + "Mozilla/5.0 (iPad; CPU OS 8_4_1 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) CriOS/45.0.2454.68 Mobile/12H321 Safari/600.1.4", + "Mozilla/5.0 (Windows NT 6.3; Win64; x64; Trident/7.0; Touch; rv:11.0) like Gecko", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.85 Safari/537.36", + "Mozilla/5.0 (iPad; CPU OS 8_1 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Version/8.0 Mobile/12B410 Safari/600.1.4", + "Mozilla/5.0 (iPad; CPU OS 7_0_4 like Mac OS X) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11B554a Safari/9537.53", + "Mozilla/5.0 (Windows NT 6.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.85 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; Win64; x64; Trident/7.0; rv:11.0) like Gecko", + "Mozilla/5.0 (Windows NT 6.3; WOW64; Trident/7.0; TNJB; rv:11.0) like Gecko", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.63 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; ARM; Trident/7.0; Touch; rv:11.0) like Gecko", + "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.85 Safari/537.36", + "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:40.0) Gecko/20100101 Firefox/40.0", + "Mozilla/5.0 (Windows NT 6.3; WOW64; Trident/7.0; MDDCJS; rv:11.0) like Gecko", + "Mozilla/5.0 (Windows NT 6.0; WOW64; rv:40.0) Gecko/20100101 Firefox/40.0", + "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.157 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.2; WOW64; rv:40.0) Gecko/20100101 Firefox/40.0", + "Mozilla/5.0 (iPhone; CPU iPhone OS 8_4 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Version/8.0 Mobile/12H143 Safari/600.1.4", + "Mozilla/5.0 (Linux; U; Android 4.4.3; en-us; KFASWI Build/KTU84M) AppleWebKit/537.36 (KHTML, like Gecko) Silk/3.68 like Chrome/39.0.2171.93 Safari/537.36", + "Mozilla/5.0 (iPad; CPU OS 8_4_1 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) GSA/7.0.55539 Mobile/12H321 Safari/600.1.4", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.155 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.85 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.85 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; Touch; rv:11.0) like Gecko", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:40.0) Gecko/20100101 Firefox/40.0", + "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:31.0) Gecko/20100101 Firefox/31.0", + "Mozilla/5.0 (iPhone; CPU iPhone OS 8_3 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Version/8.0 Mobile/12F70 Safari/600.1.4", + "Mozilla/5.0 (Windows NT 6.3; WOW64; Trident/7.0; MATBJS; rv:11.0) like Gecko", + "Mozilla/5.0 (Linux; U; Android 4.0.4; en-us; KFJWI Build/IMM76D) AppleWebKit/537.36 (KHTML, like Gecko) Silk/3.68 like Chrome/39.0.2171.93 Safari/537.36", + "Mozilla/5.0 (iPad; CPU OS 7_1 like Mac OS X) AppleWebKit/537.51.2 (KHTML, like Gecko) Version/7.0 Mobile/11D167 Safari/9537.53", + "Mozilla/5.0 (X11; CrOS armv7l 7077.134.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.156 Safari/537.36", + "Mozilla/5.0 (X11; Linux x86_64; rv:34.0) Gecko/20100101 Firefox/34.0", + "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.1; WOW64; Trident/7.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E)", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10) AppleWebKit/600.1.25 (KHTML, like Gecko) Version/8.0 Safari/600.1.25", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/600.2.5 (KHTML, like Gecko) Version/8.0.2 Safari/600.2.5", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.134 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.85 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.63 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.157 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/600.1.25 (KHTML, like Gecko) Version/8.0 Safari/600.1.25", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:39.0) Gecko/20100101 Firefox/39.0", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11) AppleWebKit/601.1.56 (KHTML, like Gecko) Version/9.0 Safari/601.1.56", + "Mozilla/5.0 (Linux; U; Android 4.4.3; en-us; KFSOWI Build/KTU84M) AppleWebKit/537.36 (KHTML, like Gecko) Silk/3.68 like Chrome/39.0.2171.93 Safari/537.36", + "Mozilla/5.0 (iPad; CPU OS 5_1_1 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko) Version/5.1 Mobile/9B206 Safari/7534.48.3", + "Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.85 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.85 Safari/537.36", + "Mozilla/5.0 (iPad; CPU OS 8_1_1 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Version/8.0 Mobile/12B435 Safari/600.1.4", + "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.63 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.157 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/12.10240", + "Mozilla/5.0 (Windows NT 6.3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.85 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; WOW64; Trident/7.0; Touch; LCJB; rv:11.0) like Gecko", + "Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; MDDRJS; rv:11.0) like Gecko", + "Mozilla/5.0 (Linux; U; Android 4.4.3; en-us; KFAPWI Build/KTU84M) AppleWebKit/537.36 (KHTML, like Gecko) Silk/3.68 like Chrome/39.0.2171.93 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; Trident/7.0; Touch; rv:11.0) like Gecko", + "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.157 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; WOW64; Trident/7.0; LCJB; rv:11.0) like Gecko", + "Mozilla/5.0 (Linux; U; Android 4.0.3; en-us; KFOT Build/IML74K) AppleWebKit/537.36 (KHTML, like Gecko) Silk/3.68 like Chrome/39.0.2171.93 Safari/537.36", + "Mozilla/5.0 (iPad; CPU OS 6_1_3 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/6.0 Mobile/10B329 Safari/8536.25", + "Mozilla/5.0 (Linux; U; Android 4.4.3; en-us; KFARWI Build/KTU84M) AppleWebKit/537.36 (KHTML, like Gecko) Silk/3.68 like Chrome/39.0.2171.93 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; WOW64; Trident/7.0; ASU2JS; rv:11.0) like Gecko", + "Mozilla/5.0 (iPad; CPU OS 8_0_2 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Version/8.0 Mobile/12A405 Safari/600.1.4", + "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0)", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.157 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.85 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; rv:38.0) Gecko/20100101 Firefox/38.0", + "Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; yie11; rv:11.0) like Gecko", + "Mozilla/5.0 (Windows NT 6.3; WOW64; Trident/7.0; MALNJS; rv:11.0) like Gecko", + "Mozilla/5.0 (iPad; CPU OS 8_4_1 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) GSA/8.0.57838 Mobile/12H321 Safari/600.1.4", + "Mozilla/5.0 (Windows NT 6.3; WOW64; rv:39.0) Gecko/20100101 Firefox/39.0", + "Mozilla/5.0 (Windows NT 10.0; rv:40.0) Gecko/20100101 Firefox/40.0", + "Mozilla/5.0 (Windows NT 6.3; WOW64; Trident/7.0; MAGWJS; rv:11.0) like Gecko", + "Mozilla/5.0 (X11; Linux x86_64; rv:31.0) Gecko/20100101 Firefox/31.0", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/600.5.17 (KHTML, like Gecko) Version/7.1.5 Safari/537.85.14", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.152 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; WOW64; Trident/7.0; Touch; TNJB; rv:11.0) like Gecko", + "Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; NP06; rv:11.0) like Gecko", + "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.85 Safari/537.36", + "Mozilla/5.0 (X11; Ubuntu; Linux i686; rv:40.0) Gecko/20100101 Firefox/40.0", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.155 Safari/537.36 OPR/31.0.1889.174", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_2) AppleWebKit/600.4.8 (KHTML, like Gecko) Version/8.0.3 Safari/600.4.8", + "Mozilla/5.0 (iPad; CPU OS 7_0_6 like Mac OS X) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11B651 Safari/9537.53", + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.85 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/600.3.18 (KHTML, like Gecko) Version/7.1.3 Safari/537.85.12", + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko; Google Web Preview) Chrome/27.0.1453 Safari/537.36", + "Mozilla/5.0 (iPad; CPU OS 8_0 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Version/8.0 Mobile/12A365 Safari/600.1.4", + "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.155 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; rv:39.0) Gecko/20100101 Firefox/39.0", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.94 AOL/9.7 AOLBuild/4343.4049.US Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.85 Safari/537.36", + "Mozilla/5.0 (iPad; CPU OS 8_4 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) CriOS/45.0.2454.68 Mobile/12H143 Safari/600.1.4", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:38.0) Gecko/20100101 Firefox/38.0", + "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:37.0) Gecko/20100101 Firefox/37.0", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:39.0) Gecko/20100101 Firefox/39.0", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36", + "Mozilla/5.0 (iPad; CPU OS 8_4_1 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Mobile/12H321", + "Mozilla/5.0 (iPad; CPU OS 7_0_3 like Mac OS X) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11B511 Safari/9537.53", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/600.1.17 (KHTML, like Gecko) Version/7.1 Safari/537.85.10", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.130 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/600.2.5 (KHTML, like Gecko) Version/7.1.2 Safari/537.85.11", + "Mozilla/5.0 (Windows NT 6.3; WOW64; Trident/7.0; Touch; ASU2JS; rv:11.0) like Gecko", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36", + "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.0.1) Gecko/2008070208 Firefox/3.0.1", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.157 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:41.0) Gecko/20100101 Firefox/41.0", + "Mozilla/5.0 (Windows NT 6.3; WOW64; Trident/7.0; Touch; MDDCJS; rv:11.0) like Gecko", + "Mozilla/5.0 (Windows NT 6.3; rv:40.0) Gecko/20100101 Firefox/40.0", + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/534.34 (KHTML, like Gecko) Qt/4.8.5 Safari/534.34", + "Mozilla/5.0 (iPhone; CPU iPhone OS 7_0 like Mac OS X) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11A465 Safari/9537.53 BingPreview/1.0b", + "Mozilla/5.0 (X11; Linux x86_64; rv:38.0) Gecko/20100101 Firefox/38.0", + "Mozilla/5.0 (iPad; CPU OS 8_4 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) GSA/7.0.55539 Mobile/12H143 Safari/600.1.4", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.155 Safari/537.36", + "Mozilla/5.0 (X11; CrOS x86_64 7262.52.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.86 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.155 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; MDDCJS; rv:11.0) like Gecko", + "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.63 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; WOW64; rv:38.0) Gecko/20100101 Firefox/38.0", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/600.4.10 (KHTML, like Gecko) Version/7.1.4 Safari/537.85.13", + "Mozilla/5.0 (Unknown; Linux x86_64) AppleWebKit/538.1 (KHTML, like Gecko) PhantomJS/2.0.0 Safari/538.1", + "Mozilla/5.0 (Windows NT 6.3; WOW64; Trident/7.0; Touch; MALNJS; rv:11.0) like Gecko", + "Mozilla/5.0 (iPad; CPU OS 8_3 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) CriOS/45.0.2454.68 Mobile/12F69 Safari/600.1.4", + "Mozilla/5.0 (Android; Tablet; rv:40.0) Gecko/40.0 Firefox/40.0", + "Mozilla/5.0 (iPhone; CPU iPhone OS 7_1_2 like Mac OS X) AppleWebKit/537.51.2 (KHTML, like Gecko) Version/7.0 Mobile/11D257 Safari/9537.53", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10) AppleWebKit/600.2.5 (KHTML, like Gecko) Version/8.0.2 Safari/600.2.5", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.157 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_4) AppleWebKit/536.30.1 (KHTML, like Gecko) Version/6.0.5 Safari/536.30.1", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.125 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.157 Safari/537.36", + "Mozilla/5.0 (Linux; U; Android 4.4.3; en-us; KFSAWI Build/KTU84M) AppleWebKit/537.36 (KHTML, like Gecko) Silk/3.68 like Chrome/39.0.2171.93 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.104 AOL/9.8 AOLBuild/4346.13.US Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; MAAU; rv:11.0) like Gecko", + "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.152 Safari/537.36", + "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E)", + "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:35.0) Gecko/20100101 Firefox/35.0", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.132 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.90 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.74.9 (KHTML, like Gecko) Version/7.0.2 Safari/537.74.9", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.157 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.155 Safari/537.36", + "Mozilla/5.0 (iPad; CPU OS 7_0_2 like Mac OS X) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11A501 Safari/9537.53", + "Mozilla/5.0 (Windows NT 6.3; WOW64; Trident/7.0; Touch; MAARJS; rv:11.0) like Gecko", + "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36", + "Mozilla/5.0 (iPad; CPU OS 7_0 like Mac OS X) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11A465 Safari/9537.53", + "Mozilla/5.0 (Windows NT 10.0; Trident/7.0; rv:11.0) like Gecko", + "Mozilla/5.0 (iPad; CPU OS 8_3 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) GSA/7.0.55539 Mobile/12F69 Safari/600.1.4", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.78.2 (KHTML, like Gecko) Version/7.0.6 Safari/537.78.2", + "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:36.0) Gecko/20100101 Firefox/36.0", + "Mozilla/5.0 (Windows NT 6.3; WOW64; Trident/7.0; MASMJS; rv:11.0) like Gecko", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.85 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; WOW64; rv:39.0) Gecko/20100101 Firefox/39.0", + "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.155 Safari/537.36 OPR/31.0.1889.174", + "Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; FunWebProducts; rv:11.0) like Gecko", + "Mozilla/5.0 (Windows NT 6.3; WOW64; Trident/7.0; MAARJS; rv:11.0) like Gecko", + "Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; BOIE9;ENUS; rv:11.0) like Gecko", + "Mozilla/5.0 (Linux; Android 4.4.2; SM-T230NU Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.84 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; EIE10;ENUSWOL; rv:11.0) like Gecko", + "Mozilla/5.0 (Windows NT 5.1; rv:39.0) Gecko/20100101 Firefox/39.0", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:39.0) Gecko/20100101 Firefox/39.0", + "Mozilla/5.0 (Linux; U; Android 4.0.4; en-us; KFJWA Build/IMM76D) AppleWebKit/537.36 (KHTML, like Gecko) Silk/3.68 like Chrome/39.0.2171.93 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.155 Safari/537.36 OPR/31.0.1889.174", + "Mozilla/5.0 (Linux; Android 4.0.4; BNTV600 Build/IMM76L) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.111 Safari/537.36", + "Mozilla/5.0 (iPhone; CPU iPhone OS 8_1_2 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Version/8.0 Mobile/12B440 Safari/600.1.4", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.101 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.118 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; yie9; rv:11.0) like Gecko", + "Mozilla/5.0 (Linux; Android 5.0.2; SM-T530NU Build/LRX22G) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.84 Safari/537.36", + "Mozilla/5.0 (iPad; CPU OS 9_0 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13A4325c Safari/601.1", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.155 Safari/537.36", + "Mozilla/5.0 (iPhone; CPU iPhone OS 8_1_3 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Version/8.0 Mobile/12B466 Safari/600.1.4", + "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:34.0) Gecko/20100101 Firefox/34.0", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.130 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.89 Safari/537.36", + "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2; WOW64; Trident/7.0)", + "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:33.0) Gecko/20100101 Firefox/33.0", + "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:39.0) Gecko/20100101 Firefox/39.0", + "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.157 Safari/537.36", + "Mozilla/5.0 (iPhone; CPU iPhone OS 8_2 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Version/8.0 Mobile/12D508 Safari/600.1.4", + "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.157 Safari/537.36", + "Mozilla/5.0 (iPad; CPU OS 8_4_1 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) CriOS/44.0.2403.67 Mobile/12H321 Safari/600.1.4", + "Mozilla/5.0 (Windows NT 6.2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.85 Safari/537.36", + "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.2; WOW64; Trident/7.0; .NET4.0E; .NET4.0C)", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.85 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.71 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.134 Safari/537.36", + "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.152 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.81 Safari/537.36", + "Mozilla/5.0 (PlayStation 4 2.57) AppleWebKit/537.73 (KHTML, like Gecko)", + "Mozilla/5.0 (Windows NT 6.1; rv:31.0) Gecko/20100101 Firefox/31.0", + "Mozilla/5.0 (Linux; Android 5.0; SM-G900V Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.84 Mobile Safari/537.36", + "Mozilla/5.0 (X11; Linux x86_64; rv:40.0) Gecko/20100101 Firefox/40.0", + "Mozilla/5.0 (Linux; Android 5.1.1; Nexus 7 Build/LMY48I) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.84 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.111 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; Win64; x64; Trident/7.0; Touch; LCJB; rv:11.0) like Gecko", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:38.0) Gecko/20100101 Firefox/38.0", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.124 Safari/537.36", + "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2; WOW64; Trident/6.0; Touch)", + "Mozilla/5.0 (Linux; Android 5.0.2; SM-T800 Build/LRX22G) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.84 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.111 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; WOW64; Trident/7.0; Touch; MASMJS; rv:11.0) like Gecko", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.155 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.152 Safari/537.36", + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.90 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.7; rv:39.0) Gecko/20100101 Firefox/39.0", + "Mozilla/5.0 (Windows NT 6.3; Win64; x64; Trident/7.0; TNJB; rv:11.0) like Gecko", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_3) AppleWebKit/537.75.14 (KHTML, like Gecko) Version/7.0.3 Safari/537.75.14", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.89 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.157 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; WOW64; Trident/7.0; ASJB; rv:11.0) like Gecko", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.153 Safari/537.36", + "Mozilla/5.0 (Linux; Android 5.0.1; SAMSUNG SCH-I545 4G Build/LRX22C) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/2.1 Chrome/34.0.1847.76 Mobile Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.115 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.155 Safari/537.36 OPR/31.0.1889.174", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.114 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; EIE10;ENUSMSN; rv:11.0) like Gecko", + "Mozilla/5.0 (iPhone; CPU iPhone OS 8_4_1 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) CriOS/45.0.2454.68 Mobile/12H321 Safari/600.1.4", + "Mozilla/5.0 (Windows NT 6.3; Win64; x64; Trident/7.0; MATBJS; rv:11.0) like Gecko", + "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:30.0) Gecko/20100101 Firefox/30.0", + "Mozilla/5.0 (Windows NT 6.3; WOW64; Trident/7.0; Touch; MASAJS; rv:11.0) like Gecko", + "Mozilla/5.0 (Windows NT 6.1; rv:41.0) Gecko/20100101 Firefox/41.0", + "Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; MALC; rv:11.0) like Gecko", + "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.94 AOL/9.7 AOLBuild/4343.4049.US Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; WOW64; rv:41.0) Gecko/20100101 Firefox/41.0", + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/33.0.0.0 Safari/534.24", + "Mozilla/5.0 (Windows NT 6.3; Win64; x64; Trident/7.0; MDDCJS; rv:11.0) like Gecko", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.153 Safari/537.36 SE 2.X MetaSr 1.0", + "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET4.0C; .NET4.0E)", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.120 Safari/537.36", + "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET4.0C; .NET4.0E)", + "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.134 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:39.0) Gecko/20100101 Firefox/39.0", + "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.155 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; yie10; rv:11.0) like Gecko", + "Mozilla/5.0 (Linux; Android 5.0; SAMSUNG-SM-G900A Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/2.1 Chrome/34.0.1847.76 Mobile Safari/537.36", + "Mozilla/5.0 (Linux; U; Android 4.0.3; en-gb; KFTT Build/IML74K) AppleWebKit/537.36 (KHTML, like Gecko) Silk/3.68 like Chrome/39.0.2171.93 Safari/537.36", + "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2; WOW64; Trident/8.0)", + "Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; TNJB; rv:11.0) like Gecko", + "Mozilla/5.0 (X11; CrOS x86_64 7077.111.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.125 Safari/537.36", + "Mozilla/5.0 (Linux; Android 4.0.4; BNTV400 Build/IMM76L) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.111 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; rv:37.0) Gecko/20100101 Firefox/37.0", + "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.130 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.152 Safari/537.36 LBBROWSER", + "Mozilla/5.0 (Windows NT 6.3; WOW64; rv:41.0) Gecko/20100101 Firefox/41.0", + "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1547.76 Safari/537.36", + "Mozilla/5.0 (Linux; Android 5.0; SAMSUNG SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/2.1 Chrome/34.0.1847.76 Mobile Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.125 Safari/537.36", + "Mozilla/5.0 (Windows NT 5.1; rv:31.0) Gecko/20100101 Firefox/31.0", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.104 AOL/9.8 AOLBuild/4346.18.US Safari/537.36", + "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.1; WOW64; Trident/7.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E; InfoPath.3; GWX:QUALIFIED)", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.107 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; MDDCJS; rv:11.0) like Gecko", + "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.104 AOL/9.8 AOLBuild/4346.13.US Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.94 AOL/9.7 AOLBuild/4343.4043.US Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:23.0) Gecko/20100101 Firefox/23.0", + "Mozilla/5.0 (Windows NT 5.1; rv:38.0) Gecko/20100101 Firefox/38.0", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.13 Safari/537.36", + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Ubuntu Chromium/44.0.2403.89 Chrome/44.0.2403.89 Safari/537.36", + "Mozilla/5.0 (iPad; CPU OS 6_0_1 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/6.0 Mobile/10A523 Safari/8536.25", + "Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; MANM; rv:11.0) like Gecko", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Maxthon/4.4.6.2000 Chrome/30.0.1599.101 Safari/537.36", + "Mozilla/5.0 (iPad; CPU OS 8_4 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) GSA/8.0.57838 Mobile/12H143 Safari/600.1.4", + "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:32.0) Gecko/20100101 Firefox/32.0", + "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; WOW64; Trident/6.0; MDDRJS)", + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.157 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.22 Safari/537.36", + "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:38.0) Gecko/20100101 Firefox/38.0", + "Mozilla/5.0 (Windows NT 6.3; WOW64; Trident/7.0; Touch; MATBJS; rv:11.0) like Gecko", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.93 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.157 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:37.0) Gecko/20100101 Firefox/37.0", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.104 AOL/9.8 AOLBuild/4346.13.US Safari/537.36", + "Mozilla/5.0 (Windows NT 5.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.0", + "Mozilla/5.0 (X11; Linux x86_64; U; en-us) AppleWebKit/537.36 (KHTML, like Gecko) Silk/3.68 like Chrome/39.0.2171.93 Safari/537.36", + "Mozilla/5.0 (X11; CrOS x86_64 6946.86.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.134 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.130 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.91 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; TNJB; rv:11.0) like Gecko", + "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.152 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; Trident/7.0; MDDRJS; rv:11.0) like Gecko", + "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.157 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.104 Safari/537.36", + "Mozilla/5.0 (iPad; CPU OS 8_3 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) GSA/8.0.57838 Mobile/12F69 Safari/600.1.4", + "Mozilla/5.0 (iPhone; CPU iPhone OS 7_1_1 like Mac OS X) AppleWebKit/537.51.2 (KHTML, like Gecko) Version/7.0 Mobile/11D201 Safari/9537.53", + "Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; GIL 3.5; rv:11.0) like Gecko", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:41.0) Gecko/20100101 Firefox/41.0", + "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LG-V410/V41010d Build/KOT49I.V41010d) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/30.0.1599.103 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.75.14 (KHTML, like Gecko) Version/7.0.3 Safari/537.75.14", + "Mozilla/5.0 (iPhone; CPU iPhone OS 8_1 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Version/8.0 Mobile/12B411 Safari/600.1.4", + "Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; MATBJS; rv:11.0) like Gecko", + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.134 Safari/537.36", + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/534.34 (KHTML, like Gecko) Qt/4.8.1 Safari/534.34", + "Mozilla/5.0 (Windows NT 6.3; WOW64; Trident/7.0; Touch; USPortal; rv:11.0) like Gecko", + "Mozilla/5.0 (iPad; CPU OS 8_4 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Mobile/12H143", + "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:40.0) Gecko/20100101 Firefox/40.0.2 Waterfox/40.0.2", + "Mozilla/5.0 (Windows NT 6.3; WOW64; Trident/7.0; SMJB; rv:11.0) like Gecko", + "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.1; WOW64; Trident/7.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; CMDTDF; .NET4.0C; .NET4.0E)", + "Mozilla/5.0 (iPad; CPU OS 6_1_2 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/6.0 Mobile/10B146 Safari/8536.25", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.130 Safari/537.36", + "Mozilla/5.0 (MSIE 9.0; Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.124 Safari/537.36", + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.84 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; Win64; x64; Trident/7.0; Touch; TNJB; rv:11.0) like Gecko", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.122 Safari/537.36 SE 2.X MetaSr 1.0", + "Mozilla/5.0 (X11; FC Linux i686; rv:24.0) Gecko/20100101 Firefox/24.0", + "Mozilla/5.0 (X11; CrOS armv7l 7262.52.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.86 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; WOW64; Trident/7.0; MASAJS; rv:11.0) like Gecko", + "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.1; WOW64; Trident/7.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; MS-RTC LM 8; .NET4.0C; .NET4.0E)", + "Mozilla/5.0 (Windows NT 6.1; Trident/7.0; yie11; rv:11.0) like Gecko", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/12.10532", + "Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; BOIE9;ENUSMSE; rv:11.0) like Gecko", + "Mozilla/5.0 (Windows NT 6.2; WOW64; rv:29.0) Gecko/20100101 Firefox/29.0", + "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET4.0C; .NET4.0E; InfoPath.3)", + "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:29.0) Gecko/20100101 Firefox/29.0", + "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E; InfoPath.3)", + "Mozilla/5.0 (Linux; Android 4.4.2; SM-T320 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.84 Safari/537.36", + "Mozilla/5.0 (iPad; CPU OS 8_4 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) CriOS/44.0.2403.67 Mobile/12H143 Safari/600.1.4", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.143 Safari/537.36", + "Mozilla/5.0 (iPhone; CPU iPhone OS 8_4_1 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) GSA/7.0.55539 Mobile/12H321 Safari/600.1.4", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.130 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.155 Safari/537.36", + "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET4.0C; .NET4.0E; 360SE)", + "Mozilla/5.0 (Linux; Android 5.0.2; LG-V410/V41020c Build/LRX22G) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/34.0.1847.118 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.81 Safari/537.36", + "Mozilla/5.0 (iPad; CPU OS 7_1_2 like Mac OS X) AppleWebKit/537.51.2 (KHTML, like Gecko) GSA/7.0.55539 Mobile/11D257 Safari/9537.53", + "Mozilla/5.0 (iPad; CPU OS 8_3 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Mobile/12F69", + "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.13 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.90 Safari/537.36", + "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; WOW64; Trident/6.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET4.0C; .NET4.0E)", + "Mozilla/5.0 (Linux; U; Android 4.4.3; en-us; KFTHWA Build/KTU84M) AppleWebKit/537.36 (KHTML, like Gecko) Silk/3.68 like Chrome/39.0.2171.93 Safari/537.36", + "Mozilla/5.0 (Android; Mobile; rv:40.0) Gecko/40.0 Firefox/40.0", + "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.153 Safari/537.36 SE 2.X MetaSr 1.0", + "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.94 AOL/9.7 AOLBuild/4343.4043.US Safari/537.36", + "Mozilla/5.0 (Linux; Android 4.4.2; SM-P600 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.84 Safari/537.36", + "Mozilla/5.0 (X11; Linux x86_64; rv:39.0) Gecko/20100101 Firefox/39.0", + "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.99 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; rv:35.0) Gecko/20100101 Firefox/35.0", + "Mozilla/5.0 (iPad; CPU OS 6_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/6.0 Mobile/10A5355d Safari/8536.25", + "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.22 Safari/537.36", + "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET4.0C; .NET4.0E; 360SE)", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.85 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; Touch; LCJB; rv:11.0) like Gecko", + "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.125 Safari/537.36", + "Mozilla/5.0 (X11; CrOS x86_64 6812.88.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.153 Safari/537.36", + "Mozilla/5.0 (X11; Linux i686; rv:38.0) Gecko/20100101 Firefox/38.0", + "Mozilla/5.0 (Windows NT 6.3; Win64; x64; Trident/7.0; Touch; ASU2JS; rv:11.0) like Gecko", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.65 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.154 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.13 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.157 Safari/537.36", + "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.1; WOW64; Trident/6.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E)", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10) AppleWebKit/537.16 (KHTML, like Gecko) Version/8.0 Safari/537.16", + "Mozilla/5.0 (Windows NT 6.1; rv:34.0) Gecko/20100101 Firefox/34.0", + "Mozilla/5.0 (Linux; Android 5.0; SAMSUNG SM-N900V 4G Build/LRX21V) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/2.1 Chrome/34.0.1847.76 Mobile Safari/537.36", + "Mozilla/5.0 (Linux; Android 4.4.3; KFTHWI Build/KTU84M) AppleWebKit/537.36 (KHTML, like Gecko) Silk/44.1.81 like Chrome/44.0.2403.128 Safari/537.36", + "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.1; WOW64; Trident/7.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; CMDTDF; .NET4.0C; .NET4.0E; GWX:QUALIFIED)", + "Mozilla/5.0 (iPad; CPU OS 7_1_2 like Mac OS X) AppleWebKit/537.51.2 (KHTML, like Gecko) CriOS/45.0.2454.68 Mobile/11D257 Safari/9537.53", + "Mozilla/5.0 (Windows NT 6.3; WOW64; rv:37.0) Gecko/20100101 Firefox/37.0", + "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Maxthon/4.4.6.1000 Chrome/30.0.1599.101 Safari/537.36", + "Mozilla/5.0 (Linux; Android 4.4.2; GT-P5210 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.84 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.99 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.155 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; WOW64; rv:38.0) Gecko/20100101 Firefox/38.0", + "Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; MDDSJS; rv:11.0) like Gecko", + "Mozilla/5.0 (Linux; Android 4.4.2; QTAQZ3 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.84 Safari/537.36", + "Mozilla/5.0 (Linux; Android 4.4.2; QMV7B Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.84 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.130 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; MATBJS; rv:11.0) like Gecko", + "Mozilla/5.0 (iPad; CPU OS 8_4_1 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) GSA/6.0.51363 Mobile/12H321 Safari/600.1.4", + "Mozilla/5.0 (iPad; CPU OS 8_1_1 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Version/8.0 Mobile/12B436 Safari/600.1.4", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.116 Safari/537.36", + "Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN) AppleWebKit/530.19.2 (KHTML, like Gecko) Version/4.0.2 Safari/530.19.1", + "Mozilla/5.0 (iPhone; CPU iPhone OS 8_4_1 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Mobile/12H321", + "Mozilla/5.0 (Linux; U; Android 4.0.3; en-ca; KFTT Build/IML74K) AppleWebKit/537.36 (KHTML, like Gecko) Silk/3.68 like Chrome/39.0.2171.93 Safari/537.36", + "Mozilla/5.0 (Windows NT 5.1; rv:30.0) Gecko/20100101 Firefox/30.0", + "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.130 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:40.0) Gecko/20100101 Firefox/40.0.2 Waterfox/40.0.2", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:38.0) Gecko/20100101 Firefox/38.0", + "Mozilla/5.0 (Windows NT 6.3; Win64; x64; Trident/7.0; LCJB; rv:11.0) like Gecko", + "Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; NISSC; rv:11.0) like Gecko", + "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.111 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.118 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.71 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9) AppleWebKit/537.71 (KHTML, like Gecko) Version/7.0 Safari/537.71", + "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.125 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; Trident/7.0; MALC; rv:11.0) like Gecko", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.155 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.132 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.0.9895 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; MSBrowserIE; rv:11.0) like Gecko", + "Mozilla/5.0 (Linux; Android 5.0.1; SAMSUNG SM-N910V 4G Build/LRX22C) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/2.1 Chrome/34.0.1847.76 Mobile Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1547.76 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.2; rv:40.0) Gecko/20100101 Firefox/40.0", + "Mozilla/5.0 (Linux; Android 5.0.2; SAMSUNG SM-T530NU Build/LRX22G) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/3.2 Chrome/38.0.2125.102 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.89 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.65 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.124 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; LCJB; rv:11.0) like Gecko", + "Mozilla/5.0 (Windows NT 6.0; rv:39.0) Gecko/20100101 Firefox/39.0", + "Mozilla/5.0 (Linux; Android 5.0.2; SM-T700 Build/LRX22G) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.84 Safari/537.36", + "Mozilla/5.0 (Linux; Android 5.0.1; SAMSUNG-SM-N910A Build/LRX22C) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/2.1 Chrome/34.0.1847.76 Mobile Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; ASU2JS; rv:11.0) like Gecko", + "Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:40.0) Gecko/20100101 Firefox/40.0", + "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:28.0) Gecko/20100101 Firefox/28.0", + "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:29.0) Gecko/20120101 Firefox/29.0", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.155 Safari/537.36", + "Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN; rv:1.9.0.8) Gecko/2009032609 Firefox/3.0.8 (.NET CLR 3.5.30729)", + "Mozilla/5.0 (X11; CrOS x86_64 7077.95.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.90 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Maxthon/4.4.6.1000 Chrome/30.0.1599.101 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.130 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.152 Safari/537.36 LBBROWSER", + "Mozilla/5.0 (Windows NT 6.1; rv:36.0) Gecko/20100101 Firefox/36.0", + "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.130 Safari/537.36", + "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/7.0)", + "Mozilla/5.0 (iPad; CPU OS 8_1_3 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) CriOS/45.0.2454.68 Mobile/12B466 Safari/600.1.4", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.125 Safari/537.36", + "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.2; Win64; x64; Trident/6.0; .NET4.0E; .NET4.0C; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET CLR 2.0.50727)", + "Mozilla/5.0 (Linux; Android 5.0.2; VK810 4G Build/LRX22G) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.84 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_3) AppleWebKit/537.76.4 (KHTML, like Gecko) Version/7.0.4 Safari/537.76.4", + "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.132 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.11; rv:40.0) Gecko/20100101 Firefox/40.0", + "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.125 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; WOW64; Trident/7.0; Touch; SMJB; rv:11.0) like Gecko", + "Mozilla/5.0 (Windows NT 6.3; Win64; x64; Trident/7.0; Touch; MDDCJS; rv:11.0) like Gecko", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.131 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; Trident/7.0; BOIE9;ENUS; rv:11.0) like Gecko", + "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.153 Safari/537.36 SE 2.X MetaSr 1.0", + "Mozilla/5.0 (iPad; CPU OS 8_4 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) GSA/6.0.51363 Mobile/12H143 Safari/600.1.4", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:38.0) Gecko/20100101 Firefox/38.0", + "Mozilla/5.0 (Windows NT 5.1; rv:41.0) Gecko/20100101 Firefox/41.0", + "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.1; WOW64; Trident/6.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E; InfoPath.3)", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.76 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2503.0 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11) AppleWebKit/601.1.50 (KHTML, like Gecko) Version/9.0 Safari/601.1.50", + "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.1; WOW64; Trident/7.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E; InfoPath.3; GWX:RESERVED)", + "Mozilla/5.0 (iPad; CPU OS 6_1 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/6.0 Mobile/10B141 Safari/8536.25", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/601.1.56 (KHTML, like Gecko) Version/9.0 Safari/601.1.56", + "Mozilla/5.0 (Linux; Android 5.1.1; Nexus 7 Build/LMY47V) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.84 Safari/537.36", + "Mozilla/5.0 (iPad; CPU OS 8_1_2 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) CriOS/45.0.2454.68 Mobile/12B440 Safari/600.1.4", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.125 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/534+ (KHTML, like Gecko) MsnBot-Media /1.0b", + "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; WOW64; Trident/7.0)", + "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.153 Safari/537.36", + "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.3; WOW64; Trident/7.0)", + "Mozilla/5.0 (Linux; Android 5.1.1; SM-G920V Build/LMY47X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.84 Mobile Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; Win64; x64; Trident/7.0; ASU2JS; rv:11.0) like Gecko", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.85 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.94 AOL/9.7 AOLBuild/4343.4049.US Safari/537.36", + "Mozilla/5.0 (X11; CrOS x86_64 6680.78.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.102 Safari/537.36", + "Mozilla/5.0 (Linux; Android 4.4.2; SM-T520 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.84 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.59 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Maxthon/4.4.6.2000 Chrome/30.0.1599.101 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.134 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.111 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; Win64; x64; Trident/7.0; Touch; MAARJS; rv:11.0) like Gecko", + "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.125 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; MALNJS; rv:11.0) like Gecko", + "Mozilla/5.0 (Linux; Android 4.4.2; SM-T900 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.84 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.134 Safari/537.36", + "Mozilla/5.0 (Windows; U; MSIE 9.0; Windows NT 9.0; en-US)", + "Mozilla/5.0 (Windows NT 6.2; WOW64; rv:39.0) Gecko/20100101 Firefox/39.0", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.94 Safari/537.36", + "Mozilla/5.0 (iPad; CPU OS 8_2 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) CriOS/45.0.2454.68 Mobile/12D508 Safari/600.1.4", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:36.0) Gecko/20100101 Firefox/36.0", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2503.0 Safari/537.36", + "Mozilla/5.0 (Linux; Android 4.1.2; GT-N8013 Build/JZO54K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.84 Safari/537.36", + "Mozilla/5.0 (Linux; U; Android 4.4.3; en-us; KFAPWA Build/KTU84M) AppleWebKit/537.36 (KHTML, like Gecko) Silk/3.68 like Chrome/39.0.2171.93 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.134 Safari/537.36", + "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.1; WOW64; Trident/5.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E; InfoPath.3)", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.7; rv:38.0) Gecko/20100101 Firefox/38.0", + "Mozilla/5.0 (Windows NT 6.3; WOW64; Trident/7.0; MALCJS; rv:11.0) like Gecko", + "Mozilla/5.0 (Windows NT 6.1; rv:30.0) Gecko/20100101 Firefox/30.0", + "Mozilla/5.0 (Linux; Android 5.0.1; SM-N910V Build/LRX22C) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.84 Mobile Safari/537.36", + "Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1667.0 Safari/537.36", + "Mozilla/5.0 (iPhone; CPU iPhone OS 8_1_1 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Version/8.0 Mobile/12B436 Safari/600.1.4", + "Mozilla/5.0 (iPad; CPU OS 8_1_3 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) GSA/7.0.55539 Mobile/12B466 Safari/600.1.4", + "Mozilla/5.0 (iPhone; CPU iPhone OS 8_0_2 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Version/8.0 Mobile/12A405 Safari/600.1.4", + "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.59 Safari/537.36", + "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.1; WOW64; Trident/5.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E)", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:37.0) Gecko/20100101 Firefox/37.0", + "Mozilla/5.0 (Linux; Android 4.4.2; SM-T310 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.84 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.45 Safari/537.36", + "Mozilla/5.0 (Linux; Android 5.1.1; Nexus 10 Build/LMY48I) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.84 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.115 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; Touch; TNJB; rv:11.0) like Gecko", + "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.107 Safari/537.36", + "Mozilla/5.0 (X11; CrOS x86_64 7077.123.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.155 Safari/537.36", + "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET4.0C; .NET4.0E)", + "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; WOW64; Trident/6.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET4.0C; .NET4.0E; 360SE)", + "Mozilla/5.0 (Linux; Android 4.4.2; QMV7A Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.84 Safari/537.36", + "Mozilla/5.0 (iPhone; CPU iPhone OS 7_0_4 like Mac OS X) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11B554a Safari/9537.53", + "Mozilla/5.0 (Windows NT 6.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.157 Safari/537.36", + "Mozilla/5.0 (Linux; Android 5.0; SAMSUNG-SM-N900A Build/LRX21V) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/2.1 Chrome/34.0.1847.76 Mobile Safari/537.36", + "Mozilla/5.0 (Linux; Android 4.4.4; XT1080 Build/SU6-7.2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.84 Mobile Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; MAARJS; rv:11.0) like Gecko", + "Mozilla/5.0 (iPad; CPU OS 8_3 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) GSA/6.0.51363 Mobile/12F69 Safari/600.1.4", + "Mozilla/5.0 (Windows NT 6.3; Win64; x64; Trident/7.0; MALNJS; rv:11.0) like Gecko", + "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Maxthon/4.4.6.2000 Chrome/30.0.1599.101 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; WOW64; Trident/7.0; Touch; ASJB; rv:11.0) like Gecko", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_1) AppleWebKit/537.73.11 (KHTML, like Gecko) Version/7.0.1 Safari/537.73.11", + "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2; WOW64; Trident/7.0; TNJB; 1ButtonTaskbar)", + "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.125 Safari/537.36", + "Mozilla/5.0 (Windows Phone 8.1; ARM; Trident/7.0; Touch; rv:11.0; IEMobile/11.0; NOKIA; Lumia 635) like Gecko", + "Mozilla/5.0 (iPad; CPU OS 5_0_1 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko) Version/5.1 Mobile/9A405 Safari/7534.48.3", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:35.0) Gecko/20100101 Firefox/35.0", + "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.101 Safari/537.36", + "Mozilla/5.0 (Linux; Android 5.1.1; SAMSUNG SM-N910P Build/LMY47X) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/2.1 Chrome/34.0.1847.76 Mobile Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; rv:33.0) Gecko/20100101 Firefox/33.0", + "Mozilla/5.0 (iPad; CPU OS 8_4_1 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Mobile/12H321 [Pinterest/iOS]", + "Mozilla/5.0 (Linux; Android 5.0.1; LGLK430 Build/LRX21Y) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/38.0.2125.102 Safari/537.36", + "Mozilla/5.0 (iPad; CPU OS 8_4_1 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Mobile/12H321 Safari", + "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2; WOW64; Trident/8.0; 1ButtonTaskbar)", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.134 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; NP08; NP08; MAAU; rv:11.0) like Gecko", + "Mozilla/5.0 (Windows NT 5.1; rv:37.0) Gecko/20100101 Firefox/37.0", + "Mozilla/5.0 (Linux; Android 4.4.2; SM-T217S Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.84 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; EIE10;ENUSMSE; rv:11.0) like Gecko", + "Mozilla/5.0 (Windows NT 6.2; WOW64; rv:33.0) Gecko/20100101 Firefox/33.0", + "Mozilla/5.0 (Windows NT 5.1; rv:35.0) Gecko/20100101 Firefox/35.0", + "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.111 Safari/537.36", + "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:37.0) Gecko/20100101 Firefox/37.0", + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.76 Safari/537.36", + "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.152 Safari/537.36 LBBROWSER", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.157 Safari/537.36", + "Mozilla/5.0 (Linux; Android 5.1; XT1254 Build/SU3TL-39) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.84 Mobile Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.13 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.2; Win64; x64; Trident/7.0; rv:11.0) like Gecko", + "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.124 Safari/537.36", + "Mozilla/5.0 (iPad; CPU OS 8_1_2 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) GSA/7.0.55539 Mobile/12B440 Safari/600.1.4", + "Mozilla/5.0 (MSIE 10.0; Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko", + "Mozilla/5.0 (iPad; CPU OS 8_3 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) CriOS/44.0.2403.67 Mobile/12F69 Safari/600.1.4", + "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.155 Safari/537.36 OPR/31.0.1889.174", + "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.124 Safari/537.36", + "Mozilla/5.0 (Linux; Android 5.0.1; SAMSUNG-SGH-I337 Build/LRX22C) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/2.1 Chrome/34.0.1847.76 Mobile Safari/537.36", + "Mozilla/5.0 (Linux; Android 4.4.3; KFASWI Build/KTU84M) AppleWebKit/537.36 (KHTML, like Gecko) Silk/44.1.81 like Chrome/44.0.2403.128 Safari/537.36", + "Mozilla/5.0 (X11; CrOS armv7l 7077.111.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.125 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.67 Safari/537.36", + "Mozilla/5.0 (iPad; CPU OS 6_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/6.0 Mobile/10A403 Safari/8536.25", + "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.114 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; WOW64; rv:36.0) Gecko/20100101 Firefox/36.0", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.134 Safari/537.36", + "Mozilla/5.0 (Linux; Android 5.0.2; SAMSUNG SM-T800 Build/LRX22G) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/3.0 Chrome/38.0.2125.102 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.125 Safari/537.36", + "Mozilla/5.0 (Linux; Android 5.0; SM-G900V Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.133 Mobile Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; MAGWJS; rv:11.0) like Gecko", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.122 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; Win64; x64; Trident/7.0; Touch; MALNJS; rv:11.0) like Gecko", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.130 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64; Trident/7.0; rv:11.0) like Gecko", + "Mozilla/5.0 (Windows NT 6.1; Trident/7.0; ATT-IE11; rv:11.0) like Gecko", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.130 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.103 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.155 Safari/537.36 OPR/31.0.1889.174", + "Mozilla/5.0 (X11; Linux i686) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.85 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.153 Safari/537.36 SE 2.X MetaSr 1.0", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7) AppleWebKit/534.48.3 (KHTML, like Gecko) Version/5.1 Safari/534.48.3", + "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.2; WOW64; Trident/7.0; .NET4.0E; .NET4.0C; .NET CLR 3.5.30729; .NET CLR 2.0.50727; .NET CLR 3.0.30729)", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.13 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.114 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; rv:32.0) Gecko/20100101 Firefox/32.0", + "Mozilla/5.0 (iPhone; CPU iPhone OS 8_4_1 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) GSA/8.0.57838 Mobile/12H321 Safari/600.1.4", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.155 Safari/537.36", + "Mozilla/5.0 (iPad; CPU OS 8_2 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) GSA/7.0.55539 Mobile/12D508 Safari/600.1.4", + "Mozilla/5.0 (iPhone; CPU iPhone OS 7_1 like Mac OS X) AppleWebKit/537.51.2 (KHTML, like Gecko) Version/7.0 Mobile/11D167 Safari/9537.53", + "Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0; MSN 9.0;MSN 9.1;MSN 9.6;MSN 10.0;MSN 10.2;MSN 10.5;MSN 11;MSN 11.5; MSNbMSNI; MSNmen-us; MSNcOTH) like Gecko", + "Mozilla/5.0 (Windows NT 5.1; rv:36.0) Gecko/20100101 Firefox/36.0", + "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.0.9895 Safari/537.36", + "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2; WOW64; Trident/7.0; 1ButtonTaskbar)", + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.102 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.124 YaBrowser/15.7.2357.2877 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:27.0) Gecko/20100101 Firefox/27.0", + "Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; BOIE9;ENUSMSNIP; rv:11.0) like Gecko", + "Mozilla/5.0 AppleWebKit/999.0 (KHTML, like Gecko) Chrome/99.0 Safari/999.0", + "Mozilla/5.0 (X11; OpenBSD amd64; rv:28.0) Gecko/20100101 Firefox/28.0", + "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.1; Trident/6.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E)", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/538.1 (KHTML, like Gecko) PhantomJS/2.0.0 Safari/538.1", + "Mozilla/5.0 (Windows NT 6.3; Win64; x64; Trident/7.0; MAGWJS; rv:11.0) like Gecko", + "Mozilla/5.0 (Linux; Android 4.4.2; GT-N5110 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.84 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.71 Safari/537.36", + "Mozilla/5.0 (iPad; CPU OS 8_1 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) CriOS/45.0.2454.68 Mobile/12B410 Safari/600.1.4", + "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:25.7) Gecko/20150824 Firefox/31.9 PaleMoon/25.7.0", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:31.0) Gecko/20100101 Firefox/31.0", + "Mozilla/5.0 (X11; Ubuntu; Linux i686; rv:39.0) Gecko/20100101 Firefox/39.0", + "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.63 Safari/537.36", + "Mozilla/5.0 (iPhone; CPU iPhone OS 9_0 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13A4325c Safari/601.1", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.107 Safari/537.36", + "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET4.0C; .NET4.0E; MS-RTC LM 8; InfoPath.3)", + "Mozilla/5.0 (Linux; Android 4.4.2; RCT6203W46 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/30.0.0.0 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.101 Safari/537.36", + "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.155 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:31.0) Gecko/20100101 Firefox/31.0", + "Mozilla/5.0 (Windows NT 6.3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.157 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.122 Safari/537.36", + "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E; Tablet PC 2.0)", + "Mozilla/5.0 (Windows NT 6.1; Trident/7.0; EIE10;ENUSWOL; rv:11.0) like Gecko", + "Mozilla/5.0 (Linux; Android 4.4.4; en-us; SAMSUNG SM-N910T Build/KTU84P) AppleWebKit/537.36 (KHTML, like Gecko) Version/2.0 Chrome/34.0.1847.76 Mobile Safari/537.36", + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36", + "Mozilla/5.0 (Linux; Android 4.4.2; RCT6203W46 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.84 Safari/537.36", + "Mozilla/5.0 (Linux; U; Android 4.0.4; en-ca; KFJWI Build/IMM76D) AppleWebKit/537.36 (KHTML, like Gecko) Silk/3.68 like Chrome/39.0.2171.93 Safari/537.36", + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Ubuntu Chromium/34.0.1847.116 Chrome/34.0.1847.116 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.153 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.22 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.137 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.45 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/30.0.1599.101 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; rv:27.0) Gecko/20100101 Firefox/27.0", + "Mozilla/5.0 (Linux; Android 4.4.2; RCT6773W22 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.84 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; ASJB; ASJB; MAAU; rv:11.0) like Gecko", + "Mozilla/5.0 (iPad; U; CPU OS 3_2 like Mac OS X; en-us) AppleWebKit/531.21.10 (KHTML, like Gecko) Version/4.0.4 Mobile/7B367 Safari/531.21.10", + "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:25.7) Gecko/20150824 Firefox/31.9 PaleMoon/25.7.0", + "Mozilla/5.0 (Linux; Android 5.0; SAMSUNG-SM-G870A Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/2.1 Chrome/34.0.1847.76 Mobile Safari/537.36", + "Mozilla/5.0 (Linux; Android 4.4.3; KFSOWI Build/KTU84M) AppleWebKit/537.36 (KHTML, like Gecko) Silk/44.1.81 like Chrome/44.0.2403.128 Safari/537.36", + "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E; InfoPath.2)", + "Mozilla/5.0 (Windows NT 5.2; rv:40.0) Gecko/20100101 Firefox/40.0", + "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.0.9895 Safari/537.36", + "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.94 AOL/9.7 AOLBuild/4343.4049.US Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; EIE10;ENUSMCM; rv:11.0) like Gecko", + "Mozilla/5.0 (Linux; Android 5.1.1; SAMSUNG SM-G920P Build/LMY47X) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/3.2 Chrome/38.0.2125.102 Mobile Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.107 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.93 Safari/537.36", + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.155 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/600.8.9 (KHTML, like Gecko)", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:35.0) Gecko/20100101 Firefox/35.0", + "Mozilla/5.0 (Windows NT 6.3; WOW64; Trident/7.0; Touch; MALCJS; rv:11.0) like Gecko", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.85 Safari/537.36", + "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.134 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.130 Safari/537.36", + "Mozilla/5.0 (Windows NT 5.2; rv:29.0) Gecko/20100101 Firefox/29.0 /29.0", + "Mozilla/5.0 (Linux; Android 5.0.2; SM-T550 Build/LRX22G) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.84 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.94 AOL/9.7 AOLBuild/4343.4049.US Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.122 Safari/537.36 SE 2.X MetaSr 1.0", + "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET4.0C; .NET4.0E)", + "Mozilla/5.0 (Linux; U; Android 4.0.3; en-gb; KFOT Build/IML74K) AppleWebKit/537.36 (KHTML, like Gecko) Silk/3.68 like Chrome/39.0.2171.93 Safari/537.36", + "Mozilla/5.0 (Linux; Android 5.0.2; SM-P900 Build/LRX22G) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.84 Safari/537.36", + "Mozilla/5.0 (Linux; Android 5.1.1; Nexus 9 Build/LMY48I) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.84 Safari/537.36", + "Mozilla/5.0 (Linux; Android 4.4.2; SM-T530NU Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.84 Safari/537.36", + "Mozilla/5.0 (X11; Linux i686; rv:40.0) Gecko/20100101 Firefox/40.0", + "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.143 Safari/537.36", + "Mozilla/5.0 (Linux; Android 5.1.1; SM-T330NU Build/LMY47X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.84 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Maxthon/4.4.7.1000 Chrome/30.0.1599.101 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:34.0) Gecko/20100101 Firefox/34.0", + "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.124 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; WOW64; rv:35.0) Gecko/20100101 Firefox/35.0", + "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.104 Safari/537.36", + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36", + "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:35.0) Gecko/20100101 Firefox/35.0", + "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.22 Safari/537.36", + "Mozilla/5.0 (Windows; U; Windows NT 6.1; zh-CN) AppleWebKit/530.19.2 (KHTML, like Gecko) Version/4.0.2 Safari/530.19.1", + "Mozilla/5.0 (Android; Tablet; rv:34.0) Gecko/34.0 Firefox/34.0", + "Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; MALCJS; rv:11.0) like Gecko", + "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E)", + "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET4.0C; .NET4.0E)", + "Mozilla/5.0 (iPad; CPU OS 7_1_2 like Mac OS X) AppleWebKit/537.51.2 (KHTML, like Gecko) GSA/8.0.57838 Mobile/11D257 Safari/9537.53", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.146 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; WOW64; Trident/7.0; yie10; rv:11.0) like Gecko", + "Mozilla/5.0 (Linux; Ubuntu 14.04) AppleWebKit/537.36 Chromium/35.0.1870.2 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; yie11; rv:11.0) like Gecko", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.63 Safari/537.36 SE 2.X MetaSr 1.0", + "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.118 Safari/537.36", + "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.122 Safari/537.36 SE 2.X MetaSr 1.0", + "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2; WOW64; Trident/8.0; TNJB; 1ButtonTaskbar)", + "Mozilla/5.0 (Linux; Android 4.4.2; RCT6773W22 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/30.0.0.0 Safari/537.36", + "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET4.0C; .NET4.0E)", + "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2503.0 Safari/537.36", + "Mozilla/5.0 (Linux; Android 5.0; SAMSUNG-SM-G900A Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.84 Mobile Safari/537.36", + "Mozilla/5.0 (Windows; U; Windows NT 6.1; zh-CN; rv:1.9.0.8) Gecko/2009032609 Firefox/3.0.8 (.NET CLR 3.5.30729)", + "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E)", + "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.65 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Maxthon/4.4.7.1000 Chrome/30.0.1599.101 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; NP08; rv:11.0) like Gecko", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.130 Safari/537.36", + "Mozilla/5.0 (Linux; Android 4.4.2; SM-T210R Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.84 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; Win64; x64; rv:40.0) Gecko/20100101 Firefox/40.0.2 Waterfox/40.0.2", + "Mozilla/5.0 (Linux; Android 5.0; SAMSUNG SM-N900P Build/LRX21V) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/2.1 Chrome/34.0.1847.76 Mobile Safari/537.36", + "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.104 AOL/9.8 AOLBuild/4346.18.US Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.22 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.130 Safari/537.36", + "Mozilla/5.0 (Linux; Android 5.0.2; SM-T350 Build/LRX22G) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.84 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; Touch; ASU2JS; rv:11.0) like Gecko", + "Mozilla/5.0 (Linux; Android 5.0.2; SM-T530NU Build/LRX22G) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.133 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.154 Safari/537.36", + "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; WOW64; Trident/7.0; 1ButtonTaskbar)", + "Mozilla/5.0 (Linux; Android 5.0.2; SAMSUNG-SM-G920A Build/LRX22G) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/3.0 Chrome/38.0.2125.102 Mobile Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2503.0 Safari/537.36", + "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET4.0C; .NET4.0E; 360SE)", + "Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; MAAU; MAAU; rv:11.0) like Gecko", + "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.132 Safari/537.36", + "Mozilla/5.0 (X11; Linux x86_64; rv:38.0) Gecko/20100101 Firefox/38.0 Iceweasel/38.2.1", + "Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; MANM; MANM; rv:11.0) like Gecko", + "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.90 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:37.0) Gecko/20100101 Firefox/37.0", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/534+ (KHTML, like Gecko) BingPreview/1.0b", + "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.93 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.81 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.94 AOL/9.7 AOLBuild/4343.4049.US Safari/537.36", + "Mozilla/5.0 (X11; Ubuntu; Linux i686; rv:38.0) Gecko/20100101 Firefox/38.0", + "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.104 Safari/537.36", + "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.107 Safari/537.36", + "Mozilla/5.0 (Linux; Android 4.4.2; QTAQZ3 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.135 Safari/537.36", + "Mozilla/5.0 (iPad; CPU OS 8_4_1 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Mobile/12H321 OverDrive Media Console/3.3.1", + "Mozilla/5.0 (iPad; CPU OS 7_1_2 like Mac OS X) AppleWebKit/537.51.2 (KHTML, like Gecko) Mobile/11D257", + "Mozilla/5.0 (iPad; CPU OS 7_1_1 like Mac OS X) AppleWebKit/537.51.2 (KHTML, like Gecko) GSA/7.0.55539 Mobile/11D201 Safari/9537.53", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.130 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.130 Safari/537.36", + "Mozilla/5.0 (Linux; Android 5.0.1; SCH-I545 Build/LRX22C) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.84 Mobile Safari/537.36", + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.130 Safari/537.36", + "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.84 Mobile Safari/537.36", + "Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Version/8.0 Mobile/12A365 Safari/600.1.4", + "Mozilla/5.0 (Windows NT 5.1; rv:34.0) Gecko/20100101 Firefox/34.0", + "Mozilla/5.0 (Windows NT 6.3; WOW64; rv:31.0) Gecko/20100101 Firefox/31.0", + "Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; Touch; MDDCJS; rv:11.0) like Gecko", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.132 Safari/537.36", + "Mozilla/5.0 (iPad;U;CPU OS 5_1_1 like Mac OS X; zh-cn)AppleWebKit/534.46.0(KHTML, like Gecko)CriOS/19.0.1084.60 Mobile/9B206 Safari/7534.48.3", + "Mozilla/5.0 (Linux; Android 4.4.3; KFAPWI Build/KTU84M) AppleWebKit/537.36 (KHTML, like Gecko) Silk/44.1.81 like Chrome/44.0.2403.128 Safari/537.36", + "Mozilla/5.0 (iPad; CPU OS 7_1_1 like Mac OS X) AppleWebKit/537.51.2 (KHTML, like Gecko) CriOS/45.0.2454.68 Mobile/11D201 Safari/9537.53", + "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.155 Safari/537.36", + "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.118 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.124 Safari/537.36", + "Mozilla/5.0 (iPad; CPU OS 8_4_1 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) CriOS/43.0.2357.61 Mobile/12H321 Safari/600.1.4", + "Mozilla/5.0 (Windows NT 6.3; WOW64; Trident/7.0; MAMIJS; rv:11.0) like Gecko", + "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.120 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.120 Safari/537.36", + "Mozilla/5.0 (Linux; Android 5.0.1; VS985 4G Build/LRX21Y) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.84 Mobile Safari/537.36", + "Mozilla/5.0 (Windows NT 5.1; rv:33.0) Gecko/20100101 Firefox/33.0", + "Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.85 Safari/537.36", + "Mozilla/5.0 (iPhone; CPU iPhone OS 8_4 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) CriOS/45.0.2454.68 Mobile/12H143 Safari/600.1.4", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.0; WOW64; rv:39.0) Gecko/20100101 Firefox/39.0", + "Mozilla/5.0 (Linux; Android 5.0.2; LG-V410/V41020b Build/LRX22G) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/34.0.1847.118 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2503.0 Safari/537.36", + "Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:38.0) Gecko/20100101 Firefox/38.0", + "Mozilla/5.0 (iPhone; CPU iPhone OS 8_1_1 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Version/8.0 Mobile/12B435 Safari/600.1.4", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.130 Safari/537.36", + "Mozilla/5.0 (X11; Linux x86_64; rv:28.0) Gecko/20100101 Firefox/28.0", + "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.157 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:36.0) Gecko/20100101 Firefox/36.0", + "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; InfoPath.3; .NET4.0C; .NET4.0E)", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.85 Safari/537.36", + "Mozilla/5.0 (Windows NT 5.2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.85 Safari/537.36", + "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.115 Safari/537.36", + "Mozilla/5.0 (Windows NT 5.2; WOW64; rv:40.0) Gecko/20100101 Firefox/40.0", + "Mozilla/5.0 (Windows NT 6.3; WOW64; Trident/7.0; MDDRJS; rv:11.0) like Gecko", + "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Maxthon/4.4.6.2000 Chrome/30.0.1599.101 Safari/537.36", + "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.3; WOW64; Trident/6.0)", + "Mozilla/5.0 (Linux; Android 5.1.1; SAMSUNG SM-G920T Build/LMY47X) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/3.2 Chrome/38.0.2125.102 Mobile Safari/537.36", + "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E; InfoPath.3; MS-RTC LM 8)", + "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2503.0 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.91 Safari/537.36", + "Mozilla/5.0 (Linux; Android 4.4.3; KFTHWI Build/KTU84M) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/34.0.0.0 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.85 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36", + "Mozilla/5.0 (Linux; Android 4.4.3; KFSAWI Build/KTU84M) AppleWebKit/537.36 (KHTML, like Gecko) Silk/44.1.81 like Chrome/44.0.2403.128 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.132 Safari/537.36", + "Mozilla/5.0 (Windows NT 5.1; rv:32.0) Gecko/20100101 Firefox/32.0", + "Mozilla/5.0 (Linux; Android 4.4.2; SM-T230NU Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.133 Safari/537.36", + "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36", + "Mozilla/5.0 (Linux; Android 4.2.2; SM-T110 Build/JDQ39) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.84 Safari/537.36", + "Mozilla/5.0 (Linux; Android 5.0.1; SAMSUNG SM-N910T Build/LRX22C) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/2.1 Chrome/34.0.1847.76 Mobile Safari/537.36", + "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.1; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET4.0C; .NET4.0E)", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.125 Safari/537.36", + "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Win64; x64; Trident/7.0)", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:33.0) Gecko/20100101 Firefox/33.0", + "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.154 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.99 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.2; WOW64; rv:38.0) Gecko/20100101 Firefox/38.0", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.89 Safari/537.36", + "Mozilla/5.0 (X11; CrOS armv7l 6946.86.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.134 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.94 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:38.0) Gecko/20100101 Firefox/38.0 SeaMonkey/2.35", + "Mozilla/5.0 (Linux; Android 4.4.2; SM-T330NU Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.84 Safari/537.36", + "Mozilla/5.0 (iPad; CPU OS 6_0_1 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/6.0 Mobile/10A8426 Safari/8536.25", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.130 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.157 Safari/537.36", + "Mozilla/5.0 (Linux; Android 5.0.2; LG-V410 Build/LRX22G) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.84 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.63 Safari/537.36 TheWorld 6", + "Mozilla/5.0 (iPad; CPU OS 8_1 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) GSA/7.0.55539 Mobile/12B410 Safari/600.1.4", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.132 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.107 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/600.2.5 (KHTML, like Gecko) Version/8.0 Safari/600.1.25", + "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; WOW64; Trident/6.0; EIE10;ENUSWOL)", + "Mozilla/5.0 (iPad; CPU OS 8_4 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) CriOS/43.0.2357.61 Mobile/12H143 Safari/600.1.4", + "Mozilla/5.0 (iPad; CPU OS 8_3 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) CriOS/43.0.2357.61 Mobile/12F69 Safari/600.1.4", + "Mozilla/5.0 (Linux; Android 4.4.2; SM-T237P Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.84 Safari/537.36", + "Mozilla/5.0 (X11; Linux i686) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.152 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; Trident/7.0; ATT; rv:11.0) like Gecko", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.90 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.157 Safari/537.36", + "Mozilla/5.0 (Linux; Android 5.0.2; SM-T800 Build/LRX22G) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.133 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; Trident/7.0; EIE10;ENUSMSN; rv:11.0) like Gecko", + "Mozilla/5.0 (Windows NT 6.3; Win64; x64; Trident/7.0; Touch; MATBJS; rv:11.0) like Gecko", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.107 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.125 Safari/537.36", + "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/30.0.1599.103 Mobile Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.81 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.101 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; EIE11;ENUSMSN; rv:11.0) like Gecko", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.125 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Maxthon/4.4.6.1000 Chrome/30.0.1599.101 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; rv:29.0) Gecko/20100101 Firefox/29.0", + "Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/30.0.1599.114 Safari/537.36 Puffin/4.5.0IT", + "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.131 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; yie8; rv:11.0) like Gecko", + "Mozilla/5.0 (Linux; U; Android 4.4.3; en-gb; KFTHWI Build/KTU84M) AppleWebKit/537.36 (KHTML, like Gecko) Silk/3.68 like Chrome/39.0.2171.93 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.155 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; Trident/7.0; FunWebProducts; rv:11.0) like Gecko", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2505.0 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; Touch; MALNJS; rv:11.0) like Gecko", + "Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; BOIE9;ENUSSEM; rv:11.0) like Gecko", + "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2; Win64; x64; Trident/6.0; Touch; WebView/1.0)", + "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.130 Safari/537.36", + "Mozilla/5.0 (iPad; CPU OS 5_1 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko) Version/5.1 Mobile/9B176 Safari/7534.48.3", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.157 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:33.0) Gecko/20100101 Firefox/33.0", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.85 Safari/537.36", + "Mozilla/5.0 (Linux; Android 5.0.1; SAMSUNG SPH-L720 Build/LRX22C) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/2.1 Chrome/34.0.1847.76 Mobile Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; Trident/7.0; yie9; rv:11.0) like Gecko", + "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.143 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.89 Safari/537.36", + "Mozilla/5.0 (Linux; U; Android 4.4.3; en-us; KFSAWA Build/KTU84M) AppleWebKit/537.36 (KHTML, like Gecko) Silk/3.68 like Chrome/39.0.2171.93 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; WOW64; rv:37.0) Gecko/20100101 Firefox/37.0", + "Mozilla/5.0 (compatible; Windows NT 6.1; Catchpoint) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.81 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:25.0) Gecko/20100101 Firefox/29.0", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:32.0) Gecko/20100101 Firefox/32.0", + "Mozilla/5.0 (Windows NT 6.0; rv:38.0) Gecko/20100101 Firefox/38.0", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.118 Safari/537.36", + "Mozilla/5.0 (Linux; Android 4.4.4; Z970 Build/KTU84P) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/30.0.0.0 Mobile Safari/537.36", + "Mozilla/5.0 (Linux; Android 5.1.1; Nexus 5 Build/LMY48I) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.84 Mobile Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_3) AppleWebKit/534.55.3 (KHTML, like Gecko) Version/5.1.3 Safari/534.53.10", + "Mozilla/5.0 (X11; CrOS armv7l 6812.88.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.153 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2227.1 Safari/537.36", + "Mozilla/5.0 (iPhone; CPU iPhone OS 6_1_3 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/6.0 Mobile/10B329 Safari/8536.25", + "Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; MAARJS; rv:11.0) like Gecko", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:36.0) Gecko/20100101 Firefox/36.0", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.155 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; WOW64; rv:34.0) Gecko/20100101 Firefox/34.0", + "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0; )", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.132 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; Win64; x64; Trident/7.0; Touch; MASAJS; rv:11.0) like Gecko", + "Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; Touch; MAARJS; rv:11.0) like Gecko", + "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.101 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.101 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/537.13+ (KHTML, like Gecko) Version/5.1.7 Safari/534.57.2", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10; rv:33.0) Gecko/20100101 Firefox/33.0", + "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.134 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.63 Safari/537.36 SE 2.X MetaSr 1.0", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 BIDUBrowser/7.6 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; MASMJS; rv:11.0) like Gecko", + "Mozilla/5.0 (Windows NT 10.0; Trident/7.0; Touch; rv:11.0) like Gecko", + "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET4.0C; .NET4.0E; 360SE)", + "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; InfoPath.3; .NET4.0C; .NET4.0E; MS-RTC LM 8)", + "Mozilla/5.0 (Windows NT 6.3; WOW64; Trident/7.0; Touch; MAGWJS; rv:11.0) like Gecko", + "Mozilla/5.0 (Linux; Android 5.1.1; SAMSUNG SM-G925T Build/LMY47X) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/3.2 Chrome/38.0.2125.102 Mobile Safari/537.36", + "Mozilla/5.0 (X11; CrOS x86_64 6457.107.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.115 Safari/537.36", + "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E; 360SE)", + "Mozilla/5.0 (iPad; CPU OS 8_4_1 like Mac OS X) AppleWebKit/600.1.4.17.9 (KHTML, like Gecko) Version/5.1 Mobile/9B206 Safari/7534.48.3", + "Mozilla/5.0 (Linux; Android 4.2.2; GT-P5113 Build/JDQ39) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.84 Safari/537.36", + "Mozilla/5.0 (X11; Linux i686; rv:24.0) Gecko/20100101 Firefox/24.0 DejaClick/2.5.0.11", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.154 Safari/537.36 LBBROWSER", + "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.122 Safari/537.36 SE 2.X MetaSr 1.0", + "Mozilla/5.0 (Linux; Android 4.4.3; KFARWI Build/KTU84M) AppleWebKit/537.36 (KHTML, like Gecko) Silk/44.1.81 like Chrome/44.0.2403.128 Safari/537.36", + "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.125 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.117 Safari/537.36", + "Mozilla/5.0 (iPad; CPU OS 8_1_3 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) GSA/8.0.57838 Mobile/12B466 Safari/600.1.4", + "Mozilla/5.0 (Unknown; Linux i686) AppleWebKit/534.34 (KHTML, like Gecko) Chrome/20.0.1132.57 Safari/534.34", + "Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; NP08; MAAU; NP08; rv:11.0) like Gecko", + "Mozilla/5.0 (Linux; Android 4.4.2; LG-V410 Build/KOT49I.V41010d) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.84 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.122 Safari/537.36 SE 2.X MetaSr 1.0", + "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.1; WOW64; Trident/7.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E; InfoPath.3)", + "Mozilla/5.0 (Windows NT 6.1; rv:28.0) Gecko/20100101 Firefox/28.0", + "Mozilla/5.0 (X11; CrOS x86_64 6946.70.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.132 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.124 Safari/537.36", + "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.1; WOW64; Trident/6.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; InfoPath.3; .NET4.0C; .NET4.0E)", + "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:33.0) Gecko/20100101 Firefox/33.0", + "Mozilla/5.0 (iPod touch; CPU iPhone OS 8_4_1 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Version/8.0 Mobile/12H321 Safari/600.1.4", + "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:38.0) Gecko/20100101 IceDragon/38.0.5 Firefox/38.0.5", + "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.152 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; managedpc; rv:11.0) like Gecko", + "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.116 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; Touch; MASMJS; rv:11.0) like Gecko", + "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.111 Safari/537.36", + "Mozilla/5.0 (Linux; U; Android 4.0.3; en-ca; KFOT Build/IML74K) AppleWebKit/537.36 (KHTML, like Gecko) Silk/3.68 like Chrome/39.0.2171.93 Safari/537.36", + "Mozilla/5.0 (Linux; Android 4.2.2; Le Pan TC802A Build/JDQ39) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.84 Safari/537.36", + "Mozilla/5.0 (iPad; CPU OS 7_1_2 like Mac OS X) AppleWebKit/537.51.2 (KHTML, like Gecko) GSA/6.0.51363 Mobile/11D257 Safari/9537.53", + "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.152 Safari/537.36 LBBROWSER", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:37.0) Gecko/20100101 Firefox/37.0", + "Mozilla/5.0 (Windows NT 6.2; ARM; Trident/7.0; Touch; rv:11.0; WPDesktop; Lumia 1520) like Gecko", + "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.65 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:42.0) Gecko/20100101 Firefox/42.0", + "Mozilla/5.0 (iPhone; CPU iPhone OS 7_0_6 like Mac OS X) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11B651 Safari/9537.53", + "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; InfoPath.2; .NET4.0C; .NET4.0E)", + "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET4.0C; .NET4.0E; 360SE)", + "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.103 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.125 Safari/537.36", + "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.1; WOW64; Trident/5.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; InfoPath.3; .NET4.0C; .NET4.0E)", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:34.0) Gecko/20100101 Firefox/34.0", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.76 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.87 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; PRU_IE; rv:11.0) like Gecko", + "Mozilla/5.0 (X11; Linux i686) AppleWebKit/537.36 (KHTML, like Gecko) Ubuntu Chromium/37.0.2062.120 Chrome/37.0.2062.120 Safari/537.36", + "Mozilla/5.0 (iPad; CPU OS 8_4_1 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Mobile/12H321 [FBAN/FBIOS;FBAV/38.0.0.6.79;FBBV/14316658;FBDV/iPad4,1;FBMD/iPad;FBSN/iPhone OS;FBSV/8.4.1;FBSS/2; FBCR/;FBID/tablet;FBLC/en_US;FBOP/1]", + "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.155 Safari/537.36 OPR/31.0.1889.174", + "Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; NP02; rv:11.0) like Gecko", + "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.111 Safari/537.36", + "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; Win64; x64; Trident/4.0; .NET CLR 2.0.50727; SLCC2; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E)", + "Mozilla/5.0 (X11; CrOS x86_64 6946.63.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.130 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.7; rv:37.0) Gecko/20100101 Firefox/37.0", + "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.115 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.0.9895 Safari/537.36", + "Mozilla/5.0 (Linux; Android 4.4.4; Nexus 7 Build/KTU84P) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.84 Safari/537.36", + "Mozilla/5.0 (Linux; Android 4.2.2; QMV7B Build/JDQ39) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.114 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; Win64; x64; Trident/7.0; Touch; MASMJS; rv:11.0) like Gecko", + "Mozilla/5.0 (compatible; MSIE 10.0; AOL 9.7; AOLBuild 4343.1028; Windows NT 6.1; WOW64; Trident/7.0)", + "Mozilla/5.0 (Linux; U; Android 4.0.3; en-us) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.59 Mobile Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; Trident/7.0; Touch; TNJB; rv:11.0) like Gecko", + "Mozilla/5.0 (iPad; CPU OS 8_1_3 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Mobile/12B466", + "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0; Active Content Browser)", + "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.1; WOW64; Trident/7.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET4.0C; .NET4.0E)", + "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.130 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.124 Safari/537.36", + "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.1; Trident/6.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E; InfoPath.3)", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.81 Safari/537.36", + "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2; Win64; x64; Trident/6.0; WebView/1.0)", + "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.89 Safari/537.36", + "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.130 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.91 Safari/537.36", + "Mozilla/5.0 (iPad; U; CPU OS 5_0 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko) Version/5.1 Mobile/9A334 Safari/7534.48.3", + "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.130 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) coc_coc_browser/50.0.125 Chrome/44.0.2403.125 Safari/537.36", + "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; WOW64; Trident/6.0; SLCC2; .NET CLR 2.0.50727; .NET4.0C; .NET4.0E)", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.124 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; Win64; x64; Trident/7.0; MAARJS; rv:11.0) like Gecko", + "Mozilla/5.0 (Linux; Android 5.0; SAMSUNG SM-N900T Build/LRX21V) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/2.1 Chrome/34.0.1847.76 Mobile Safari/537.36", + "Mozilla/5.0 (iPhone; CPU iPhone OS 8_4 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) GSA/7.0.55539 Mobile/12H143 Safari/600.1.4", +} diff --git a/internal/infrastructure/repository/file_repository.go b/internal/infrastructure/repository/file_repository.go new file mode 100644 index 00000000..b0f595a1 --- /dev/null +++ b/internal/infrastructure/repository/file_repository.go @@ -0,0 +1,218 @@ +package repository + +import ( + "encoding/json" + "encoding/xml" + "fmt" + "io" + "io/fs" + "os" + "path/filepath" + "strings" + + "github.com/fyvri/fresh-proxy-list/internal/entity" + "github.com/fyvri/fresh-proxy-list/pkg/utils" + + "gopkg.in/yaml.v3" +) + +type FileRepository struct { + MkdirAll func(path string, perm os.FileMode) error + Create func(name string) (io.Writer, error) + CSVWriter utils.CSVWriterUtilInterface +} + +type FileRepositoryInterface interface { + SaveFile(filePath string, data interface{}, format string) error + CreateDirectory(filePath string) error + WriteTxt(writer io.Writer, data interface{}) error + EncodeCSV(writer io.Writer, data interface{}) error + WriteCSV(writer io.Writer, header []string, rows [][]string) error + EncodeJSON(writer io.Writer, data interface{}) error + EncodeXML(writer io.Writer, data interface{}) error + EncodeYAML(writer io.Writer, data interface{}) error +} + +type MkdirAllFunc func(path string, perm os.FileMode) error +type CreateFunc func(name string) (io.Writer, error) + +func NewFileRepository(mkdirAll MkdirAllFunc, create CreateFunc, csvWriter utils.CSVWriterUtilInterface) FileRepositoryInterface { + return &FileRepository{ + MkdirAll: mkdirAll, + Create: create, + CSVWriter: csvWriter, + } +} + +func (r *FileRepository) SaveFile(filePath string, data interface{}, format string) error { + if err := r.CreateDirectory(filePath); err != nil { + return err + } + + file, err := r.Create(filePath) + if err != nil { + return fmt.Errorf("error creating file %s: %v", filePath, err) + } + defer func() { + if f, ok := file.(io.Closer); ok { + f.Close() + } + }() + + switch format { + case "txt": + return r.WriteTxt(file, data) + case "json": + return r.EncodeJSON(file, data) + case "csv": + return r.EncodeCSV(file, data) + case "xml": + return r.EncodeXML(file, data) + case "yaml": + return r.EncodeYAML(file, data) + default: + return fmt.Errorf("unsupported format: %s", format) + } +} + +func (r *FileRepository) CreateDirectory(filePath string) error { + err := r.MkdirAll(filepath.Dir(filePath), fs.ModePerm) + if err != nil { + return fmt.Errorf("error creating directory %s: %v", filePath, err) + } + return nil +} + +func (r *FileRepository) WriteTxt(writer io.Writer, data interface{}) error { + var dataString string + if stringData, ok := data.([]string); ok { + dataString = strings.Join(stringData, "\n") + } + + _, err := writer.Write([]byte(dataString)) + if err != nil { + return fmt.Errorf("error writing TXT: %v", err) + } + return nil +} + +func (r *FileRepository) EncodeCSV(writer io.Writer, data interface{}) error { + switch proxyData := data.(type) { + case []string: + rows := make([][]string, len(proxyData)) + for i, rowElem := range proxyData { + rows[i] = []string{rowElem} + } + return r.WriteCSV(writer, nil, rows) + case []entity.Proxy: + header := []string{"Proxy", "IP", "Port", "TimeTaken", "CheckedAt"} + rows := make([][]string, len(proxyData)) + for i, proxy := range proxyData { + rows[i] = []string{proxy.Proxy, proxy.IP, proxy.Port, fmt.Sprintf("%v", proxy.TimeTaken), proxy.CheckedAt} + } + return r.WriteCSV(writer, header, rows) + case []entity.AdvancedProxy: + header := []string{"Proxy", "IP", "Port", "Categories", "TimeTaken", "CheckedAt"} + rows := make([][]string, len(proxyData)) + for i, proxy := range proxyData { + rows[i] = []string{proxy.Proxy, proxy.IP, proxy.Port, strings.Join(proxy.Categories, ","), fmt.Sprintf("%v", proxy.TimeTaken), proxy.CheckedAt} + } + return r.WriteCSV(writer, header, rows) + default: + return fmt.Errorf("invalid data type for CSV encoding") + } +} + +func (r *FileRepository) WriteCSV(writer io.Writer, header []string, rows [][]string) error { + csvWriter := r.CSVWriter.Init(writer) + defer r.CSVWriter.Flush(csvWriter) + + if header != nil { + if err := r.CSVWriter.Write(csvWriter, header); err != nil { + return fmt.Errorf("failed to write header: %w", err) + } + } + + for _, row := range rows { + if err := r.CSVWriter.Write(csvWriter, row); err != nil { + return fmt.Errorf("failed to write row: %w", err) + } + } + + return nil +} + +func (r *FileRepository) EncodeJSON(writer io.Writer, data interface{}) error { + err := json.NewEncoder(writer).Encode(data) + if err != nil { + return fmt.Errorf("error encoding JSON: %v", err) + } + return nil +} + +func (r *FileRepository) EncodeXML(writer io.Writer, data interface{}) error { + var err error + switch proxyData := data.(type) { + case []string: + view := entity.ProxyXMLClassicView{ + XMLName: xml.Name{Local: "proxies"}, + Proxies: make([]string, len(proxyData)), + } + copy(view.Proxies, proxyData) + err = xml.NewEncoder(writer).Encode(view) + case []entity.Proxy: + view := entity.ProxyXMLAdvancedView{ + XMLName: xml.Name{Local: "proxies"}, + Proxies: make([]entity.Proxy, len(proxyData)), + } + copy(view.Proxies, proxyData) + err = xml.NewEncoder(writer).Encode(view) + case []entity.AdvancedProxy: + view := entity.ProxyXMLAllAdvancedView{ + XMLName: xml.Name{Local: "Proxies"}, + Proxies: make([]entity.AdvancedProxy, len(proxyData)), + } + copy(view.Proxies, proxyData) + err = xml.NewEncoder(writer).Encode(view) + } + + if err != nil { + return fmt.Errorf("error encoding XML: %v", err) + } + return nil +} + +func (r *FileRepository) EncodeYAML(writer io.Writer, data interface{}) error { + var err error + switch proxyData := data.(type) { + case []string: + view := struct { + Proxies []string `yaml:"proxies"` + }{ + Proxies: make([]string, len(proxyData)), + } + copy(view.Proxies, proxyData) + err = yaml.NewEncoder(writer).Encode(view) + case []entity.Proxy: + view := struct { + Proxies []entity.Proxy `yaml:"proxies"` + }{ + Proxies: make([]entity.Proxy, len(proxyData)), + } + copy(view.Proxies, proxyData) + err = yaml.NewEncoder(writer).Encode(view) + case []entity.AdvancedProxy: + view := struct { + Proxies []entity.AdvancedProxy `yaml:"proxies"` + }{ + Proxies: make([]entity.AdvancedProxy, len(proxyData)), + } + copy(view.Proxies, proxyData) + err = yaml.NewEncoder(writer).Encode(view) + } + + if err != nil { + return fmt.Errorf("error encoding YAML: %v", err) + } + return nil +} diff --git a/internal/infrastructure/repository/proxy_repository.go b/internal/infrastructure/repository/proxy_repository.go new file mode 100644 index 00000000..96c1a8c1 --- /dev/null +++ b/internal/infrastructure/repository/proxy_repository.go @@ -0,0 +1,132 @@ +package repository + +import ( + "cmp" + "slices" + "sync" + + "github.com/fyvri/fresh-proxy-list/internal/entity" +) + +type ProxyRepository struct { + Mutex sync.RWMutex + AllClassicView []string + HTTPClassicView []string + HTTPSClassicView []string + SOCKS4ClassicView []string + SOCKS5ClassicView []string + AllAdvancedView []entity.AdvancedProxy + HTTPAdvancedView []entity.Proxy + HTTPSAdvancedView []entity.Proxy + SOCKS4AdvancedView []entity.Proxy + SOCKS5AdvancedView []entity.Proxy +} + +type ProxyRepositoryInterface interface { + Store(proxy *entity.Proxy) + GetAllClassicView() []string + GetHTTPClassicView() []string + GetHTTPSClassicView() []string + GetSOCKS4ClassicView() []string + GetSOCKS5ClassicView() []string + GetAllAdvancedView() []entity.AdvancedProxy + GetHTTPAdvancedView() []entity.Proxy + GetHTTPSAdvancedView() []entity.Proxy + GetSOCKS4AdvancedView() []entity.Proxy + GetSOCKS5AdvancedView() []entity.Proxy +} + +func NewProxyRepository() ProxyRepositoryInterface { + return &ProxyRepository{ + Mutex: sync.RWMutex{}, + } +} + +func (r *ProxyRepository) Store(proxy *entity.Proxy) { + r.Mutex.Lock() + defer r.Mutex.Unlock() + + updateProxyAll := func(proxy *entity.Proxy, classicList *[]string, advancedList *[]entity.AdvancedProxy) { + n, found := slices.BinarySearchFunc(*advancedList, entity.AdvancedProxy{Proxy: proxy.Proxy}, func(a, b entity.AdvancedProxy) int { + return cmp.Compare(a.Proxy, b.Proxy) + }) + if found { + if proxy.Category == "HTTP" && proxy.TimeTaken > 0 { + (*advancedList)[n].TimeTaken = proxy.TimeTaken + } + + if m, found := slices.BinarySearch((*advancedList)[n].Categories, proxy.Category); !found { + (*advancedList)[n].Categories = slices.Insert((*advancedList)[n].Categories, m, proxy.Category) + } + } else { + *classicList = append(*classicList, proxy.Proxy) + *advancedList = slices.Insert(*advancedList, n, entity.AdvancedProxy{ + Proxy: proxy.Proxy, + IP: proxy.IP, + Port: proxy.Port, + TimeTaken: proxy.TimeTaken, + CheckedAt: proxy.CheckedAt, + Categories: []string{ + proxy.Category, + }, + }) + } + } + + switch proxy.Category { + case "HTTP": + r.HTTPClassicView = append(r.HTTPClassicView, proxy.Proxy) + r.HTTPAdvancedView = append(r.HTTPAdvancedView, *proxy) + case "HTTPS": + r.HTTPSClassicView = append(r.HTTPSClassicView, proxy.Proxy) + r.HTTPSAdvancedView = append(r.HTTPSAdvancedView, *proxy) + case "SOCKS4": + r.SOCKS4ClassicView = append(r.SOCKS4ClassicView, proxy.Proxy) + r.SOCKS4AdvancedView = append(r.SOCKS4AdvancedView, *proxy) + case "SOCKS5": + r.SOCKS5ClassicView = append(r.SOCKS5ClassicView, proxy.Proxy) + r.SOCKS5AdvancedView = append(r.SOCKS5AdvancedView, *proxy) + } + + updateProxyAll(proxy, &r.AllClassicView, &r.AllAdvancedView) +} + +func (r *ProxyRepository) GetAllClassicView() []string { + return r.AllClassicView +} + +func (r *ProxyRepository) GetHTTPClassicView() []string { + return r.HTTPClassicView +} + +func (r *ProxyRepository) GetHTTPSClassicView() []string { + return r.HTTPSClassicView +} + +func (r *ProxyRepository) GetSOCKS4ClassicView() []string { + return r.SOCKS4ClassicView +} + +func (r *ProxyRepository) GetSOCKS5ClassicView() []string { + return r.SOCKS5ClassicView +} + +func (r *ProxyRepository) GetAllAdvancedView() []entity.AdvancedProxy { + return r.AllAdvancedView +} + +func (r *ProxyRepository) GetHTTPAdvancedView() []entity.Proxy { + return r.HTTPAdvancedView +} + +func (r *ProxyRepository) GetHTTPSAdvancedView() []entity.Proxy { + return r.HTTPSAdvancedView +} + +func (r *ProxyRepository) GetSOCKS4AdvancedView() []entity.Proxy { + return r.SOCKS4AdvancedView +} + +func (r *ProxyRepository) GetSOCKS5AdvancedView() []entity.Proxy { + return r.SOCKS5AdvancedView +} diff --git a/internal/infrastructure/repository/source_repository.go b/internal/infrastructure/repository/source_repository.go new file mode 100644 index 00000000..c421ff1e --- /dev/null +++ b/internal/infrastructure/repository/source_repository.go @@ -0,0 +1,38 @@ +package repository + +import ( + "encoding/json" + "errors" + "fmt" + + "github.com/fyvri/fresh-proxy-list/internal/entity" +) + +type SourceRepository struct { + ProxyResources string +} + +type SourceRepositoryInterface interface { + LoadSources() ([]entity.Source, error) +} + +func NewSourceRepository(proxyResources string) SourceRepositoryInterface { + return &SourceRepository{ + ProxyResources: proxyResources, + } +} + +func (r *SourceRepository) LoadSources() ([]entity.Source, error) { + sourcesJSON := r.ProxyResources + if sourcesJSON == "" { + return nil, errors.New("PROXY_RESOURCES not found on environment") + } + + var sources []entity.Source + err := json.Unmarshal([]byte(sourcesJSON), &sources) + if err != nil { + return nil, fmt.Errorf("error parsing JSON: %v", err) + } + + return sources, nil +} diff --git a/internal/service/proxy_service.go b/internal/service/proxy_service.go new file mode 100644 index 00000000..ba7b910d --- /dev/null +++ b/internal/service/proxy_service.go @@ -0,0 +1,142 @@ +package service + +import ( + "crypto/tls" + "fmt" + "math/rand" + "net" + "net/http" + "strings" + "time" + + "github.com/fyvri/fresh-proxy-list/internal/entity" + "github.com/fyvri/fresh-proxy-list/pkg/utils" + + "h12.io/socks" +) + +type ProxyService struct { + FetcherUtil utils.FetcherUtilInterface + URLParserUtil utils.URLParserUtilInterface + HTTPTestingSites []string + HTTPSTestingSites []string + UserAgents []string + Semaphore chan struct{} +} + +type ProxyServiceInterface interface { + Check(category string, ip string, port string) (*entity.Proxy, error) + GetTestingSite(category string) string + GetRandomUserAgent() string +} + +func NewProxyService( + fetcherUtil utils.FetcherUtilInterface, + urlParserUtil utils.URLParserUtilInterface, + httpTestingSites []string, + httpsTestingSites []string, + userAgents []string, +) ProxyServiceInterface { + return &ProxyService{ + FetcherUtil: fetcherUtil, + URLParserUtil: urlParserUtil, + HTTPTestingSites: httpTestingSites, + HTTPSTestingSites: httpsTestingSites, + UserAgents: userAgents, + Semaphore: make(chan struct{}, 500), + } +} + +func (s *ProxyService) Check(category string, ip string, port string) (*entity.Proxy, error) { + s.Semaphore <- struct{}{} + defer func() { <-s.Semaphore }() + + var ( + transport *http.Transport + proxy = ip + ":" + port + proxyURI = strings.ToLower(category + "://" + proxy) + testingSite = s.GetTestingSite(category) + timeout = 60 * time.Second + ) + + if category == "HTTP" || category == "HTTPS" { + proxyURL, err := s.URLParserUtil.Parse(proxyURI) + if err != nil { + return nil, fmt.Errorf("error parsing proxy URL: %v", err) + } + + transport = &http.Transport{ + Proxy: http.ProxyURL(proxyURL), + DisableKeepAlives: true, + DialContext: (&net.Dialer{ + Timeout: timeout, + KeepAlive: timeout, + }).DialContext, + TLSHandshakeTimeout: timeout, + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: category == "HTTPS", + }, + } + } else if category == "SOCKS4" || category == "SOCKS5" { + proxyURL := socks.Dial(proxyURI) + transport = &http.Transport{ + Dial: proxyURL, + DisableKeepAlives: true, + DialContext: (&net.Dialer{ + Timeout: timeout, + KeepAlive: timeout, + }).DialContext, + } + } else { + return nil, fmt.Errorf("proxy category %s not supported", category) + } + + req, err := s.FetcherUtil.NewRequest("GET", testingSite, nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %s", err) + } + req.Header.Set("User-Agent", s.GetRandomUserAgent()) + + startTime := time.Now() + resp, err := s.FetcherUtil.Do(&http.Client{ + Transport: transport, + Timeout: timeout, + }, req) + + // statusCode := "" + // if err == nil { + // statusCode = http.StatusText(resp.StatusCode) + // } + // log.Printf("Check %s: %s ~> %s ~> %v", fmt.Sprintf("%-25s", proxy), fmt.Sprintf("%-30s", statusCode), testingSite, err) + + if err != nil { + return nil, fmt.Errorf("request error: %s", err) + } + defer resp.Body.Close() + endTime := time.Now() + timeTaken := endTime.Sub(startTime).Seconds() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected status code %d: %s", resp.StatusCode, http.StatusText(resp.StatusCode)) + } + + return &entity.Proxy{ + Proxy: proxy, + IP: ip, + Port: port, + Category: category, + CheckedAt: endTime.Format(time.RFC3339), + TimeTaken: timeTaken, + }, nil +} + +func (s *ProxyService) GetTestingSite(category string) string { + if category == "HTTPS" { + return s.HTTPSTestingSites[rand.Intn(len(s.HTTPSTestingSites))] + } + return s.HTTPTestingSites[rand.Intn(len(s.HTTPTestingSites))] +} + +func (s *ProxyService) GetRandomUserAgent() string { + return s.UserAgents[rand.Intn(len(s.UserAgents))] +} diff --git a/internal/usecase/file_usecase.go b/internal/usecase/file_usecase.go new file mode 100644 index 00000000..81976e70 --- /dev/null +++ b/internal/usecase/file_usecase.go @@ -0,0 +1,59 @@ +package usecase + +import ( + "path/filepath" + "strings" + "sync" + + "github.com/fyvri/fresh-proxy-list/internal/infrastructure/repository" +) + +type fileUsecase struct { + FileRepository repository.FileRepositoryInterface + ProxyRepository repository.ProxyRepositoryInterface + FileOutputExtensions []string + WaitGroup sync.WaitGroup +} + +type FileUsecaseInterface interface { + SaveFiles() +} + +func NewFileUsecase(fileRepository repository.FileRepositoryInterface, proxyRepository repository.ProxyRepositoryInterface, fileOutputExtensions []string) FileUsecaseInterface { + return &fileUsecase{ + FileRepository: fileRepository, + ProxyRepository: proxyRepository, + FileOutputExtensions: fileOutputExtensions, + WaitGroup: sync.WaitGroup{}, + } +} + +func (uc *fileUsecase) SaveFiles() { + createFile := func(filename string, classic []string, advanced interface{}) { + uc.WaitGroup.Add((len(uc.FileOutputExtensions) * 2) + 1) + + filename = strings.ToLower(filename) + for _, ext := range uc.FileOutputExtensions { + go func(ext string) { + defer uc.WaitGroup.Done() + uc.FileRepository.SaveFile(filepath.Join("storage", "classic", filename+"."+ext), classic, ext) + }(ext) + go func(ext string) { + defer uc.WaitGroup.Done() + uc.FileRepository.SaveFile(filepath.Join("storage", "advanced", filename+"."+ext), advanced, ext) + }(ext) + } + + go func() { + defer uc.WaitGroup.Done() + uc.FileRepository.SaveFile(filepath.Join("storage", "classic", filename+".txt"), classic, "txt") + }() + } + + createFile("all", uc.ProxyRepository.GetAllClassicView(), uc.ProxyRepository.GetAllAdvancedView()) + createFile("http", uc.ProxyRepository.GetHTTPClassicView(), uc.ProxyRepository.GetHTTPAdvancedView()) + createFile("https", uc.ProxyRepository.GetHTTPSClassicView(), uc.ProxyRepository.GetHTTPSAdvancedView()) + createFile("socks4", uc.ProxyRepository.GetSOCKS4ClassicView(), uc.ProxyRepository.GetSOCKS4AdvancedView()) + createFile("socks5", uc.ProxyRepository.GetSOCKS5ClassicView(), uc.ProxyRepository.GetSOCKS5AdvancedView()) + uc.WaitGroup.Wait() +} diff --git a/internal/usecase/proxy_usecase.go b/internal/usecase/proxy_usecase.go new file mode 100644 index 00000000..d897c8cc --- /dev/null +++ b/internal/usecase/proxy_usecase.go @@ -0,0 +1,126 @@ +package usecase + +import ( + "fmt" + "net" + "regexp" + "slices" + "strconv" + "strings" + "sync" + + "github.com/fyvri/fresh-proxy-list/internal/entity" + "github.com/fyvri/fresh-proxy-list/internal/infrastructure/repository" + "github.com/fyvri/fresh-proxy-list/internal/service" +) + +type ProxyUsecase struct { + ProxyRepository repository.ProxyRepositoryInterface + ProxyService service.ProxyServiceInterface + ProxyMap sync.Map + SpecialIPs []string + PrivateIPs []net.IPNet +} + +type ProxyUsecaseInterface interface { + ProcessProxy(category string, proxy string, isChecked bool) (*entity.Proxy, error) + IsSpecialIP(ip string) bool + GetAllAdvancedView() []entity.AdvancedProxy +} + +func NewProxyUsecase( + proxyRepository repository.ProxyRepositoryInterface, + proxyService service.ProxyServiceInterface, + specialIPs []string, + privateIPs []net.IPNet, +) ProxyUsecaseInterface { + return &ProxyUsecase{ + ProxyRepository: proxyRepository, + ProxyService: proxyService, + SpecialIPs: specialIPs, + PrivateIPs: privateIPs, + ProxyMap: sync.Map{}, + } +} + +func (uc *ProxyUsecase) ProcessProxy(category string, proxy string, isChecked bool) (*entity.Proxy, error) { + proxy = strings.TrimSpace(strings.ReplaceAll(strings.ReplaceAll(proxy, "\r", ""), "\n", "")) + if proxy == "" { + return nil, fmt.Errorf("proxy not found") + } + + proxyParts := strings.Split(proxy, ":") + if len(proxyParts) != 2 { + return nil, fmt.Errorf("proxy format incorrect") + } + + pattern := `^((25[0-5]|2[0-4][0-9]|[0-1]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[0-1]?[0-9][0-9]?)\:(0|[1-9][0-9]{0,4})$` + re := regexp.MustCompile(pattern) + if !re.MatchString(proxy) { + return nil, fmt.Errorf("proxy format not match") + } + + if uc.IsSpecialIP(proxyParts[0]) { + return nil, fmt.Errorf("proxy belongs to special ip") + } + + port, err := strconv.Atoi(proxyParts[1]) + if err != nil || port < 0 || port > 65535 { + return nil, fmt.Errorf("proxy port format incorrect") + } + + _, loaded := uc.ProxyMap.LoadOrStore(category+"_"+proxy, true) + if loaded { + return nil, fmt.Errorf("proxy has been processed") + } + + var ( + data *entity.Proxy + proxyIP, proxyPort = proxyParts[0], proxyParts[1] + ) + if isChecked { + data, err = uc.ProxyService.Check(category, proxyIP, proxyPort) + if err != nil { + return nil, err + } + } else { + data = &entity.Proxy{ + Proxy: proxy, + IP: proxyIP, + Port: proxyPort, + Category: category, + TimeTaken: 0, + CheckedAt: "", + } + } + uc.ProxyRepository.Store(data) + + return data, nil +} + +func (uc *ProxyUsecase) IsSpecialIP(ip string) bool { + if _, found := slices.BinarySearch(uc.SpecialIPs, ip); found { + return true + } + + ipAddress := net.ParseIP(ip) + if ipAddress == nil { + return true + } + + if ipAddress.IsLoopback() || ipAddress.IsMulticast() || ipAddress.IsUnspecified() { + return true + } + + for _, r := range uc.PrivateIPs { + if r.Contains(ipAddress) { + return true + } + } + + return false +} + +func (uc *ProxyUsecase) GetAllAdvancedView() []entity.AdvancedProxy { + return uc.ProxyRepository.GetAllAdvancedView() +} diff --git a/internal/usecase/source_usecase.go b/internal/usecase/source_usecase.go new file mode 100644 index 00000000..896f4e4f --- /dev/null +++ b/internal/usecase/source_usecase.go @@ -0,0 +1,52 @@ +package usecase + +import ( + "fmt" + "regexp" + "strings" + + "github.com/fyvri/fresh-proxy-list/internal/entity" + "github.com/fyvri/fresh-proxy-list/internal/infrastructure/repository" + "github.com/fyvri/fresh-proxy-list/pkg/utils" +) + +type SourceUsecase struct { + SourceRepository repository.SourceRepositoryInterface + FetcherUtil utils.FetcherUtilInterface +} + +type SourceUsecaseInterface interface { + LoadSources() ([]entity.Source, error) + ProcessSource(source *entity.Source) ([]string, error) +} + +func NewSourceUsecase(sourceRepository repository.SourceRepositoryInterface, fetcherUtil utils.FetcherUtilInterface) SourceUsecaseInterface { + return &SourceUsecase{ + SourceRepository: sourceRepository, + FetcherUtil: fetcherUtil, + } +} + +func (uc *SourceUsecase) LoadSources() ([]entity.Source, error) { + return uc.SourceRepository.LoadSources() +} + +func (uc *SourceUsecase) ProcessSource(source *entity.Source) ([]string, error) { + body, err := uc.FetcherUtil.FetchData(source.URL) + if err != nil { + return nil, err + } + + var proxies []string + switch source.Method { + case "LIST": + proxies = strings.Split(strings.TrimSpace(string(body)), "\n") + case "SCRAP": + re := regexp.MustCompile(`[0-9]+(?:\.[0-9]+){3}:[0-9]+`) + proxies = re.FindAllString(string(body), -1) + default: + return nil, fmt.Errorf("source method not found: %s", source.Method) + } + + return proxies, nil +} diff --git a/pkg/utils/csv_writer_util.go b/pkg/utils/csv_writer_util.go new file mode 100644 index 00000000..001b6e31 --- /dev/null +++ b/pkg/utils/csv_writer_util.go @@ -0,0 +1,31 @@ +package utils + +import ( + "encoding/csv" + "io" +) + +type CSVWriterUtil struct { +} + +type CSVWriterUtilInterface interface { + Init(w io.Writer) *csv.Writer + Flush(csvWriter *csv.Writer) + Write(csvWriter *csv.Writer, record []string) error +} + +func NewCSVWriter() CSVWriterUtilInterface { + return &CSVWriterUtil{} +} + +func (u *CSVWriterUtil) Init(w io.Writer) *csv.Writer { + return csv.NewWriter(w) +} + +func (u *CSVWriterUtil) Flush(csvWriter *csv.Writer) { + csvWriter.Flush() +} + +func (u *CSVWriterUtil) Write(csvWriter *csv.Writer, record []string) error { + return csvWriter.Write(record) +} diff --git a/pkg/utils/fetcher_util.go b/pkg/utils/fetcher_util.go new file mode 100644 index 00000000..4a2b43ae --- /dev/null +++ b/pkg/utils/fetcher_util.go @@ -0,0 +1,58 @@ +package utils + +import ( + "fmt" + "io" + "net/http" +) + +type FetcherUtil struct { + Client *http.Client + NewRequestFunc func(method string, url string, body io.Reader) (*http.Request, error) +} + +type FetcherUtilInterface interface { + NewRequest(method, url string, body io.Reader) (*http.Request, error) + Do(client *http.Client, req *http.Request) (*http.Response, error) + FetchData(url string) ([]byte, error) +} + +func NewFetcher(client *http.Client, newRequestFunc func(method, url string, body io.Reader) (*http.Request, error)) FetcherUtilInterface { + return &FetcherUtil{ + Client: client, + NewRequestFunc: newRequestFunc, + } +} + +func (u *FetcherUtil) Do(client *http.Client, req *http.Request) (*http.Response, error) { + return client.Do(req) +} + +func (u *FetcherUtil) NewRequest(method string, url string, body io.Reader) (*http.Request, error) { + return http.NewRequest(method, url, body) +} + +func (u *FetcherUtil) FetchData(url string) ([]byte, error) { + req, err := u.NewRequestFunc("GET", url, nil) + if err != nil { + return nil, err + } + + resp, err := u.Do(u.Client, req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return body, fmt.Errorf("failed to fetch data: %s", http.StatusText(resp.StatusCode)) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + return body, nil +} diff --git a/pkg/utils/url_parser_util.go b/pkg/utils/url_parser_util.go new file mode 100644 index 00000000..0d7c4d7a --- /dev/null +++ b/pkg/utils/url_parser_util.go @@ -0,0 +1,19 @@ +package utils + +import ( + "net/url" +) + +type URLParserUtil struct{} + +type URLParserUtilInterface interface { + Parse(rawURL string) (*url.URL, error) +} + +func NewURLParser() URLParserUtilInterface { + return &URLParserUtil{} +} + +func (u *URLParserUtil) Parse(rawURL string) (*url.URL, error) { + return url.Parse(rawURL) +} diff --git a/test/unit/internal/entity/source_test.go b/test/unit/internal/entity/source_test.go new file mode 100644 index 00000000..378afde1 --- /dev/null +++ b/test/unit/internal/entity/source_test.go @@ -0,0 +1,127 @@ +package entity_test + +import ( + "encoding/json" + "strconv" + "testing" + + "github.com/fyvri/fresh-proxy-list/internal/entity" +) + +var ( + expectedButGotMessage = "Expected %v = %v, but got = %v" + expectedErrorButGotMessage = "Expected %v error = %v, but got = %v" + testMethod = "LIST" + testCategory = "HTTP" + testURL = "http://example.com" + testIsChecked = false +) + +func TestUnmarshalJSONWithIsChecked(t *testing.T) { + var ( + source = entity.Source{} + data = []byte(`{ + "method": "` + testMethod + `", + "category": "` + testCategory + `", + "url": "` + testURL + `", + "is_checked": ` + strconv.FormatBool(testIsChecked) + ` + }`) + ) + err := json.Unmarshal(data, &source) + + if err != nil { + t.Errorf(expectedErrorButGotMessage, "unmarshal", nil, err) + } + + if source.Method != testMethod { + t.Errorf(expectedButGotMessage, "method", testMethod, source.Method) + } + + if source.Category != testCategory { + t.Errorf(expectedButGotMessage, "category", testCategory, source.Category) + } + + if source.URL != testURL { + t.Errorf(expectedButGotMessage, "url", testURL, source.Category) + } + + if source.IsChecked != testIsChecked { + t.Errorf(expectedButGotMessage, "is_checked", testIsChecked, source.IsChecked) + } +} + +func TestUnmarshalJSONWithoutIsChecked(t *testing.T) { + var ( + source = entity.Source{} + data = []byte(`{ + "method": "` + testMethod + `", + "category": "` + testCategory + `", + "url": "` + testURL + `" + }`) + ) + err := json.Unmarshal(data, &source) + + if err != nil { + t.Errorf(expectedErrorButGotMessage, "unmarshal", nil, err) + } + + if source.Method != testMethod { + t.Errorf(expectedButGotMessage, "method", testMethod, source.Method) + } + + if source.Category != testCategory { + t.Errorf(expectedButGotMessage, "category", testCategory, source.Category) + } + + if source.URL != testURL { + t.Errorf(expectedButGotMessage, "url", testURL, source.Category) + } + + if source.IsChecked != true { + t.Errorf(expectedButGotMessage, "is_checked", true, source.IsChecked) + } +} + +func TestUnmarshalJSONWithInvalidData(t *testing.T) { + var ( + source = entity.Source{} + data = []byte(`{ + "method": "` + testMethod + `", + "category": "` + testCategory + `", + "url": "` + testURL + `", + "is_checked": "string_instead_of_bool" + }`) + ) + err := json.Unmarshal(data, &source) + if err == nil { + t.Errorf(expectedButGotMessage, "unmarshal", "any error", err) + } +} + +func TestUnmarshalJSONWithEmptyData(t *testing.T) { + var ( + source = entity.Source{} + data = []byte(`{}`) + ) + err := json.Unmarshal(data, &source) + + if err != nil { + t.Errorf(expectedErrorButGotMessage, "unmarshal", nil, err) + } + + if source.Method != "" { + t.Errorf(expectedButGotMessage, "method", "empty", source.Method) + } + + if source.Category != "" { + t.Errorf(expectedButGotMessage, "category", "empty", source.Category) + } + + if source.URL != "" { + t.Errorf(expectedButGotMessage, "url", "empty", source.Category) + } + + if source.IsChecked != true { + t.Errorf(expectedButGotMessage, "is_checked", true, source.IsChecked) + } +} diff --git a/test/unit/internal/infrastructure/repository/file_repository_test.go b/test/unit/internal/infrastructure/repository/file_repository_test.go new file mode 100644 index 00000000..62f31a43 --- /dev/null +++ b/test/unit/internal/infrastructure/repository/file_repository_test.go @@ -0,0 +1,531 @@ +package repository_test + +import ( + "bytes" + "errors" + "fmt" + "io" + "io/fs" + "os" + "strings" + "testing" + + "github.com/fyvri/fresh-proxy-list/internal/infrastructure/repository" + "github.com/fyvri/fresh-proxy-list/pkg/utils" +) + +var ( + column1 = "Column1" + column2 = "Column2" + row1Column1 = "Row1Column1" + row1Column2 = "Row1Column2" + row2Column1 = "Row2Column1" + row2Column2 = "Row2Column2" +) + +func TestNewFileRepository(t *testing.T) { + mockMkdirAll := func(path string, perm fs.FileMode) error { + if path == "" { + return errors.New("path cannot be empty") + } + return nil + } + mockCreate := func(name string) (io.Writer, error) { + if name == "" { + return nil, errors.New("file name cannot be empty") + } + return &bytes.Buffer{}, nil + } + mockCSVWriterUtil := &mockCSVWriterUtil{} + fileRepository := repository.NewFileRepository(mockMkdirAll, mockCreate, mockCSVWriterUtil) + + if fileRepository == nil { + t.Errorf(expectedReturnNonNil, "NewFileRepository", "FileRepositoryInterface") + } + + r, ok := fileRepository.(*repository.FileRepository) + if !ok { + t.Errorf(expectedTypeAssertionErrorMessage, "*FileRepository") + } + + if r.MkdirAll == nil { + t.Errorf("expected mkdirAll to be set") + } + + if r.Create == nil { + t.Errorf("expected create to be set") + } +} + +func TestSaveFile(t *testing.T) { + type fields struct { + mkdirAll func(path string, perm os.FileMode) error + create func(name string) (io.Writer, error) + csvWriter utils.CSVWriterUtilInterface + } + + type args struct { + path string + data interface{} + format string + } + + tests := []struct { + name string + fields fields + args args + want string + wantError error + }{ + { + name: "CreateDirectoryError", + fields: fields{ + mkdirAll: func(path string, perm os.FileMode) error { + return errors.New("error creating directory") + }, + create: func(name string) (io.Writer, error) { + return nil, nil + }, + }, + args: args{ + path: testClassicFilePath + "." + testTXTExtension, + data: strings.Join(testIPs, "\n"), + format: testTXTExtension, + }, + want: "", + wantError: fmt.Errorf("error creating directory %v: %v", testClassicFilePath+"."+testTXTExtension, "error creating directory"), + }, + { + name: "CreateFileError", + fields: fields{ + mkdirAll: func(path string, perm os.FileMode) error { + return nil + }, + create: func(name string) (io.Writer, error) { + return nil, errors.New("error creating file") + }, + }, + args: args{ + path: testClassicFilePath + "." + testTXTExtension, + data: testIPs, + format: testTXTExtension, + }, + want: "", + wantError: fmt.Errorf("error creating file %v: %v", testClassicFilePath+"."+testTXTExtension, "error creating file"), + }, + { + name: "UnsupportedFormat", + fields: fields{ + mkdirAll: func(path string, perm os.FileMode) error { + return nil + }, + create: func(name string) (io.Writer, error) { + return &bytes.Buffer{}, nil + }, + }, + args: args{ + path: testClassicFilePath + "." + testTXTExtension, + data: testIPs, + format: "unsupported-format", + }, + want: "", + wantError: fmt.Errorf("unsupported format: %v", "unsupported-format"), + }, + { + name: "WriteTXTSuccess", + fields: fields{ + mkdirAll: func(path string, perm os.FileMode) error { + return nil + }, + create: func(name string) (io.Writer, error) { + return &bytes.Buffer{}, nil + }, + }, + args: args{ + path: testClassicFilePath + "." + testTXTExtension, + data: testIPs, + format: testTXTExtension, + }, + want: testIP1 + testIP2, + wantError: nil, + }, + { + name: "WriteTXTError", + fields: fields{ + mkdirAll: func(path string, perm os.FileMode) error { + return nil + }, + create: func(name string) (io.Writer, error) { + return &mockWriter{ + errWrite: errors.New(testErrorWriting), + }, nil + }, + }, + args: args{ + path: testClassicFilePath + "." + testTXTExtension, + data: testIPs, + format: testTXTExtension, + }, + want: "", + wantError: fmt.Errorf("error writing TXT: %v", testErrorWriting), + }, + { + name: "EncodeJSON", + fields: fields{ + mkdirAll: func(path string, perm os.FileMode) error { + return nil + }, + create: func(name string) (io.Writer, error) { + return &bytes.Buffer{}, nil + }, + }, + args: args{ + path: testClassicFilePath + "." + testJSONExtension, + data: string(testProxiesToString), + format: testJSONExtension, + }, + want: string(testProxiesToString), + wantError: nil, + }, + { + name: "EncodeJSONError", + fields: fields{ + mkdirAll: func(path string, perm os.FileMode) error { + return nil + }, + create: func(name string) (io.Writer, error) { + return &mockWriter{ + errWrite: errors.New(testErrorWriting), + }, nil + }, + }, + args: args{ + path: testClassicFilePath + "." + testJSONExtension, + data: testProxies, + format: testJSONExtension, + }, + want: "", + wantError: fmt.Errorf(testErrorEncode, "JSON", testErrorWriting), + }, + { + name: "EncodeCSVWithStringData", + fields: fields{ + mkdirAll: func(path string, perm os.FileMode) error { + return nil + }, + create: func(name string) (io.Writer, error) { + return &bytes.Buffer{}, nil + }, + csvWriter: &mockCSVWriterUtil{}, + }, + args: args{ + path: testClassicFilePath + "." + testCSVExtension, + data: testIPs, + format: testCSVExtension, + }, + want: string(testIPsToString) + "\n", + wantError: nil, + }, + { + name: "EncodeCSVWithProxyData", + fields: fields{ + mkdirAll: func(path string, perm os.FileMode) error { + return nil + }, + create: func(name string) (io.Writer, error) { + return &bytes.Buffer{}, nil + }, + csvWriter: &mockCSVWriterUtil{}, + }, + args: args{ + path: testAdvancedFilePath + "." + testCSVExtension, + data: testProxies, + format: testCSVExtension, + }, + want: string(testProxiesToString) + "\n", + wantError: nil, + }, + { + name: "EncodeCSVWithAdvancedProxyData", + fields: fields{ + mkdirAll: func(path string, perm os.FileMode) error { + return nil + }, + create: func(name string) (io.Writer, error) { + return &bytes.Buffer{}, nil + }, + csvWriter: &mockCSVWriterUtil{}, + }, + args: args{ + path: testAdvancedFilePath + "." + testCSVExtension, + data: testAdvancedProxies, + format: testCSVExtension, + }, + want: string(testAdvancedProxiesToString) + "\n", + wantError: nil, + }, + { + name: "EncodeCSVWithErrorDataType", + fields: fields{ + mkdirAll: func(path string, perm os.FileMode) error { + return nil + }, + create: func(name string) (io.Writer, error) { + return &bytes.Buffer{}, nil + }, + csvWriter: &mockCSVWriterUtil{}, + }, + args: args{ + path: testClassicFilePath + "." + testCSVExtension, + data: []error{}, + format: testCSVExtension, + }, + want: "", + wantError: errors.New("invalid data type for CSV encoding"), + }, + { + name: "EncodeXMLWithStringStruct", + fields: fields{ + mkdirAll: func(path string, perm os.FileMode) error { + return nil + }, + create: func(name string) (io.Writer, error) { + return &bytes.Buffer{}, nil + }, + }, + args: args{ + path: testClassicFilePath + "." + testXMLExtension, + data: testIPs, + format: testXMLExtension, + }, + want: string(testIPsToString), + wantError: nil, + }, + { + name: "EncodeXMLWithProxyStruct", + fields: fields{ + mkdirAll: func(path string, perm os.FileMode) error { + return nil + }, + create: func(name string) (io.Writer, error) { + return &bytes.Buffer{}, nil + }, + }, + args: args{ + path: testClassicFilePath + "." + testXMLExtension, + data: testProxies, + format: testXMLExtension, + }, + want: string(testProxiesToString), + wantError: nil, + }, + { + name: "EncodeXMLWithAdvancedProxyStruct", + fields: fields{ + mkdirAll: func(path string, perm os.FileMode) error { + return nil + }, + create: func(name string) (io.Writer, error) { + return &bytes.Buffer{}, nil + }, + }, + args: args{ + path: testClassicFilePath + "." + testXMLExtension, + data: testAdvancedProxies, + format: testXMLExtension, + }, + want: string(testAdvancedProxiesToString), + wantError: nil, + }, + { + name: "EncodeXMLError", + fields: fields{ + mkdirAll: func(path string, perm os.FileMode) error { + return nil + }, + create: func(name string) (io.Writer, error) { + return &mockWriter{ + errWrite: errors.New(testErrorWriting), + }, nil + }, + }, + args: args{ + path: testClassicFilePath + "." + testXMLExtension, + data: testProxies, + format: testXMLExtension, + }, + want: "", + wantError: fmt.Errorf(testErrorEncode, "XML", testErrorWriting), + }, + { + name: "EncodeYAMLWithStringStruct", + fields: fields{ + mkdirAll: func(path string, perm os.FileMode) error { + return nil + }, + create: func(name string) (io.Writer, error) { + return &bytes.Buffer{}, nil + }, + }, + args: args{ + path: testClassicFilePath + "." + testYAMLExtension, + data: testIPs, + format: testYAMLExtension, + }, + want: string(testIPsToString), + wantError: nil, + }, + { + name: "EncodeYAMLWithProxyStruct", + fields: fields{ + mkdirAll: func(path string, perm os.FileMode) error { + return nil + }, + create: func(name string) (io.Writer, error) { + return &bytes.Buffer{}, nil + }, + }, + args: args{ + path: testClassicFilePath + "." + testYAMLExtension, + data: testProxies, + format: testYAMLExtension, + }, + want: string(testProxiesToString), + wantError: nil, + }, + { + name: "EncodeYAMLWithAdvancedProxyStruct", + fields: fields{ + mkdirAll: func(path string, perm os.FileMode) error { + return nil + }, + create: func(name string) (io.Writer, error) { + return &bytes.Buffer{}, nil + }, + }, + args: args{ + path: testClassicFilePath + "." + testYAMLExtension, + data: testAdvancedProxies, + format: testYAMLExtension, + }, + want: string(testAdvancedProxiesToString), + wantError: nil, + }, + { + name: "EncodeYAMLError", + fields: fields{ + mkdirAll: func(path string, perm os.FileMode) error { + return nil + }, + create: func(name string) (io.Writer, error) { + return &mockWriter{ + errWrite: errors.New(testErrorWriting), + }, nil + }, + }, + args: args{ + path: testClassicFilePath + "." + testYAMLExtension, + data: testProxies, + format: testYAMLExtension, + }, + want: "", + wantError: fmt.Errorf(testErrorEncode, "YAML", "yaml: write error: error writing"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &repository.FileRepository{ + MkdirAll: tt.fields.mkdirAll, + Create: tt.fields.create, + CSVWriter: tt.fields.csvWriter, + } + err := r.SaveFile(tt.args.path, tt.args.data, tt.args.format) + if (err != nil && tt.wantError != nil && err.Error() != tt.wantError.Error()) || + (err == nil && tt.wantError != nil) || + (err != nil && tt.wantError == nil) { + t.Errorf(expectedErrorButGotMessage, "SaveFile()", tt.wantError, err) + } + }) + } +} + +func TestWriteCSV(t *testing.T) { + type fields struct { + csvWriter utils.CSVWriterUtilInterface + } + + type args struct { + header []string + rows [][]string + } + + tests := []struct { + name string + fields fields + args args + wantError error + }{ + { + name: "Success", + fields: fields{ + csvWriter: &mockCSVWriterUtil{}, + }, + args: args{ + header: []string{column1, column2}, + rows: [][]string{ + {row1Column1, row1Column2}, + {row2Column1, row2Column2}, + }, + }, + wantError: nil, + }, + { + name: "ErrorWritingHeader", + fields: fields{ + csvWriter: &mockCSVWriterUtil{ + errWrite: errors.New("write header error"), + }, + }, + args: args{ + header: []string{column1, column2}, + rows: [][]string{ + {row1Column1, row1Column2}, + {row2Column1, row2Column2}, + }, + }, + wantError: fmt.Errorf("failed to write header: %w", errors.New("write header error")), + }, + { + name: "ErrorWritingRow", + fields: fields{ + csvWriter: &mockCSVWriterUtil{ + errWrite: errors.New("write row error"), + }, + }, + args: args{ + header: nil, + rows: [][]string{ + {row1Column1, row1Column2}, + {row2Column1, row2Column2}, + }, + }, + wantError: fmt.Errorf("failed to write row: %w", errors.New("write row error")), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var buf bytes.Buffer + r := &repository.FileRepository{ + CSVWriter: tt.fields.csvWriter, + } + err := r.WriteCSV(&buf, tt.args.header, tt.args.rows) + if (err != nil && tt.wantError != nil && err.Error() != tt.wantError.Error()) || + (err == nil && tt.wantError != nil) || + (err != nil && tt.wantError == nil) { + t.Errorf(expectedErrorButGotMessage, "WriteCSV()", tt.wantError, err) + } + }) + } +} diff --git a/test/unit/internal/infrastructure/repository/proxy_repository_test.go b/test/unit/internal/infrastructure/repository/proxy_repository_test.go new file mode 100644 index 00000000..51c07333 --- /dev/null +++ b/test/unit/internal/infrastructure/repository/proxy_repository_test.go @@ -0,0 +1,772 @@ +package repository_test + +import ( + "reflect" + "testing" + + "github.com/fyvri/fresh-proxy-list/internal/entity" + "github.com/fyvri/fresh-proxy-list/internal/infrastructure/repository" +) + +func TestNewProxyRepository(t *testing.T) { + tests := []struct { + name string + want repository.ProxyRepositoryInterface + }{ + { + name: "Success", + want: &repository.ProxyRepository{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + proxyRepository := repository.NewProxyRepository() + + if proxyRepository == nil { + t.Errorf(expectedReturnNonNil, "NewProxyRepository", "ProxyRepositoryInterface") + } + + got, ok := proxyRepository.(*repository.ProxyRepository) + if !ok { + t.Errorf(expectedTypeAssertionErrorMessage, "*repository.ProxyRepository") + } + + if !reflect.DeepEqual(tt.want, got) { + t.Errorf(expectedButGotMessage, "*repository.ProxyRepository", tt.want, got) + } + }) + } +} + +func TestProxyRepository(t *testing.T) { + type fields struct { + allClassicView []string + httpClassicView []string + httpsClassicView []string + socks4ClassicView []string + socks5ClassicView []string + allAdvancedView []entity.AdvancedProxy + httpAdvancedView []entity.Proxy + httpsAdvancedView []entity.Proxy + socks4AdvancedView []entity.Proxy + socks5AdvancedView []entity.Proxy + } + + type args struct { + proxy entity.Proxy + } + + tests := []struct { + name string + fields fields + args args + want fields + wantErr error + }{ + { + name: "StoreHTTPProxy", + fields: fields{ + allClassicView: []string{}, + httpClassicView: []string{}, + httpsClassicView: []string{}, + socks4ClassicView: []string{}, + socks5ClassicView: []string{}, + allAdvancedView: []entity.AdvancedProxy{}, + httpAdvancedView: []entity.Proxy{}, + httpsAdvancedView: []entity.Proxy{}, + socks4AdvancedView: []entity.Proxy{}, + socks5AdvancedView: []entity.Proxy{}, + }, + args: args{ + proxy: testProxyEntity1, + }, + want: fields{ + allClassicView: []string{ + testProxy1, + }, + httpClassicView: []string{ + testProxy1, + }, + httpsClassicView: []string{}, + socks4ClassicView: []string{}, + socks5ClassicView: []string{}, + allAdvancedView: []entity.AdvancedProxy{ + testAdvancedProxyEntity1, + }, + httpAdvancedView: []entity.Proxy{ + testProxyEntity1, + }, + httpsAdvancedView: []entity.Proxy{}, + socks4AdvancedView: []entity.Proxy{}, + socks5AdvancedView: []entity.Proxy{}, + }, + wantErr: nil, + }, + { + name: "StoreHTTPSProxy", + fields: fields{ + allClassicView: []string{}, + httpClassicView: []string{}, + httpsClassicView: []string{}, + socks4ClassicView: []string{}, + socks5ClassicView: []string{}, + allAdvancedView: []entity.AdvancedProxy{}, + httpAdvancedView: []entity.Proxy{}, + httpsAdvancedView: []entity.Proxy{}, + socks4AdvancedView: []entity.Proxy{}, + socks5AdvancedView: []entity.Proxy{}, + }, + args: args{ + proxy: testProxyEntity2, + }, + want: fields{ + allClassicView: []string{ + testProxy2, + }, + httpClassicView: []string{}, + httpsClassicView: []string{ + testProxy2, + }, + socks4ClassicView: []string{}, + socks5ClassicView: []string{}, + allAdvancedView: []entity.AdvancedProxy{ + testAdvancedProxyEntity2, + }, + httpAdvancedView: []entity.Proxy{}, + httpsAdvancedView: []entity.Proxy{ + testProxyEntity2, + }, + socks4AdvancedView: []entity.Proxy{}, + socks5AdvancedView: []entity.Proxy{}, + }, + wantErr: nil, + }, + { + name: "StoreSOCKS4Proxy", + fields: fields{ + allClassicView: []string{}, + httpClassicView: []string{}, + httpsClassicView: []string{}, + socks4ClassicView: []string{}, + socks5ClassicView: []string{}, + allAdvancedView: []entity.AdvancedProxy{}, + httpAdvancedView: []entity.Proxy{}, + httpsAdvancedView: []entity.Proxy{}, + socks4AdvancedView: []entity.Proxy{}, + socks5AdvancedView: []entity.Proxy{}, + }, + args: args{ + proxy: testProxyEntity3, + }, + want: fields{ + allClassicView: []string{ + testProxy3, + }, + httpClassicView: []string{}, + httpsClassicView: []string{}, + socks4ClassicView: []string{ + testProxy3, + }, + socks5ClassicView: []string{}, + allAdvancedView: []entity.AdvancedProxy{ + testAdvancedProxyEntity3, + }, + httpAdvancedView: []entity.Proxy{}, + httpsAdvancedView: []entity.Proxy{}, + socks4AdvancedView: []entity.Proxy{ + testProxyEntity3, + }, + socks5AdvancedView: []entity.Proxy{}, + }, + wantErr: nil, + }, + { + name: "StoreSOCKS5Proxy", + fields: fields{ + allClassicView: []string{}, + httpClassicView: []string{}, + httpsClassicView: []string{}, + socks4ClassicView: []string{}, + socks5ClassicView: []string{}, + allAdvancedView: []entity.AdvancedProxy{}, + httpAdvancedView: []entity.Proxy{}, + httpsAdvancedView: []entity.Proxy{}, + socks4AdvancedView: []entity.Proxy{}, + socks5AdvancedView: []entity.Proxy{}, + }, + args: args{ + proxy: testProxyEntity4, + }, + want: fields{ + allClassicView: []string{ + testProxy4, + }, + httpClassicView: []string{}, + httpsClassicView: []string{}, + socks4ClassicView: []string{}, + socks5ClassicView: []string{ + testProxy4, + }, + allAdvancedView: []entity.AdvancedProxy{ + testAdvancedProxyEntity4, + }, + httpAdvancedView: []entity.Proxy{}, + httpsAdvancedView: []entity.Proxy{}, + socks4AdvancedView: []entity.Proxy{}, + socks5AdvancedView: []entity.Proxy{ + testProxyEntity4, + }, + }, + wantErr: nil, + }, + { + name: "DuplicatedProxyWithinHTTPCategoryAndDifferentCategory", + fields: fields{ + allClassicView: []string{ + testProxy3, + testProxy4, + }, + httpClassicView: []string{}, + httpsClassicView: []string{}, + socks4ClassicView: []string{ + testProxy3, + }, + socks5ClassicView: []string{ + testProxy4, + }, + allAdvancedView: []entity.AdvancedProxy{ + testAdvancedProxyEntity3, + testAdvancedProxyEntity4, + }, + httpAdvancedView: []entity.Proxy{}, + httpsAdvancedView: []entity.Proxy{}, + socks4AdvancedView: []entity.Proxy{ + testProxyEntity3, + }, + socks5AdvancedView: []entity.Proxy{ + testProxyEntity4, + }, + }, + args: args{ + proxy: entity.Proxy{ + Category: testHTTPCategory, + IP: testIP4, + Port: testPort4, + Proxy: testProxy4, + TimeTaken: testTimeTaken, + CheckedAt: testCheckedAt, + }, + }, + want: fields{ + allClassicView: []string{ + testProxy3, + testProxy4, + }, + httpClassicView: []string{ + testProxy4, + }, + httpsClassicView: []string{}, + socks4ClassicView: []string{ + testProxy3, + }, + socks5ClassicView: []string{ + testProxy4, + }, + allAdvancedView: []entity.AdvancedProxy{ + testAdvancedProxyEntity3, + { + Proxy: testAdvancedProxyEntity4.Proxy, + IP: testAdvancedProxyEntity4.IP, + Port: testAdvancedProxyEntity4.Port, + TimeTaken: testTimeTaken, + CheckedAt: testAdvancedProxyEntity4.CheckedAt, + Categories: []string{ + testHTTPCategory, + testProxyEntity4.Category, + }, + }, + }, + httpAdvancedView: []entity.Proxy{ + { + Category: testHTTPCategory, + IP: testIP4, + Port: testPort4, + Proxy: testProxy4, + TimeTaken: testTimeTaken, + CheckedAt: testCheckedAt, + }, + }, + httpsAdvancedView: []entity.Proxy{}, + socks4AdvancedView: []entity.Proxy{ + testProxyEntity3, + }, + socks5AdvancedView: []entity.Proxy{ + testProxyEntity4, + }, + }, + wantErr: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &repository.ProxyRepository{ + AllClassicView: tt.fields.allClassicView, + HTTPClassicView: tt.fields.httpClassicView, + HTTPSClassicView: tt.fields.httpsClassicView, + SOCKS4ClassicView: tt.fields.socks4ClassicView, + SOCKS5ClassicView: tt.fields.socks5ClassicView, + AllAdvancedView: tt.fields.allAdvancedView, + HTTPAdvancedView: tt.fields.httpAdvancedView, + HTTPSAdvancedView: tt.fields.httpsAdvancedView, + SOCKS4AdvancedView: tt.fields.socks4AdvancedView, + SOCKS5AdvancedView: tt.fields.socks5AdvancedView, + } + r.Store(&tt.args.proxy) + + views := map[string]struct { + got interface{} + want interface{} + }{ + "GetAllClassicView()": {r.AllClassicView, tt.want.allClassicView}, + "GetHTTPClassicView()": {r.HTTPClassicView, tt.want.httpClassicView}, + "GetHTTPSClassicView()": {r.HTTPSClassicView, tt.want.httpsClassicView}, + "GetSOCKS4ClassicView()": {r.SOCKS4ClassicView, tt.want.socks4ClassicView}, + "GetSOCKS5ClassicView()": {r.SOCKS5ClassicView, tt.want.socks5ClassicView}, + "GetAllAdvancedView()": {r.AllAdvancedView, tt.want.allAdvancedView}, + "GetHTTPAdvancedView()": {r.HTTPAdvancedView, tt.want.httpAdvancedView}, + "GetHTTPSAdvancedView()": {r.HTTPSAdvancedView, tt.want.httpsAdvancedView}, + "GetSOCKS4AdvancedView()": {r.SOCKS4AdvancedView, tt.want.socks4AdvancedView}, + "GetSOCKS5AdvancedView()": {r.SOCKS5AdvancedView, tt.want.socks5AdvancedView}, + } + for name, v := range views { + if !reflect.DeepEqual(v.got, v.want) { + t.Errorf(expectedButGotMessage, name, v.want, v.got) + } + } + }) + } +} + +func TestGetAllClassicView(t *testing.T) { + tests := []struct { + name string + setup func() *repository.ProxyRepository + want []string + }{ + { + name: "EmptyAllProxies", + setup: func() *repository.ProxyRepository { + return &repository.ProxyRepository{ + AllClassicView: []string{}, + } + }, + want: []string{}, + }, + { + name: "WithAllProxies", + setup: func() *repository.ProxyRepository { + r := &repository.ProxyRepository{} + r.AllClassicView = []string{ + testProxy1, + testProxy2, + testProxy3, + testProxy4, + } + return r + }, + want: []string{ + testProxy1, + testProxy2, + testProxy3, + testProxy4, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := tt.setup() + got := r.GetAllClassicView() + if !reflect.DeepEqual(got, tt.want) { + t.Errorf(expectedButGotMessage, "GetAllClassicView()", tt.want, got) + } + }) + } +} + +func TestGetHTTPClassicView(t *testing.T) { + tests := []struct { + name string + setup func() *repository.ProxyRepository + want []string + }{ + { + name: "EmptyHTTPProxies", + setup: func() *repository.ProxyRepository { + return &repository.ProxyRepository{ + HTTPClassicView: []string{}, + } + }, + want: []string{}, + }, + { + name: "WithHTTPProxies", + setup: func() *repository.ProxyRepository { + r := &repository.ProxyRepository{} + r.HTTPClassicView = []string{ + testProxy1, + } + return r + }, + want: []string{ + testProxy1, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := tt.setup() + got := r.GetHTTPClassicView() + if !reflect.DeepEqual(got, tt.want) { + t.Errorf(expectedButGotMessage, "GetHTTPClassicView()", tt.want, got) + } + }) + } +} + +func TestGetHTTPSClassicView(t *testing.T) { + tests := []struct { + name string + setup func() *repository.ProxyRepository + want []string + }{ + { + name: "EmptyHTTPSProxies", + setup: func() *repository.ProxyRepository { + return &repository.ProxyRepository{ + HTTPSClassicView: []string{}, + } + }, + want: []string{}, + }, + { + name: "WithHTTPSProxies", + setup: func() *repository.ProxyRepository { + r := &repository.ProxyRepository{} + r.HTTPSClassicView = []string{ + testProxy2, + } + return r + }, + want: []string{ + testProxy2, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := tt.setup() + got := r.GetHTTPSClassicView() + if !reflect.DeepEqual(got, tt.want) { + t.Errorf(expectedButGotMessage, "GetHTTPSClassicView()", tt.want, got) + } + }) + } +} + +func TestGetSOCKS4ClassicView(t *testing.T) { + tests := []struct { + name string + setup func() *repository.ProxyRepository + want []string + }{ + { + name: "EmptySOCKS4Proxies", + setup: func() *repository.ProxyRepository { + r := &repository.ProxyRepository{} + r.SOCKS4ClassicView = []string{} + return r + }, + want: []string{}, + }, + { + name: "WithSOCKS4Proxies", + setup: func() *repository.ProxyRepository { + return &repository.ProxyRepository{ + SOCKS4ClassicView: []string{ + testProxy3, + }, + } + }, + want: []string{ + testProxy3, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := tt.setup() + got := r.GetSOCKS4ClassicView() + if !reflect.DeepEqual(got, tt.want) { + t.Errorf(expectedButGotMessage, "GetSOCKS4ClassicView()", tt.want, got) + } + }) + } +} + +func TestGetSOCKS5ClassicView(t *testing.T) { + tests := []struct { + name string + setup func() *repository.ProxyRepository + want []string + }{ + { + name: "EmptySOCKS5Proxies", + setup: func() *repository.ProxyRepository { + return &repository.ProxyRepository{ + SOCKS5ClassicView: []string{}, + } + }, + want: []string{}, + }, + { + name: "WithSOCKS5Proxies", + setup: func() *repository.ProxyRepository { + r := &repository.ProxyRepository{} + r.SOCKS5ClassicView = []string{ + testProxy4, + } + return r + }, + want: []string{ + testProxy4, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := tt.setup() + got := r.GetSOCKS5ClassicView() + if !reflect.DeepEqual(got, tt.want) { + t.Errorf(expectedButGotMessage, "GetSOCKS5ClassicView()", tt.want, got) + } + }) + } +} + +func TestGetAllAdvancedView(t *testing.T) { + tests := []struct { + name string + setup func() *repository.ProxyRepository + want []entity.AdvancedProxy + }{ + { + name: "EmptyAllProxies", + setup: func() *repository.ProxyRepository { + return &repository.ProxyRepository{ + AllAdvancedView: []entity.AdvancedProxy{}, + } + }, + want: []entity.AdvancedProxy{}, + }, + { + name: "WithAllProxies", + setup: func() *repository.ProxyRepository { + r := &repository.ProxyRepository{} + r.AllAdvancedView = []entity.AdvancedProxy{ + testAdvancedProxyEntity1, + testAdvancedProxyEntity2, + testAdvancedProxyEntity3, + testAdvancedProxyEntity4, + } + return r + }, + want: []entity.AdvancedProxy{ + testAdvancedProxyEntity1, + testAdvancedProxyEntity2, + testAdvancedProxyEntity3, + testAdvancedProxyEntity4, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := tt.setup() + got := r.GetAllAdvancedView() + if !reflect.DeepEqual(got, tt.want) { + t.Errorf(expectedButGotMessage, "GetAllAdvancedView()", tt.want, got) + } + }) + } +} + +func TestGetHTTPAdvancedView(t *testing.T) { + tests := []struct { + name string + setup func() *repository.ProxyRepository + want []entity.Proxy + }{ + { + name: "EmptyHTTPProxies", + setup: func() *repository.ProxyRepository { + return &repository.ProxyRepository{ + HTTPAdvancedView: []entity.Proxy{}, + } + }, + want: []entity.Proxy{}, + }, + { + name: "WithHTTPProxies", + setup: func() *repository.ProxyRepository { + r := &repository.ProxyRepository{} + r.HTTPAdvancedView = []entity.Proxy{ + testProxyEntity1, + } + return r + }, + want: []entity.Proxy{ + testProxyEntity1, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := tt.setup() + got := r.GetHTTPAdvancedView() + if !reflect.DeepEqual(got, tt.want) { + t.Errorf(expectedButGotMessage, "GetHTTPAdvancedView()", tt.want, got) + } + }) + } +} + +func TestGetHTTPSAdvancedView(t *testing.T) { + tests := []struct { + name string + setup func() *repository.ProxyRepository + want []entity.Proxy + }{ + { + name: "EmptyHTTPSProxies", + setup: func() *repository.ProxyRepository { + return &repository.ProxyRepository{ + HTTPSAdvancedView: []entity.Proxy{}, + } + }, + want: []entity.Proxy{}, + }, + { + name: "WithHTTPSProxies", + setup: func() *repository.ProxyRepository { + r := &repository.ProxyRepository{} + r.HTTPSAdvancedView = []entity.Proxy{ + testProxyEntity2, + } + return r + }, + want: []entity.Proxy{ + testProxyEntity2, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := tt.setup() + got := r.GetHTTPSAdvancedView() + if !reflect.DeepEqual(got, tt.want) { + t.Errorf(expectedButGotMessage, "GetHTTPSAdvancedView()", tt.want, got) + } + }) + } +} + +func TestGetSOCKS4AdvancedView(t *testing.T) { + tests := []struct { + name string + setup func() *repository.ProxyRepository + want []entity.Proxy + }{ + { + name: "EmptySOCKS4Proxies", + setup: func() *repository.ProxyRepository { + return &repository.ProxyRepository{ + SOCKS4AdvancedView: []entity.Proxy{}, + } + }, + want: []entity.Proxy{}, + }, + { + name: "WithSOCKSProxies", + setup: func() *repository.ProxyRepository { + r := &repository.ProxyRepository{} + r.SOCKS4AdvancedView = []entity.Proxy{ + testProxyEntity3, + } + return r + }, + want: []entity.Proxy{ + testProxyEntity3, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := tt.setup() + got := r.GetSOCKS4AdvancedView() + if !reflect.DeepEqual(got, tt.want) { + t.Errorf(expectedButGotMessage, "GetSOCKS4AdvancedView()", tt.want, got) + } + }) + } +} + +func TestGetSOCKS5AdvancedView(t *testing.T) { + tests := []struct { + name string + setup func() *repository.ProxyRepository + want []entity.Proxy + }{ + { + name: "EmptySOCKS5Proxies", + setup: func() *repository.ProxyRepository { + return &repository.ProxyRepository{ + SOCKS5AdvancedView: []entity.Proxy{}, + } + }, + want: []entity.Proxy{}, + }, + { + name: "WithSOCKS5Proxies", + setup: func() *repository.ProxyRepository { + r := &repository.ProxyRepository{} + r.SOCKS5AdvancedView = []entity.Proxy{ + testProxyEntity4, + } + return r + }, + want: []entity.Proxy{ + testProxyEntity4, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := tt.setup() + got := r.GetSOCKS5AdvancedView() + if !reflect.DeepEqual(got, tt.want) { + t.Errorf(expectedButGotMessage, "GetSOCKS5AdvancedView()", tt.want, got) + } + }) + } +} diff --git a/test/unit/internal/infrastructure/repository/repository_test.go b/test/unit/internal/infrastructure/repository/repository_test.go new file mode 100644 index 00000000..4d1f79c9 --- /dev/null +++ b/test/unit/internal/infrastructure/repository/repository_test.go @@ -0,0 +1,190 @@ +package repository_test + +import ( + "encoding/csv" + "encoding/json" + "io" + "time" + + "github.com/fyvri/fresh-proxy-list/internal/entity" +) + +var ( + expectedButGotMessage = "Expected %v = %v, but got = %v" + expectedErrorButGotMessage = "Expected %v error = %v, but got = %v" + expectedTypeAssertionErrorMessage = "Expected type assertion error, but got = %v" + expectedReturnNonNil = "Expected %v to return a non-nil %v" + testErrorWriting = "error writing" + testErrorEncode = "error encoding %s: %s" + testStorageDir = "/tmp" + testClassicDir = "/classic" + testAdvancedDir = "/advanced" + testClassicFilePath = testStorageDir + testClassicDir + "/test_file" + testAdvancedFilePath = testStorageDir + testAdvancedDir + "/test_file" + testTXTExtension = "txt" + testCSVExtension = "csv" + testJSONExtension = "json" + testXMLExtension = "xml" + testYAMLExtension = "yaml" + testHTTPCategory = "HTTP" + testHTTPSCategory = "HTTPS" + testSOCKS4Category = "SOCKS4" + testSOCKS5Category = "SOCKS5" + testTimeTaken = 1.2345 + testCheckedAt = "2024-07-27T00:00:00Z" + + testIP1 = "13.37.0.1" + testPort1 = "1337" + testProxy1 = testIP1 + ":" + testPort1 + testCategory1 = testHTTPCategory + testProxyEntity1 = entity.Proxy{ + Proxy: testProxy1, + IP: testIP1, + Port: testPort1, + Category: testCategory1, + TimeTaken: 0, + CheckedAt: time.Now().Format(time.RFC3339), + } + testAdvancedProxyEntity1 = entity.AdvancedProxy{ + Proxy: testProxyEntity1.Proxy, + IP: testProxyEntity1.IP, + Port: testProxyEntity1.Port, + TimeTaken: testProxyEntity1.TimeTaken, + CheckedAt: testProxyEntity1.CheckedAt, + Categories: []string{ + testCategory1, + }, + } + + testIP2 = "13.37.0.2" + testPort2 = "1337" + testProxy2 = testIP2 + ":" + testPort2 + testCategory2 = testHTTPSCategory + testProxyEntity2 = entity.Proxy{ + Proxy: testProxy2, + IP: testIP2, + Port: testPort2, + Category: testCategory2, + TimeTaken: 0, + CheckedAt: time.Now().Format(time.RFC3339), + } + testAdvancedProxyEntity2 = entity.AdvancedProxy{ + Proxy: testProxyEntity2.Proxy, + IP: testProxyEntity2.IP, + Port: testProxyEntity2.Port, + TimeTaken: testProxyEntity2.TimeTaken, + CheckedAt: testProxyEntity2.CheckedAt, + Categories: []string{ + testCategory2, + }, + } + + testIP3 = "13.37.0.3" + testPort3 = "1337" + testProxy3 = testIP3 + ":" + testPort3 + testCategory3 = testSOCKS4Category + testProxyEntity3 = entity.Proxy{ + Proxy: testProxy3, + IP: testIP3, + Port: testPort3, + Category: testCategory3, + TimeTaken: 0, + CheckedAt: time.Now().Format(time.RFC3339), + } + testAdvancedProxyEntity3 = entity.AdvancedProxy{ + Proxy: testProxyEntity3.Proxy, + IP: testProxyEntity3.IP, + Port: testProxyEntity3.Port, + TimeTaken: testProxyEntity3.TimeTaken, + CheckedAt: testProxyEntity3.CheckedAt, + Categories: []string{ + testCategory3, + }, + } + + testIP4 = "13.37.0.4" + testPort4 = "1337" + testProxy4 = testIP4 + ":" + testPort4 + testCategory4 = testSOCKS5Category + testProxyEntity4 = entity.Proxy{ + Proxy: testProxy4, + IP: testIP4, + Port: testPort4, + Category: testCategory4, + TimeTaken: 0, + CheckedAt: time.Now().Format(time.RFC3339), + } + testAdvancedProxyEntity4 = entity.AdvancedProxy{ + Proxy: testProxyEntity4.Proxy, + IP: testProxyEntity4.IP, + Port: testProxyEntity4.Port, + TimeTaken: testProxyEntity4.TimeTaken, + CheckedAt: testProxyEntity4.CheckedAt, + Categories: []string{ + testCategory4, + }, + } + + testIPs = []string{ + testIP1, + testIP2, + testIP3, + testIP4, + } + testProxies = []entity.Proxy{ + testProxyEntity1, + testProxyEntity2, + testProxyEntity3, + testProxyEntity4, + } + testAdvancedProxies = []entity.AdvancedProxy{ + testAdvancedProxyEntity1, + testAdvancedProxyEntity2, + testAdvancedProxyEntity3, + testAdvancedProxyEntity4, + } + testIPsToString, _ = json.Marshal(testIPs) + testProxiesToString, _ = json.Marshal(testProxies) + testAdvancedProxiesToString, _ = json.Marshal(testAdvancedProxies) +) + +type mockWriter struct { + errWrite error + errClose error +} + +func (m *mockWriter) Write(p []byte) (int, error) { + if m.errWrite != nil { + return 0, m.errWrite + } + return len(p), nil +} + +func (m *mockWriter) Close() error { + if m.errClose != nil { + return m.errClose + } + return nil +} + +type mockCSVWriterUtil struct { + errFlush error + errWrite error +} + +func (m *mockCSVWriterUtil) Init(w io.Writer) *csv.Writer { + return csv.NewWriter(w) +} + +func (m *mockCSVWriterUtil) Flush(csvWriter *csv.Writer) { + if m.errFlush != nil { + return + } +} + +func (m *mockCSVWriterUtil) Write(csvWriter *csv.Writer, record []string) error { + if m.errWrite != nil { + return m.errWrite + } + return nil +} diff --git a/test/unit/internal/infrastructure/repository/source_repository_test.go b/test/unit/internal/infrastructure/repository/source_repository_test.go new file mode 100644 index 00000000..f8e9afa0 --- /dev/null +++ b/test/unit/internal/infrastructure/repository/source_repository_test.go @@ -0,0 +1,113 @@ +package repository_test + +import ( + "errors" + "reflect" + "testing" + + "github.com/fyvri/fresh-proxy-list/internal/entity" + "github.com/fyvri/fresh-proxy-list/internal/infrastructure/repository" +) + +func TestNewSourceRepository(t *testing.T) { + type args struct { + proxy_resources string + } + + tests := []struct { + name string + args args + want repository.SourceRepositoryInterface + }{ + { + name: "Success", + args: args{ + proxy_resources: "", + }, + want: &repository.SourceRepository{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sourceRepository := repository.NewSourceRepository(tt.args.proxy_resources) + + if sourceRepository == nil { + t.Errorf(expectedReturnNonNil, "NewSourceRepository", "SourceRepositoryInterface") + } + + got, ok := sourceRepository.(*repository.SourceRepository) + if !ok { + t.Errorf(expectedTypeAssertionErrorMessage, "*repository.SourceRepository") + } + + if !reflect.DeepEqual(tt.want, got) { + t.Errorf(expectedButGotMessage, "*repository.SourceRepository", tt.want, got) + } + }) + } +} + +func TestLoadSources(t *testing.T) { + type args struct { + proxy_resources string + } + + tests := []struct { + name string + args args + want []entity.Source + wantErr error + }{ + { + name: "EmptyResources", + args: args{ + proxy_resources: "", + }, + want: nil, + wantErr: errors.New("PROXY_RESOURCES not found on environment"), + }, + { + name: "InvalidJSON", + args: args{ + proxy_resources: `{"invalid": "json"`, + }, + want: nil, + wantErr: errors.New("error parsing JSON: unexpected end of JSON input"), + }, + { + name: "ValidJSON", + args: args{ + proxy_resources: `[{"method": "GET", "category": "general", "url": "http://example.com", "is_checked": true}]`, + }, + want: []entity.Source{ + { + Method: "GET", + Category: "general", + URL: "http://example.com", + IsChecked: true, + }, + }, + wantErr: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &repository.SourceRepository{ + ProxyResources: tt.args.proxy_resources, + } + got, err := r.LoadSources() + + if !reflect.DeepEqual(got, tt.want) { + t.Errorf(expectedButGotMessage, "LoadSources()", tt.want, got) + } + + if (err != nil && tt.wantErr != nil && err.Error() != tt.wantErr.Error()) || + (err != nil && tt.wantErr == nil) || + (err == nil && tt.wantErr != nil) { + t.Errorf(expectedErrorButGotMessage, "LoadSources()", tt.wantErr, err) + } + }) + } +} diff --git a/test/unit/internal/service/proxy_service_test.go b/test/unit/internal/service/proxy_service_test.go new file mode 100644 index 00000000..815631b2 --- /dev/null +++ b/test/unit/internal/service/proxy_service_test.go @@ -0,0 +1,335 @@ +package service_test + +import ( + "errors" + "fmt" + "io" + "net/http" + "net/http/httptest" + "net/url" + "reflect" + "testing" + "time" + + "github.com/fyvri/fresh-proxy-list/internal/entity" + "github.com/fyvri/fresh-proxy-list/internal/service" + "github.com/fyvri/fresh-proxy-list/pkg/utils" +) + +var ( + expectedButGotMessage = "Expected %v = %v, but got = %v" + expectedErrorButGotMessage = "Expected %v error = %v, but got = %v" + expectedNonEmptyMessage = "Expected non-empty %v from %v" + expectedReturnNonNil = "Expected %v to return a non-nil %v" + expectedTypeAssertionErrorMessage = "Expected type assertion error, but got = %v" + testIP = "13.37.0.1" + testPort = "8080" + testProxy = testIP + ":" + testPort + testHTTPCategory = "HTTP" + testHTTPSCategory = "HTTPS" + testSOCKS4Category = "SOCKS4" + testHTTPTestingSites = []string{"http://test1.com", "http://test2.com"} + testHTTPSTestingSites = []string{"https://secure1.com", "https://secure2.com"} + testUserAgents = []string{"Mozilla", "Chrome", "Safari"} +) + +type mockURLParserUtil struct { + ParseFunc func(urlStr string) (*url.URL, error) +} + +func (m *mockURLParserUtil) Parse(urlStr string) (*url.URL, error) { + if m.ParseFunc != nil { + return m.ParseFunc(urlStr) + } + return url.Parse(urlStr) +} + +type mockFetcherUtil struct { + fetchDataByte []byte + fetcherError error + NewRequestFunc func(method, url string, body io.Reader) (*http.Request, error) + DoFunc func(client *http.Client, req *http.Request) (*http.Response, error) +} + +func (m *mockFetcherUtil) FetchData(url string) ([]byte, error) { + if m.fetcherError != nil { + return nil, m.fetcherError + } + return m.fetchDataByte, nil +} + +func (m *mockFetcherUtil) Do(client *http.Client, req *http.Request) (*http.Response, error) { + if m.DoFunc != nil { + return m.DoFunc(client, req) + } + return httptest.NewRecorder().Result(), nil +} + +func (m *mockFetcherUtil) NewRequest(method string, url string, body io.Reader) (*http.Request, error) { + if m.NewRequestFunc != nil { + return m.NewRequestFunc(method, url, body) + } + return http.NewRequest(method, url, body) +} + +func TestNewProxyService(t *testing.T) { + proxyService := service.NewProxyService(&mockFetcherUtil{}, &mockURLParserUtil{}, testHTTPTestingSites, testHTTPSTestingSites, testUserAgents) + if proxyService == nil { + t.Errorf(expectedReturnNonNil, "NewProxyService", "ProxyServiceInterface") + } + + s, ok := proxyService.(*service.ProxyService) + if !ok { + t.Errorf(expectedTypeAssertionErrorMessage, "*service.ProxyService") + } + + if !reflect.DeepEqual(s.HTTPTestingSites, testHTTPTestingSites) { + t.Errorf(expectedButGotMessage, "HTTPTestingSites", testHTTPTestingSites, s.HTTPTestingSites) + } + + if !reflect.DeepEqual(s.HTTPSTestingSites, testHTTPSTestingSites) { + t.Errorf(expectedButGotMessage, "HTTPSTestingSites", testHTTPSTestingSites, s.HTTPSTestingSites) + } + + if !reflect.DeepEqual(s.UserAgents, testUserAgents) { + t.Errorf(expectedButGotMessage, "UserAgents", testUserAgents, s.UserAgents) + } +} + +func TestCheck(t *testing.T) { + type fields struct { + fetcherUtil utils.FetcherUtilInterface + urlParserUtil utils.URLParserUtilInterface + } + + type args struct { + category string + ip string + port string + } + + tests := []struct { + name string + fields fields + args args + want *entity.Proxy + wantError error + }{ + { + name: "TestValid", + fields: fields{ + fetcherUtil: &mockFetcherUtil{}, + urlParserUtil: &mockURLParserUtil{}, + }, + args: args{ + category: testHTTPSCategory, + ip: testIP, + port: testPort, + }, + want: &entity.Proxy{ + Category: testHTTPSCategory, + Proxy: testProxy, + IP: testIP, + Port: testPort, + TimeTaken: 123.45, + CheckedAt: time.Now().Format(time.RFC3339), + }, + wantError: nil, + }, + { + name: "TestErrorParseURL", + fields: fields{ + fetcherUtil: &mockFetcherUtil{}, + urlParserUtil: &mockURLParserUtil{ + ParseFunc: func(urlStr string) (*url.URL, error) { + return nil, errors.New("parse error") + }, + }, + }, + args: args{ + category: testHTTPCategory, + ip: testIP, + port: testPort, + }, + want: nil, + wantError: errors.New("error parsing proxy URL: parse error"), + }, + { + name: "TestCreatingRequest", + fields: fields{ + fetcherUtil: &mockFetcherUtil{ + NewRequestFunc: func(method, url string, body io.Reader) (*http.Request, error) { + return nil, errors.New("error creating request") + }, + }, + }, + args: args{ + category: testSOCKS4Category, + ip: testIP, + port: testPort, + }, + want: nil, + wantError: errors.New("error creating request: error creating request"), + }, + { + name: "TestUnsupportedProxyCategory", + fields: fields{}, + args: args{ + category: "FTP", + ip: testIP, + port: testPort, + }, + want: nil, + wantError: errors.New("proxy category FTP not supported"), + }, + { + name: "TestRequestError", + fields: fields{ + fetcherUtil: &mockFetcherUtil{ + DoFunc: func(client *http.Client, req *http.Request) (*http.Response, error) { + return nil, fmt.Errorf("network error") + }, + }, + urlParserUtil: &mockURLParserUtil{}, + }, + args: args{ + category: testHTTPCategory, + ip: testIP, + port: testPort, + }, + want: nil, + wantError: errors.New("request error: network error"), + }, + { + name: "TestUnexpectedStatusCode", + fields: fields{ + fetcherUtil: &mockFetcherUtil{ + DoFunc: func(client *http.Client, req *http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: http.StatusInternalServerError, + Body: http.NoBody, + }, nil + }, + }, + urlParserUtil: &mockURLParserUtil{}, + }, + args: args{ + category: testHTTPCategory, + ip: testIP, + port: testPort, + }, + want: nil, + wantError: errors.New("unexpected status code 500: Internal Server Error"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &service.ProxyService{ + FetcherUtil: tt.fields.fetcherUtil, + URLParserUtil: tt.fields.urlParserUtil, + HTTPTestingSites: testHTTPTestingSites, + HTTPSTestingSites: testHTTPSTestingSites, + UserAgents: testUserAgents, + Semaphore: make(chan struct{}, 10), + } + got, err := s.Check(tt.args.category, tt.args.ip, tt.args.port) + + if (err != nil && tt.wantError != nil && err.Error() != tt.wantError.Error()) || + (err == nil && tt.wantError != nil) || + (err != nil && tt.wantError == nil) { + t.Errorf(expectedErrorButGotMessage, "ProxyService.Check()", tt.wantError, err) + } + + if tt.want != nil && + (!reflect.DeepEqual(got.Category, tt.want.Category) || + !reflect.DeepEqual(got.Proxy, tt.want.Proxy) || + !reflect.DeepEqual(got.IP, tt.want.IP) || + !reflect.DeepEqual(got.Port, tt.want.Port)) { + t.Errorf(expectedButGotMessage, "ProxyService.Check()", tt.want, got) + } + }) + } +} + +func TestGetTestingSite(t *testing.T) { + type fields struct { + httpTestingSites []string + httpsTestingSites []string + } + + tests := []struct { + name string + fields fields + want []string + }{ + { + name: "HTTP", + fields: fields{ + httpTestingSites: testHTTPTestingSites, + }, + want: testHTTPTestingSites, + }, + { + name: "HTTPS", + fields: fields{ + httpsTestingSites: testHTTPSTestingSites, + }, + want: testHTTPSTestingSites, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &service.ProxyService{ + HTTPTestingSites: tt.fields.httpTestingSites, + HTTPSTestingSites: tt.fields.httpsTestingSites, + } + + site := s.GetTestingSite(tt.name) + if len(site) == 0 { + t.Errorf(expectedNonEmptyMessage, "site", tt.name+" sites") + } + + found := false + for _, expectedSite := range tt.want { + if expectedSite == site { + found = true + break + } + } + if !found { + t.Errorf(expectedButGotMessage, "site", tt.want, site) + } + }) + } +} + +func TestGetRandomUserAgent(t *testing.T) { + tests := []struct { + name string + }{ + { + name: "RandomUserAgent", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &service.ProxyService{ + UserAgents: testUserAgents, + } + site := s.GetRandomUserAgent() + found := false + for _, ua := range s.UserAgents { + if ua == site { + found = true + break + } + } + if !found { + t.Errorf(expectedButGotMessage, "user agent", s.UserAgents, site) + } + }) + } +} diff --git a/test/unit/internal/usecase/file_usecase_test.go b/test/unit/internal/usecase/file_usecase_test.go new file mode 100644 index 00000000..b416d831 --- /dev/null +++ b/test/unit/internal/usecase/file_usecase_test.go @@ -0,0 +1,104 @@ +package usecase_test + +import ( + "path/filepath" + "strings" + "sync" + "testing" + + "github.com/fyvri/fresh-proxy-list/internal/entity" + "github.com/fyvri/fresh-proxy-list/internal/usecase" +) + +var ( + mutex sync.Mutex +) + +func TestSaveFiles(t *testing.T) { + mockFileRepository := &mockFileRepository{} + mockProxyRepository := &mockProxyRepository{} + + mockProxyRepository.GetAllClassicViewFunc = func() []string { + return []string{ + testProxy1, + testProxy2, + testProxy3, + testProxy4, + } + } + mockProxyRepository.GetAllAdvancedViewFunc = func() []entity.AdvancedProxy { + return []entity.AdvancedProxy{ + testAdvancedProxyEntity1, + testAdvancedProxyEntity2, + testAdvancedProxyEntity3, + testAdvancedProxyEntity4, + } + } + + mockProxyRepository.GetHTTPClassicViewFunc = func() []string { + return []string{testProxy1} + } + mockProxyRepository.GetHTTPAdvancedViewFunc = func() []entity.Proxy { + return []entity.Proxy{ + testProxyEntity1, + } + } + + mockProxyRepository.GetHTTPSClassicViewFunc = func() []string { + return []string{ + testProxy2, + } + } + mockProxyRepository.GetHTTPSAdvancedViewFunc = func() []entity.Proxy { + return []entity.Proxy{ + testProxyEntity2, + } + } + + mockProxyRepository.GetSOCKS4ClassicViewFunc = func() []string { + return []string{ + testProxy3, + } + } + mockProxyRepository.GetSOCKS4AdvancedViewFunc = func() []entity.Proxy { + return []entity.Proxy{ + testProxyEntity3, + } + } + + mockProxyRepository.GetSOCKS5ClassicViewFunc = func() []string { + return []string{ + testProxy4, + } + } + mockProxyRepository.GetSOCKS5AdvancedViewFunc = func() []entity.Proxy { + return []entity.Proxy{ + testProxyEntity4, + } + } + + got := 0 + mockFileRepository.SaveFileFunc = func(filename string, data interface{}, extension string) error { + mutex.Lock() + defer mutex.Unlock() + + got++ + t.Logf("SaveFile called with filename: %s, extension: %s", filename, extension) + if !strings.HasPrefix(filename, filepath.Join(testStorageDir, testClassicDir)) && + !strings.HasPrefix(filename, filepath.Join(testStorageDir, testAdvancedDir)) { + t.Errorf(unexpectedMessage, "filename", filename) + } + if extension != testCSVExtension && extension != testJSONExtension && extension != testXMLExtension && extension != testYAMLExtension && extension != testTXTExtension { + t.Errorf(unexpectedMessage, "extension", extension) + } + return nil + } + uc := usecase.NewFileUsecase(mockFileRepository, mockProxyRepository, testFileOutputExtensions) + uc.SaveFiles() + + // (5 categories * number of extensions * 2 file types (classic, advanced)) + (5 all * 1 extension txt * 1 file type classic) + want := (5 * len(testFileOutputExtensions) * 2) + (5 * 1 * 1) + if got != want { + t.Errorf(expectedButGotMessage, "calls", want, got) + } +} diff --git a/test/unit/internal/usecase/proxy_usecase_test.go b/test/unit/internal/usecase/proxy_usecase_test.go new file mode 100644 index 00000000..58049d42 --- /dev/null +++ b/test/unit/internal/usecase/proxy_usecase_test.go @@ -0,0 +1,387 @@ +package usecase_test + +import ( + "errors" + "net" + "reflect" + "sync" + "testing" + + "github.com/fyvri/fresh-proxy-list/internal/entity" + "github.com/fyvri/fresh-proxy-list/internal/infrastructure/repository" + "github.com/fyvri/fresh-proxy-list/internal/service" + "github.com/fyvri/fresh-proxy-list/internal/usecase" +) + +func TestNewProxyUsecase(t *testing.T) { + mockProxyRepository := &mockProxyRepository{} + mockProxyService := &mockProxyService{} + proxyUsecase := usecase.NewProxyUsecase(mockProxyRepository, mockProxyService, testSpecialIPs, testPrivateIPs) + if proxyUsecase == nil { + t.Errorf(expectedReturnNonNil, "NewProxyUsecase", "ProxyUsecaseInterface") + } + + uc, ok := proxyUsecase.(*usecase.ProxyUsecase) + if !ok { + t.Errorf(expectedTypeAssertionErrorMessage, "*usecase.ProxyUsecase") + } + + testKey := testProxyEntity1.Category + "_" + testProxyEntity1.Proxy + testValue := true + uc.ProxyMap.Store(testKey, testValue) + + got, ok := uc.ProxyMap.Load(testKey) + if !ok || got != testValue { + t.Errorf(expectedButGotMessage, "value", testValue, got) + } + + _, loaded := uc.ProxyMap.LoadOrStore(testKey, false) + if !loaded { + t.Errorf("Expected LoadOrStore to return true indicating the key was loaded") + } + + got, _ = uc.ProxyMap.Load(testKey) + if got != testValue { + t.Errorf(expectedButGotMessage, "value after LoadOrStore", testValue, got) + } + + if !reflect.DeepEqual(uc.SpecialIPs, testSpecialIPs) { + t.Errorf(expectedButGotMessage, "SpecialIPs", testSpecialIPs, uc.SpecialIPs) + } + + if !reflect.DeepEqual(uc.PrivateIPs, testPrivateIPs) { + t.Errorf(expectedButGotMessage, "PrivateIPs", testPrivateIPs, uc.PrivateIPs) + } +} + +func TestProcessProxy(t *testing.T) { + type fields struct { + proxyRepository repository.ProxyRepositoryInterface + proxyService service.ProxyServiceInterface + } + + type args struct { + category string + proxy string + isChecked bool + } + + tests := []struct { + name string + fields fields + args args + want *entity.Proxy + wantError error + }{ + { + name: "ProxyNotFound", + fields: fields{ + proxyRepository: &mockProxyRepository{}, + proxyService: &mockProxyService{}, + }, + args: args{ + category: testHTTPCategory, + proxy: " ", + isChecked: false, + }, + want: nil, + wantError: errors.New("proxy not found"), + }, + { + name: "ProxyFormatIncorrect", + fields: fields{ + proxyRepository: &mockProxyRepository{}, + proxyService: &mockProxyService{}, + }, + args: args{ + category: testHTTPCategory, + proxy: "invalid-proxy", + isChecked: false, + }, + want: nil, + wantError: errors.New("proxy format incorrect"), + }, + { + name: "ProxyFormatNotMatch", + fields: fields{ + proxyRepository: &mockProxyRepository{}, + proxyService: &mockProxyService{}, + }, + args: args{ + category: testHTTPCategory, + proxy: "invalid-proxy:1337", + isChecked: false, + }, + want: nil, + wantError: errors.New("proxy format not match"), + }, + { + name: "ProxyIsSpecialIP", + fields: fields{ + proxyRepository: &mockProxyRepository{}, + proxyService: &mockProxyService{}, + }, + args: args{ + category: testHTTPCategory, + proxy: "1.1.1.1:1337", + isChecked: false, + }, + want: nil, + wantError: errors.New("proxy belongs to special ip"), + }, + { + name: "ProxyPortIsMoreThan65535", + fields: fields{ + proxyRepository: &mockProxyRepository{}, + proxyService: &mockProxyService{}, + }, + args: args{ + category: testHTTPCategory, + proxy: testIP1 + ":65540", + isChecked: false, + }, + want: nil, + wantError: errors.New("proxy port format incorrect"), + }, + { + name: "ProxyHasBeenProcessed", + fields: fields{ + proxyRepository: &mockProxyRepository{}, + }, + args: args{ + category: testProxyEntity1.Category, + proxy: testProxyEntity1.Proxy, + isChecked: false, + }, + want: nil, + wantError: errors.New("proxy has been processed"), + }, + { + name: "ValidProxy", + fields: fields{ + proxyRepository: &mockProxyRepository{}, + proxyService: &mockProxyService{ + CheckFunc: func(category string, ip string, port string) (*entity.Proxy, error) { + return &testProxyEntity1, nil + }, + }, + }, + args: args{ + category: testProxyEntity1.Category, + proxy: testProxyEntity1.Proxy, + isChecked: true, + }, + want: &entity.Proxy{ + Category: testProxyEntity1.Category, + Proxy: testProxyEntity1.Proxy, + IP: testProxyEntity1.IP, + Port: testProxyEntity1.Port, + TimeTaken: testProxyEntity1.TimeTaken, + CheckedAt: testProxyEntity1.CheckedAt, + }, + wantError: nil, + }, + { + name: "NotValidProxy", + fields: fields{ + proxyRepository: &mockProxyRepository{}, + proxyService: &mockProxyService{ + CheckFunc: func(category string, ip string, port string) (*entity.Proxy, error) { + return nil, errors.New("proxy not valid") + }, + }, + }, + args: args{ + category: testProxyEntity1.Category, + proxy: testProxyEntity1.Proxy, + isChecked: true, + }, + want: nil, + wantError: errors.New("proxy not valid"), + }, + { + name: "ValidProxyWithNotChecked", + fields: fields{ + proxyRepository: &mockProxyRepository{}, + proxyService: &mockProxyService{ + CheckFunc: func(category string, ip string, port string) (*entity.Proxy, error) { + return &testProxyEntity1, nil + }, + }, + }, + args: args{ + category: testProxyEntity1.Category, + proxy: testProxyEntity1.Proxy, + isChecked: false, + }, + want: &entity.Proxy{ + Category: testProxyEntity1.Category, + Proxy: testProxyEntity1.Proxy, + IP: testProxyEntity1.IP, + Port: testProxyEntity1.Port, + TimeTaken: 0, + CheckedAt: "", + }, + wantError: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + uc := &usecase.ProxyUsecase{ + ProxyRepository: tt.fields.proxyRepository, + ProxyService: tt.fields.proxyService, + ProxyMap: sync.Map{}, + SpecialIPs: testSpecialIPs, + PrivateIPs: testPrivateIPs, + } + + if tt.name == "ProxyHasBeenProcessed" { + uc.ProxyMap.Store(tt.args.category+"_"+tt.args.proxy, true) + } + + got, err := uc.ProcessProxy(tt.args.category, tt.args.proxy, tt.args.isChecked) + + if err != nil && err.Error() != tt.wantError.Error() { + t.Errorf(expectedErrorButGotMessage, "ProcessProxy()", tt.wantError, err) + } + + if !reflect.DeepEqual(got, tt.want) { + t.Errorf(expectedButGotMessage, "ProcessProxy()", tt.want, got) + } + }) + } +} + +func TestIsSpecialIP(t *testing.T) { + type args struct { + ip string + } + + type fields struct { + specialIPs []string + privateIPs []net.IPNet + } + + tests := []struct { + name string + fields fields + args args + want bool + }{ + { + name: "ItIsSpecialIP", + fields: fields{ + specialIPs: testSpecialIPs, + privateIPs: testPrivateIPs, + }, + args: args{ + ip: "1.1.1.1", + }, + want: true, + }, + { + name: "ErrorParseIP", + fields: fields{ + specialIPs: testSpecialIPs, + privateIPs: testPrivateIPs, + }, + args: args{ + ip: "13.37.1", + }, + want: true, + }, + { + name: "ItIsUnspecified", + fields: fields{ + specialIPs: testSpecialIPs, + privateIPs: testPrivateIPs, + }, + args: args{ + ip: "::1", + }, + want: true, + }, + { + name: "ItIsPrivateIP", + fields: fields{ + specialIPs: testSpecialIPs, + privateIPs: testPrivateIPs, + }, + args: args{ + ip: "5.5.5.5", + }, + want: true, + }, + { + name: "ItIsNotSpecialIP", + fields: fields{ + specialIPs: testSpecialIPs, + privateIPs: testPrivateIPs, + }, + args: args{ + ip: "13.37.0.1", + }, + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + uc := &usecase.ProxyUsecase{ + SpecialIPs: testSpecialIPs, + PrivateIPs: testPrivateIPs, + } + got := uc.IsSpecialIP(tt.args.ip) + if got != tt.want { + t.Errorf(expectedButGotMessage, "IP: "+tt.args.ip, tt.want, got) + } + }) + } +} + +func TestGetAllAdvancedView(t *testing.T) { + type fields struct { + proxyRepository repository.ProxyRepositoryInterface + } + + tests := []struct { + name string + fields fields + want []entity.AdvancedProxy + }{ + { + name: "Should return all advanced view proxies", + fields: fields{ + proxyRepository: &mockProxyRepository{ + GetAllAdvancedViewFunc: func() []entity.AdvancedProxy { + return []entity.AdvancedProxy{ + testAdvancedProxyEntity1, + } + }, + }, + }, + want: []entity.AdvancedProxy{ + testAdvancedProxyEntity1, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + uc := &usecase.ProxyUsecase{ + ProxyRepository: tt.fields.proxyRepository, + } + got := uc.GetAllAdvancedView() + if len(got) != len(tt.want) { + t.Errorf(expectedButGotMessage, "GetAllAdvancedView()", tt.want, got) + } + + for i, v := range got { + if !reflect.DeepEqual(v, tt.want[i]) { + t.Errorf(expectedButGotMessage, "GetAllAdvancedView()", tt.want[i], v) + } + } + }) + } +} diff --git a/test/unit/internal/usecase/source_usecase_test.go b/test/unit/internal/usecase/source_usecase_test.go new file mode 100644 index 00000000..695548f0 --- /dev/null +++ b/test/unit/internal/usecase/source_usecase_test.go @@ -0,0 +1,250 @@ +package usecase_test + +import ( + "errors" + "reflect" + "testing" + + "github.com/fyvri/fresh-proxy-list/internal/entity" + "github.com/fyvri/fresh-proxy-list/internal/infrastructure/repository" + "github.com/fyvri/fresh-proxy-list/internal/usecase" + "github.com/fyvri/fresh-proxy-list/pkg/utils" +) + +var ( + testListMethod = "LIST" + testScrapMethod = "SCRAP" + testCategory = "HTTP" + testURL = "http://example.com" + testIsChecked = true +) + +func TestNewSourceUsecase(t *testing.T) { + type fields struct { + sourceRepository repository.SourceRepositoryInterface + fetcherUtil utils.FetcherUtilInterface + } + + tests := []struct { + name string + fields fields + want *usecase.SourceUsecase + }{ + { + name: "Success", + fields: fields{ + sourceRepository: &mockSourceRepository{}, + fetcherUtil: &mockFetcherUtil{}, + }, + want: &usecase.SourceUsecase{ + SourceRepository: &mockSourceRepository{}, + FetcherUtil: &mockFetcherUtil{}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sourceUsecase := usecase.NewSourceUsecase(tt.fields.sourceRepository, tt.fields.fetcherUtil) + if sourceUsecase == nil { + t.Errorf(expectedReturnNonNil, "NewSourceUsecase", "SourceUsecaseInterface") + } + + got, ok := sourceUsecase.(*usecase.SourceUsecase) + if !ok { + t.Errorf(expectedTypeAssertionErrorMessage, "*usecase.SourceUsecase") + } + + if !reflect.DeepEqual(tt.want, got) { + t.Errorf(expectedButGotMessage, "*usecase.SourceUsecase", tt.want, got) + } + }) + } +} + +func TestLoadSourcesSuccess(t *testing.T) { + type fields struct { + sourceRepository repository.SourceRepositoryInterface + } + + tests := []struct { + name string + fields fields + want []entity.Source + wantError error + }{ + { + name: "Success", + fields: fields{ + sourceRepository: &mockSourceRepository{ + LoadSourcesFunc: func() ([]entity.Source, error) { + return []entity.Source{ + { + Method: testListMethod, + Category: testCategory, + URL: testURL, + IsChecked: testIsChecked, + }, + }, nil + }, + }, + }, + want: []entity.Source{ + { + Method: testListMethod, + Category: testCategory, + URL: testURL, + IsChecked: testIsChecked, + }, + }, + wantError: nil, + }, + { + name: "Error", + fields: fields{ + sourceRepository: &mockSourceRepository{ + LoadSourcesFunc: func() ([]entity.Source, error) { + return nil, errors.New("load proxy resource error") + }, + }, + }, + want: nil, + wantError: errors.New("load proxy resource error"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + uc := &usecase.SourceUsecase{ + SourceRepository: tt.fields.sourceRepository, + } + got, err := uc.LoadSources() + + if err != nil && err.Error() != tt.wantError.Error() { + t.Errorf(expectedErrorButGotMessage, "ProcessProxy()", tt.wantError, err) + } + + if !reflect.DeepEqual(got, tt.want) { + t.Errorf(expectedButGotMessage, "ProcessProxy()", tt.want, got) + } + }) + } +} + +func TestProcessSource(t *testing.T) { + type fields struct { + fetcherUtil utils.FetcherUtilInterface + } + + type args struct { + source entity.Source + } + + tests := []struct { + name string + fields fields + args args + want []string + wantError error + }{ + { + name: "TestFetcherError", + fields: fields{ + fetcherUtil: &mockFetcherUtil{ + fetcherError: errors.New("error creating request"), + }, + }, + args: args{ + source: entity.Source{ + Method: testListMethod, + Category: testCategory, + URL: testURL, + IsChecked: testIsChecked, + }, + }, + want: nil, + wantError: errors.New("error creating request"), + }, + { + name: "TestFetcherWithListMethod", + fields: fields{ + fetcherUtil: &mockFetcherUtil{ + fetchDataByte: []byte(testProxy1 + "\n" + testProxy2 + "\n" + testProxy3 + "\n" + testProxy4), + }, + }, + args: args{ + source: entity.Source{ + Method: testListMethod, + Category: testCategory, + URL: testURL, + IsChecked: testIsChecked, + }, + }, + want: []string{ + testProxy1, + testProxy2, + testProxy3, + testProxy4, + }, + wantError: nil, + }, + { + name: "TestFetcherWithScrapMethod", + fields: fields{ + fetcherUtil: &mockFetcherUtil{ + fetchDataByte: []byte(testProxy1 + "\n" + testProxy2 + "\n" + testProxy3 + "\n" + testProxy4), + }, + }, + args: args{ + source: entity.Source{ + Method: testScrapMethod, + Category: testCategory, + URL: testURL, + IsChecked: testIsChecked, + }, + }, + want: []string{ + testProxy1, + testProxy2, + testProxy3, + testProxy4, + }, + wantError: nil, + }, + { + name: "TestFetcherWithUndefinedMethod", + fields: fields{ + fetcherUtil: &mockFetcherUtil{}, + }, + args: args{ + source: entity.Source{ + Method: "NO_METHOD", + Category: testCategory, + URL: testURL, + IsChecked: testIsChecked, + }, + }, + want: nil, + wantError: errors.New("source method not found: NO_METHOD"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + uc := &usecase.SourceUsecase{ + FetcherUtil: tt.fields.fetcherUtil, + } + got, err := uc.ProcessSource(&tt.args.source) + + if (err != nil && tt.wantError != nil && err.Error() != tt.wantError.Error()) || + (err == nil && tt.wantError != nil) || + (err != nil && tt.wantError == nil) { + t.Errorf(expectedErrorButGotMessage, "SourceUsecase.ProcessSource()", tt.wantError, err) + } + + if !reflect.DeepEqual(got, tt.want) { + t.Errorf(expectedButGotMessage, "SourceUsecase.ProcessSource()", tt.want, got) + } + }) + } +} diff --git a/test/unit/internal/usecase/usecase_test.go b/test/unit/internal/usecase/usecase_test.go new file mode 100644 index 00000000..3d746d20 --- /dev/null +++ b/test/unit/internal/usecase/usecase_test.go @@ -0,0 +1,367 @@ +package usecase_test + +import ( + "io" + "net" + "net/http" + "net/http/httptest" + "sync" + "time" + + "github.com/fyvri/fresh-proxy-list/internal/entity" +) + +var ( + unexpectedMessage = "Unexpected %v: %v" + expectedButGotMessage = "Expected %v = %v, but got = %v" + expectedErrorButGotMessage = "Expected %v error = %v, but got = %v" + expectedReturnNonNil = "Expected %v to return a non-nil %v" + expectedTypeAssertionErrorMessage = "Expected type assertion error, but got = %v" + testStorageDir = "storage" + testClassicDir = "classic" + testAdvancedDir = "advanced" + testTXTExtension = "txt" + testCSVExtension = "csv" + testJSONExtension = "json" + testXMLExtension = "xml" + testYAMLExtension = "yaml" + testFileOutputExtensions = []string{testTXTExtension, testCSVExtension} + testHTTPCategory = "HTTP" + testHTTPSCategory = "HTTPS" + testSOCKS4Category = "SOCKS4" + testSOCKS5Category = "SOCKS5" + testSpecialIPs = []string{"1.1.1.1", "2.2.2.2"} + testPrivateIPs = []net.IPNet{ + { + IP: net.IP{3, 3, 3, 3}, + Mask: net.CIDRMask(8, 32), + }, + { + IP: net.IP{4, 4, 4, 4}, + Mask: net.CIDRMask(12, 32), + }, + { + IP: net.IP{5, 5, 5, 5}, + Mask: net.CIDRMask(16, 32), + }, + } + + testIP1 = "13.37.0.1" + testPort1 = "1337" + testProxy1 = testIP1 + ":" + testPort1 + testCategory1 = testHTTPCategory + testProxyEntity1 = entity.Proxy{ + Proxy: testProxy1, + IP: testIP1, + Port: testPort1, + Category: testCategory1, + TimeTaken: 0, + CheckedAt: time.Now().Format(time.RFC3339), + } + testAdvancedProxyEntity1 = entity.AdvancedProxy{ + Proxy: testProxyEntity1.Proxy, + IP: testProxyEntity1.IP, + Port: testProxyEntity1.Port, + TimeTaken: testProxyEntity1.TimeTaken, + CheckedAt: testProxyEntity1.CheckedAt, + Categories: []string{ + testCategory1, + }, + } + + testIP2 = "13.37.0.2" + testPort2 = "1337" + testProxy2 = testIP2 + ":" + testPort2 + testCategory2 = testHTTPSCategory + testProxyEntity2 = entity.Proxy{ + Proxy: testProxy2, + IP: testIP2, + Port: testPort2, + Category: testCategory2, + TimeTaken: 0, + CheckedAt: time.Now().Format(time.RFC3339), + } + testAdvancedProxyEntity2 = entity.AdvancedProxy{ + Proxy: testProxyEntity2.Proxy, + IP: testProxyEntity2.IP, + Port: testProxyEntity2.Port, + TimeTaken: testProxyEntity2.TimeTaken, + CheckedAt: testProxyEntity2.CheckedAt, + Categories: []string{ + testCategory2, + }, + } + + testIP3 = "13.37.0.3" + testPort3 = "1337" + testProxy3 = testIP3 + ":" + testPort3 + testCategory3 = testSOCKS4Category + testProxyEntity3 = entity.Proxy{ + Proxy: testProxy3, + IP: testIP3, + Port: testPort3, + Category: testCategory3, + TimeTaken: 0, + CheckedAt: time.Now().Format(time.RFC3339), + } + testAdvancedProxyEntity3 = entity.AdvancedProxy{ + Proxy: testProxyEntity3.Proxy, + IP: testProxyEntity3.IP, + Port: testProxyEntity3.Port, + TimeTaken: testProxyEntity3.TimeTaken, + CheckedAt: testProxyEntity3.CheckedAt, + Categories: []string{ + testCategory3, + }, + } + + testIP4 = "13.37.0.4" + testPort4 = "1337" + testProxy4 = testIP4 + ":" + testPort4 + testCategory4 = testSOCKS5Category + testProxyEntity4 = entity.Proxy{ + Proxy: testProxy4, + IP: testIP4, + Port: testPort4, + Category: testCategory4, + TimeTaken: 0, + CheckedAt: time.Now().Format(time.RFC3339), + } + testAdvancedProxyEntity4 = entity.AdvancedProxy{ + Proxy: testProxyEntity4.Proxy, + IP: testProxyEntity4.IP, + Port: testProxyEntity4.Port, + TimeTaken: testProxyEntity4.TimeTaken, + CheckedAt: testProxyEntity4.CheckedAt, + Categories: []string{ + testCategory4, + }, + } +) + +type mockFetcherUtil struct { + fetchDataByte []byte + fetcherError error + NewRequestFunc func(method, url string, body io.Reader) (*http.Request, error) + DoFunc func(client *http.Client, req *http.Request) (*http.Response, error) +} + +func (m *mockFetcherUtil) FetchData(url string) ([]byte, error) { + if m.fetcherError != nil { + return nil, m.fetcherError + } + return m.fetchDataByte, nil +} + +func (m *mockFetcherUtil) Do(client *http.Client, req *http.Request) (*http.Response, error) { + if m.DoFunc != nil { + return m.DoFunc(client, req) + } + return httptest.NewRecorder().Result(), nil +} + +func (m *mockFetcherUtil) NewRequest(method string, url string, body io.Reader) (*http.Request, error) { + if m.NewRequestFunc != nil { + return m.NewRequestFunc(method, url, body) + } + return http.NewRequest(method, url, body) +} + +type mockProxyService struct { + CheckFunc func(category string, ip string, port string) (*entity.Proxy, error) + GetTestingSiteFunc func(category string) string + GetRandomUserAgentFunc func() string +} + +func (m *mockProxyService) Check(category string, ip string, port string) (*entity.Proxy, error) { + if m.CheckFunc != nil { + return m.CheckFunc(category, ip, port) + } + return nil, nil +} + +func (m *mockProxyService) GetTestingSite(category string) string { + if m.GetTestingSiteFunc != nil { + return m.GetTestingSiteFunc(category) + } + return "" +} + +func (m *mockProxyService) GetRandomUserAgent() string { + if m.GetRandomUserAgentFunc != nil { + return m.GetRandomUserAgentFunc() + } + return "" +} + +type mockSourceRepository struct { + LoadSourcesFunc func() ([]entity.Source, error) +} + +func (m *mockSourceRepository) LoadSources() ([]entity.Source, error) { + return m.LoadSourcesFunc() +} + +type mockFileRepository struct { + SaveFileFunc func(filename string, data interface{}, format string) error + CreateDirectoryFunc func(filePath string) error + WriteTxtFunc func(writer io.Writer, data interface{}) error + EncodeCSVFunc func(writer io.Writer, data interface{}) error + WriteCSVFunc func(writer io.Writer, header []string, rows [][]string) error + EncodeJSONFunc func(writer io.Writer, data interface{}) error + EncodeXMLFunc func(writer io.Writer, data interface{}) error + EncodeYAMLFunc func(writer io.Writer, data interface{}) error +} + +func (m *mockFileRepository) SaveFile(filename string, data interface{}, ext string) error { + if m.SaveFileFunc != nil { + return m.SaveFileFunc(filename, data, ext) + } + return nil +} + +func (m *mockFileRepository) CreateDirectory(filePath string) error { + if m.CreateDirectoryFunc != nil { + return m.CreateDirectoryFunc(filePath) + } + return nil +} + +func (m *mockFileRepository) WriteTxt(writer io.Writer, data interface{}) error { + if m.WriteTxtFunc != nil { + return m.WriteTxtFunc(writer, data) + } + return nil +} + +func (m *mockFileRepository) EncodeCSV(writer io.Writer, data interface{}) error { + if m.EncodeCSVFunc != nil { + return m.EncodeCSVFunc(writer, data) + } + return nil +} + +func (m *mockFileRepository) WriteCSV(writer io.Writer, header []string, rows [][]string) error { + if m.WriteCSVFunc != nil { + return m.WriteCSVFunc(writer, header, rows) + } + return nil +} + +func (m *mockFileRepository) EncodeJSON(writer io.Writer, data interface{}) error { + if m.EncodeJSONFunc != nil { + return m.EncodeJSONFunc(writer, data) + } + return nil +} + +func (m *mockFileRepository) EncodeXML(writer io.Writer, data interface{}) error { + if m.EncodeXMLFunc != nil { + return m.EncodeXMLFunc(writer, data) + } + return nil +} + +func (m *mockFileRepository) EncodeYAML(writer io.Writer, data interface{}) error { + if m.EncodeYAMLFunc != nil { + return m.EncodeYAMLFunc(writer, data) + } + return nil +} + +type mockProxyRepository struct { + StoreFunc func(proxy *entity.Proxy) + GetAllClassicViewFunc func() []string + GetHTTPClassicViewFunc func() []string + GetHTTPSClassicViewFunc func() []string + GetSOCKS4ClassicViewFunc func() []string + GetSOCKS5ClassicViewFunc func() []string + GetAllAdvancedViewFunc func() []entity.AdvancedProxy + GetHTTPAdvancedViewFunc func() []entity.Proxy + GetHTTPSAdvancedViewFunc func() []entity.Proxy + GetSOCKS4AdvancedViewFunc func() []entity.Proxy + GetSOCKS5AdvancedViewFunc func() []entity.Proxy + + StoredProxies []entity.Proxy + Mutex sync.Mutex +} + +func (m *mockProxyRepository) Store(proxy *entity.Proxy) { + m.Mutex.Lock() + defer m.Mutex.Unlock() + m.StoredProxies = append(m.StoredProxies, *proxy) +} + +func (m *mockProxyRepository) GetStoredProxies() []entity.Proxy { + return m.StoredProxies +} + +func (m *mockProxyRepository) GetAllClassicView() []string { + if m.GetAllClassicViewFunc != nil { + return m.GetAllClassicViewFunc() + } + return nil +} + +func (m *mockProxyRepository) GetHTTPClassicView() []string { + if m.GetHTTPClassicViewFunc != nil { + return m.GetHTTPClassicViewFunc() + } + return nil +} + +func (m *mockProxyRepository) GetHTTPSClassicView() []string { + if m.GetHTTPSClassicViewFunc != nil { + return m.GetHTTPSClassicViewFunc() + } + return nil +} + +func (m *mockProxyRepository) GetSOCKS4ClassicView() []string { + if m.GetSOCKS4ClassicViewFunc != nil { + return m.GetSOCKS4ClassicViewFunc() + } + return nil +} + +func (m *mockProxyRepository) GetSOCKS5ClassicView() []string { + if m.GetSOCKS5ClassicViewFunc != nil { + return m.GetSOCKS5ClassicViewFunc() + } + return nil +} + +func (m *mockProxyRepository) GetAllAdvancedView() []entity.AdvancedProxy { + if m.GetAllAdvancedViewFunc != nil { + return m.GetAllAdvancedViewFunc() + } + return nil +} + +func (m *mockProxyRepository) GetHTTPAdvancedView() []entity.Proxy { + if m.GetHTTPAdvancedViewFunc != nil { + return m.GetHTTPAdvancedViewFunc() + } + return nil +} + +func (m *mockProxyRepository) GetHTTPSAdvancedView() []entity.Proxy { + if m.GetHTTPSAdvancedViewFunc != nil { + return m.GetHTTPSAdvancedViewFunc() + } + return nil +} + +func (m *mockProxyRepository) GetSOCKS4AdvancedView() []entity.Proxy { + if m.GetSOCKS4AdvancedViewFunc != nil { + return m.GetSOCKS4AdvancedViewFunc() + } + return nil +} + +func (m *mockProxyRepository) GetSOCKS5AdvancedView() []entity.Proxy { + if m.GetSOCKS5AdvancedViewFunc != nil { + return m.GetSOCKS5AdvancedViewFunc() + } + return nil +} diff --git a/test/unit/pkg/utils/csv_writer_util_test.go b/test/unit/pkg/utils/csv_writer_util_test.go new file mode 100644 index 00000000..d2f05d6c --- /dev/null +++ b/test/unit/pkg/utils/csv_writer_util_test.go @@ -0,0 +1,155 @@ +package util_test + +import ( + "bytes" + "encoding/csv" + "io" + "reflect" + "testing" + + "github.com/fyvri/fresh-proxy-list/pkg/utils" +) + +var ( + writerBufferSize = 1024 +) + +func TestNewCSVWriter(t *testing.T) { + tests := []struct { + name string + want utils.CSVWriterUtilInterface + }{ + { + name: "Success", + want: &utils.CSVWriterUtil{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + csvWriterUtil := utils.NewCSVWriter() + if csvWriterUtil == nil { + t.Errorf(expectedReturnNonNil, "NewCSVWriter", "CSVWriterUtilInterface") + } + + got, ok := csvWriterUtil.(*utils.CSVWriterUtil) + if !ok { + t.Errorf(expectedTypeAssertionErrorMessage, "*utils.CSVWriterUtil") + } + + if !reflect.DeepEqual(tt.want, got) { + t.Errorf(expectedButGotMessage, "*utils.CSVWriterUtil", tt.want, got) + } + }) + } +} + +func TestInit(t *testing.T) { + type args struct { + writer io.Writer + } + + tests := []struct { + name string + args args + want *csv.Writer + }{ + { + name: "Success", + args: args{ + writer: bytes.NewBuffer(make([]byte, writerBufferSize)), + }, + want: csv.NewWriter(bytes.NewBuffer(make([]byte, writerBufferSize))), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + u := utils.NewCSVWriter() + got := u.Init(tt.args.writer) + if reflect.TypeOf(got) != reflect.TypeOf(tt.want) { + t.Errorf(expectedButGotMessage, "Init()", reflect.TypeOf(tt.want), reflect.TypeOf(got)) + } + }) + } +} + +func TestFlush(t *testing.T) { + type setup struct { + newCSVWriter func() *utils.CSVWriterUtil + } + + type args struct { + csvWriter *csv.Writer + } + + tests := []struct { + name string + setup setup + args args + }{ + { + name: "Success", + setup: setup{ + newCSVWriter: func() *utils.CSVWriterUtil { + return utils.NewCSVWriter().(*utils.CSVWriterUtil) + }, + }, + args: args{ + csvWriter: csv.NewWriter(bytes.NewBuffer(make([]byte, writerBufferSize))), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + u := tt.setup.newCSVWriter() + u.Flush(tt.args.csvWriter) + }) + } +} + +func TestWrite(t *testing.T) { + type setup struct { + newCSVWriter func() *utils.CSVWriterUtil + } + + type args struct { + writer io.Writer + record []string + } + + tests := []struct { + name string + setup setup + args args + wantError error + }{ + { + name: "Success", + setup: setup{ + newCSVWriter: func() *utils.CSVWriterUtil { + return utils.NewCSVWriter().(*utils.CSVWriterUtil) + }, + }, + args: args{ + writer: &bytes.Buffer{}, + record: []string{"a", "b", "c"}, + }, + wantError: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + u := tt.setup.newCSVWriter() + csvWriter := u.Init(tt.args.writer) + err := u.Write(csvWriter, tt.args.record) + if (err != nil && tt.wantError != nil && err.Error() != tt.wantError.Error()) || + (err == nil && tt.wantError != nil) || + (err != nil && tt.wantError == nil) { + t.Errorf(expectedErrorButGotMessage, "Write()", tt.wantError, err) + } + }) + } +} diff --git a/test/unit/pkg/utils/fetcher_util_test.go b/test/unit/pkg/utils/fetcher_util_test.go new file mode 100644 index 00000000..e252a2b9 --- /dev/null +++ b/test/unit/pkg/utils/fetcher_util_test.go @@ -0,0 +1,251 @@ +package util_test + +import ( + "bytes" + "errors" + "fmt" + "io" + "net/http" + "reflect" + "testing" + + "github.com/fyvri/fresh-proxy-list/pkg/utils" +) + +var ( + testGETMethod = "GET" + testPOSTMethod = "POST" + testClient = http.DefaultClient + testNewRequest = http.NewRequest +) + +func TestNewFetcher(t *testing.T) { + fetcherUtil := utils.NewFetcher(testClient, testNewRequest) + + if fetcherUtil == nil { + t.Errorf(expectedReturnNonNil, "NewFetcher", "FetcherInterface") + } + + fetcherUtilInstance, ok := fetcherUtil.(*utils.FetcherUtil) + if !ok { + t.Errorf(expectedTypeAssertionErrorMessage, "*utils.FetcherUtil") + } + + req, err := fetcherUtilInstance.NewRequestFunc(testGETMethod, testRawURL, nil) + if err != nil { + t.Errorf(expectedButGotMessage, "newRequest", "no error", err) + } + + if req.Method != testGETMethod { + t.Errorf(expectedButGotMessage, "method", testGETMethod, req.Method) + } + + if req.URL.String() != testRawURL { + t.Errorf(expectedButGotMessage, "URL", testRawURL, req.URL.String()) + } + + if fetcherUtilInstance.Client != nil && fetcherUtilInstance.Client != http.DefaultClient { + t.Errorf(expectedButGotMessage, "client", http.DefaultClient, fetcherUtilInstance.Client) + } +} + +func TestFetchData(t *testing.T) { + type fields struct { + transport *mockTransport + newRequestFunc func(method string, url string, body io.Reader) (*http.Request, error) + } + + type args struct { + url string + } + + tests := []struct { + name string + fields fields + args args + want []byte + wantErr error + }{ + { + name: "Success", + fields: fields{ + transport: &mockTransport{ + response: &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(&mockReadCloser{ + data: []byte("response data"), + }), + }, + err: nil, + }, + newRequestFunc: testNewRequest, + }, + args: args{ + url: testRawURL, + }, + want: []byte("response data"), + wantErr: nil, + }, + { + name: "NewRequestError", + fields: fields{ + transport: &mockTransport{ + response: nil, + err: nil, + }, + newRequestFunc: func(method string, url string, body io.Reader) (*http.Request, error) { + return nil, fmt.Errorf("new request error") + }, + }, + args: args{ + url: testRawURL, + }, + want: nil, + wantErr: errors.New("new request error"), + }, + { + name: "RequestError", + fields: fields{ + transport: &mockTransport{ + response: nil, + err: fmt.Errorf("request error"), + }, + newRequestFunc: testNewRequest, + }, + args: args{ + url: testRawURL, + }, + want: nil, + wantErr: fmt.Errorf("Get \"%s\": request error", testRawURL), + }, + { + name: "ResponseError", + fields: fields{ + transport: &mockTransport{ + response: &http.Response{ + StatusCode: http.StatusInternalServerError, + Body: io.NopCloser(&mockReadCloser{ + data: []byte("error response"), + }), + }, + err: nil, + }, + newRequestFunc: testNewRequest, + }, + args: args{ + url: testRawURL, + }, + want: []byte("error response"), + wantErr: errors.New("failed to fetch data: Internal Server Error"), + }, + { + name: "ReadBodyError", + fields: fields{ + transport: &mockTransport{ + response: &http.Response{ + StatusCode: http.StatusOK, + Body: &mockReadCloser{ + errRead: fmt.Errorf("body read error"), + }, + }, + err: nil, + }, + newRequestFunc: testNewRequest, + }, + args: args{ + url: testRawURL, + }, + want: nil, + wantErr: errors.New("body read error"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fetcherUtil := &utils.FetcherUtil{ + Client: &http.Client{ + Transport: tt.fields.transport, + }, + NewRequestFunc: tt.fields.newRequestFunc, + } + got, err := fetcherUtil.FetchData(tt.args.url) + + if (err != nil && tt.wantErr != nil && err.Error() != tt.wantErr.Error()) || + (err == nil && tt.wantErr != nil) || + (err != nil && tt.wantErr == nil) { + t.Errorf(expectedErrorButGotMessage, "FetchData()", tt.wantErr, err) + } + + if !reflect.DeepEqual(got, tt.want) { + t.Errorf(expectedButGotMessage, "FetchData()", tt.want, got) + } + }) + } +} + +func TestNewRequest(t *testing.T) { + type args struct { + method string + url string + body io.Reader + } + + type want struct { + url string + method string + } + + tests := []struct { + name string + args args + want want + wantErr error + }{ + { + name: testGETMethod, + args: args{ + method: http.MethodGet, + url: testRawURL, + body: nil, + }, + want: want{ + url: testRawURL, + method: http.MethodGet, + }, + wantErr: nil, + }, + { + name: testPOSTMethod, + args: args{ + method: http.MethodPost, + url: testRawURL, + body: bytes.NewReader([]byte("body")), + }, + want: want{ + url: testRawURL, + method: http.MethodPost, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + u := utils.NewFetcher(&http.Client{ + Transport: &mockTransport{}, + }, testNewRequest) + req, err := u.NewRequest(tt.args.method, tt.args.url, tt.args.body) + + if err != nil { + t.Errorf(expectedErrorButGotMessage, "NewRequest()", nil, err) + } + + if req.Method != tt.want.method { + t.Errorf(expectedButGotMessage, "method", tt.want.method, req.Method) + } + + if req.URL.String() != tt.want.url { + t.Errorf(expectedButGotMessage, "URL", tt.want.url, req.URL.String()) + } + }) + } +} diff --git a/test/unit/pkg/utils/url_parser_util_test.go b/test/unit/pkg/utils/url_parser_util_test.go new file mode 100644 index 00000000..1be7ade8 --- /dev/null +++ b/test/unit/pkg/utils/url_parser_util_test.go @@ -0,0 +1,61 @@ +package util_test + +import ( + "net/url" + "reflect" + "testing" + + "github.com/fyvri/fresh-proxy-list/pkg/utils" +) + +func TestParse(t *testing.T) { + type args struct { + rawURL string + } + tests := []struct { + name string + args args + want *url.URL + wantError error + }{ + { + name: "ValidURL", + args: args{ + rawURL: testRawURL, + }, + want: &url.URL{ + Scheme: testScheme, + Host: testHost, + }, + wantError: nil, + }, + { + name: "ValidURLWithFullURL", + args: args{ + rawURL: testFullURL, + }, + want: &url.URL{ + Scheme: testScheme, + Host: testHost, + Path: testPath, + RawQuery: testRawQuery, + }, + wantError: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + u := utils.NewURLParser() + got, err := u.Parse(tt.args.rawURL) + + if tt.wantError != nil { + t.Errorf(expectedErrorButGotMessage, "Parse()", err, tt.wantError) + } + + if !reflect.DeepEqual(got, tt.want) { + t.Errorf(expectedErrorButGotMessage, "Parse()", err, tt.wantError) + } + }) + } +} diff --git a/test/unit/pkg/utils/util_test.go b/test/unit/pkg/utils/util_test.go new file mode 100644 index 00000000..d0407982 --- /dev/null +++ b/test/unit/pkg/utils/util_test.go @@ -0,0 +1,49 @@ +package util_test + +import ( + "io" + "net/http" +) + +var ( + expectedButGotMessage = "Expected %v = %v, but got = %v" + expectedErrorButGotMessage = "Expected %v error = %v, but got = %v" + expectedReturnNonNil = "Expected %v to return a non-nil %v" + expectedTypeAssertionErrorMessage = "Expected type assertion error, but got = %v" + testScheme = "https" + testHost = "example.com" + testPath = "/path" + testRawQuery = "query=1" + testRawURL = testScheme + "://" + testHost + testFullURL = testRawURL + testPath + "?" + testRawQuery +) + +type mockTransport struct { + response *http.Response + err error +} + +func (m *mockTransport) RoundTrip(req *http.Request) (*http.Response, error) { + return m.response, m.err +} + +type mockReadCloser struct { + data []byte + errRead error + errClose error +} + +func (m *mockReadCloser) Read(p []byte) (int, error) { + if m.errRead != nil { + return 0, m.errRead + } + copy(p, m.data) + return len(m.data), io.EOF +} + +func (m *mockReadCloser) Close() error { + if m.errClose != nil { + return m.errClose + } + return nil +}