diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..b444581 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "gomod" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "daily" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..2ee8c75 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,44 @@ +name: Build Test + +on: + push: + branches: + - dev + +jobs: + build: + name: Build check - dev + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Setup go with cache + uses: magnetikonline/action-golang-cache@v5 + with: + go-version-file: go.mod + - run: go version + - name: Extract build target module name. + run: echo "BUILD_FILE_NAME=$(head -n1 go.mod | cut -d ' ' -f2)" >> $GITHUB_ENV + - name: Fetch Dependencies + run: go get + - name: Build test + run: go build -v ${{ env.BUILD_FILE_NAME }} + - name: if fail + uses: actions/github-script@v7 + with: + github-token: ${{github.token}} + script: | + const ref = "${{github.ref}}" + const pull_number = Number(ref.split("/")[2]) + await github.pulls.createReview({ + ...context.repo, + pull_number, + body:"Failed to build. ", + event: "REQUEST_CHANGES" + }) + await github.pulls.update({ + ...context.repo, + pull_number, + state: "open" + }) + if: failure() \ No newline at end of file diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..39b5a28 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,92 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL Advanced" + +on: + push: + branches: [ "dev" ] + pull_request: + branches: [ "main" ] + schedule: + - cron: '21 2 * * 6' + +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + # Runner size impacts CodeQL analysis time. To learn more, please see: + # - https://gh.io/recommended-hardware-resources-for-running-codeql + # - https://gh.io/supported-runners-and-hardware-resources + # - https://gh.io/using-larger-runners (GitHub.com only) + # Consider using larger runners or machines with greater resources for possible analysis time improvements. + runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + permissions: + # required for all workflows + security-events: write + + # required to fetch internal or private CodeQL packs + packages: read + + # only required for workflows in private repositories + actions: read + contents: read + + strategy: + fail-fast: false + matrix: + include: + - language: go + build-mode: autobuild + # CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' + # Use `c-cpp` to analyze code written in C, C++ or both + # Use 'java-kotlin' to analyze code written in Java, Kotlin or both + # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both + # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, + # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. + # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how + # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + # If the analyze step fails for one of the languages you are analyzing with + # "We were unable to automatically build your code", modify the matrix above + # to set the build mode to "manual" for that language. Then modify this step + # to build your code. + # â„šī¸ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + - if: matrix.build-mode == 'manual' + shell: bash + run: | + echo 'If you are using a "manual" build mode for one or more of the' \ + 'languages you are analyzing, replace this with the commands to build' \ + 'your code, for example:' + echo ' make bootstrap' + echo ' make release' + exit 1 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{matrix.language}}" diff --git a/.gitignore b/.gitignore index 31ae8b7..f6de8aa 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,6 @@ nosetests.xml .mr.developer.cfg .project .pydevproject + +.idea/ +.vscode diff --git a/README.md b/README.md index 426d575..a19b8cf 100644 --- a/README.md +++ b/README.md @@ -19,24 +19,19 @@ What can one expect in namebench 2.0? BUILDING: ========= -Building requires Go 1.2 to be installed: http://golang.org/ +Building requires Go 1.23.1 to be installed: http://golang.org/ * Create a workspace directory, and cd into it. * Prepare your workspace directory: -``` - export GOPATH=`pwd` - git clone https://github.com/google/namebench.git src/github.com/google/namebench - go get github.com/mattn/go-sqlite3 - go get golang.org/x/net/publicsuffix - go get github.com/miekg/dns +```shell + go get ``` * Build it. -``` - cd src/github.com/google/namebench - go build namebench.go +```shell + go build namebench ``` You should have an executable named 'namebench' in the current directory. @@ -44,5 +39,7 @@ You should have an executable named 'namebench' in the current directory. RUNNING: ======== +* CLI mode: (go run) namebench --mode now --join_string '\t' --dns_filter 0 + * End-user: run ./namebench, which should open up a UI window. * Developer, run ./namebench_dev_server.sh for an auto-reloading webserver at http://localhost:9080/ diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..034e848 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,21 @@ +# Security Policy + +## Supported Versions + +Use this section to tell people about which versions of your project are +currently being supported with security updates. + +| Version | Supported | +| ------- | ------------------ | +| 5.1.x | :white_check_mark: | +| 5.0.x | :x: | +| 4.0.x | :white_check_mark: | +| < 4.0 | :x: | + +## Reporting a Vulnerability + +Use this section to tell people how to report a vulnerability. + +Tell them where to go, how often they can expect to get an update on a +reported vulnerability, what to expect if the vulnerability is accepted or +declined, etc. diff --git a/build.sh b/build.sh index 75e1c4f..c6f6b21 100755 --- a/build.sh +++ b/build.sh @@ -1,4 +1,4 @@ #!/bin/sh packages=$(grep -hr "^package" */*.go | sort -u | cut -d" " -f2 | sed s/"^"/"\.\.\."/ | xargs) echo "Rebuilding $packages" -go install $packages +go install "$packages" diff --git a/dnschecks/dnschecks.go b/dnschecks/dnschecks.go deleted file mode 100644 index e474ce8..0000000 --- a/dnschecks/dnschecks.go +++ /dev/null @@ -1,26 +0,0 @@ -package dnschecks - -import ( - "github.com/google/namebench/dnsqueue" - "log" - "strings" -) - -func DnsSec(ip string) (ok bool, err error) { - r := &dnsqueue.Request{ - Destination: ip, - RecordType: "A", - RecordName: "www.dnssec-failed.org.", - VerifySignature: true, - } - result, err := dnsqueue.SendQuery(r) - for _, answer := range result.Answers { - // TODO(tstromberg): Implement properly. - if strings.Contains(answer.String, "RRSIG") { - log.Printf("DnsSec for %s: true", ip) - return true, err - } - } - log.Printf("DnsSec for %s: false", ip) - return false, err -} diff --git a/dnsqueue/dnsqueue.go b/dnsqueue/dnsqueue.go deleted file mode 100644 index 9a10e22..0000000 --- a/dnsqueue/dnsqueue.go +++ /dev/null @@ -1,128 +0,0 @@ -// dnsqueue is a library for queueing up a large number of DNS requests. -package dnsqueue - -import ( - "errors" - "fmt" - "github.com/miekg/dns" - "log" - "time" -) - -// Request contains data for making a DNS request -type Request struct { - Destination string - RecordType string - RecordName string - VerifySignature bool - - exit bool -} - -// Answer contains a single answer returned by a DNS server. -type Answer struct { - Ttl uint32 - Name string - String string -} - -// Result contains metadata relating to a set of DNS server results. -type Result struct { - Request Request - Duration time.Duration - Answers []Answer - Error string -} - -// Queue contains methods and state for setting up a request queue. -type Queue struct { - Requests chan *Request - Results chan *Result - WorkerCount int - Quit chan bool -} - -// StartQueue starts a new queue with max length of X with worker count Y. -func StartQueue(size, workers int) (q *Queue) { - q = &Queue{ - Requests: make(chan *Request, size), - Results: make(chan *Result, size), - WorkerCount: workers, - } - for i := 0; i < q.WorkerCount; i++ { - go startWorker(q.Requests, q.Results) - } - return -} - -// Queue.Add adds a request to the queue. Only blocks if queue is full. -func (q *Queue) Add(dest, record_type, record_name string) { - q.Requests <- &Request{ - Destination: dest, - RecordType: record_type, - RecordName: record_name, - } -} - -// Queue.SendDieSignal sends a signal to the workers that they can go home now. -func (q *Queue) SendCompletionSignal() { - log.Printf("Sending completion signal...") - for i := 0; i < q.WorkerCount; i++ { - q.Requests <- &Request{exit: true} - } -} - -// startWorker starts a thread to watch the request channel and populate result channel. -func startWorker(queue <-chan *Request, results chan<- *Result) { - for request := range queue { - if request.exit { - log.Printf("Completion received, worker is done.") - return - } - result, err := SendQuery(request) - if err != nil { - log.Printf("Error sending query: %s", err) - } - log.Printf("Sending back result: %s", result) - results <- &result - } -} - -// Send a DNS query via UDP, configured by a Request object. If successful, -// stores response details in Result object, otherwise, returns Result object -// with an error string. -func SendQuery(request *Request) (result Result, err error) { - log.Printf("Sending query: %s", request) - result.Request = *request - - record_type, ok := dns.StringToType[request.RecordType] - if !ok { - result.Error = fmt.Sprintf("Invalid type: %s", request.RecordType) - return result, errors.New(result.Error) - } - - m := new(dns.Msg) - if request.VerifySignature == true { - log.Printf("SetEdns0 for %s", request.RecordName) - m.SetEdns0(4096, true) - } - m.SetQuestion(request.RecordName, record_type) - c := new(dns.Client) - in, rtt, err := c.Exchange(m, request.Destination) - // log.Printf("Answer: %s [%d] %s", in, rtt, err) - - result.Duration = rtt - if err != nil { - result.Error = err.Error() - } else { - for _, rr := range in.Answer { - answer := Answer{ - Ttl: rr.Header().Ttl, - Name: rr.Header().Name, - String: rr.String(), - } - result.Answers = append(result.Answers, answer) - } - } - return result, nil -} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..3335558 --- /dev/null +++ b/go.mod @@ -0,0 +1,53 @@ +module namebench + +go 1.23.1 + +require ( + github.com/Cellularhacker/apiError-go v0.0.3 + github.com/Cellularhacker/apiHandler-gin-go v1.0.2 + github.com/Cellularhacker/core-go v1.0.7 + github.com/Cellularhacker/logger-go v1.0.4 + github.com/Cellularhacker/util-go v0.0.8 + github.com/goccy/go-json v0.10.3 + github.com/mattn/go-sqlite3 v1.14.23 + github.com/miekg/dns v1.1.62 + golang.org/x/net v0.29.0 +) + +require ( + github.com/Cellularhacker/logger v1.0.3 // indirect + github.com/bytedance/sonic v1.12.3 // indirect + github.com/bytedance/sonic/loader v0.2.0 // indirect + github.com/cloudwego/base64x v0.1.4 // indirect + github.com/cloudwego/iasm v0.2.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.5 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/gin-gonic/gin v1.10.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.22.1 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/joho/godotenv v1.5.1 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.8 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/onsi/ginkgo v1.16.5 // indirect + github.com/pelletier/go-toml/v2 v2.2.3 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.0 // indirect + golang.org/x/arch v0.10.0 // indirect + golang.org/x/crypto v0.27.0 // indirect + golang.org/x/mod v0.21.0 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/sys v0.25.0 // indirect + golang.org/x/text v0.18.0 // indirect + golang.org/x/tools v0.25.0 // indirect + google.golang.org/protobuf v1.34.2 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..2501604 --- /dev/null +++ b/go.sum @@ -0,0 +1,184 @@ +github.com/Cellularhacker/apiError-go v0.0.3 h1:dD7M5T3+cLu7euqsi4Pc1zjnj5kast0ORpEyiVa6czA= +github.com/Cellularhacker/apiError-go v0.0.3/go.mod h1:ICem0XY2o52M9LiR59ebu9ZAIYJjx0ezuDtJSLm75Ls= +github.com/Cellularhacker/apiHandler-gin-go v1.0.2 h1:NFhhLinpIAWrKJB+qsjnd/JaAW+o9ijEIGvJEo2PAuA= +github.com/Cellularhacker/apiHandler-gin-go v1.0.2/go.mod h1:/duENtojJoj1c19ReS2IRyPsUX1vY03EXnukzaS/pfc= +github.com/Cellularhacker/core-go v1.0.7 h1:iptwzIeUFtzwKxg52ZS5da97OqsXY/Tu2KrAMt++OFg= +github.com/Cellularhacker/core-go v1.0.7/go.mod h1:9N1hUl68KNQdEFr6eX6xC+JzuM1/sn5DRxcHJuO1evw= +github.com/Cellularhacker/logger v1.0.3 h1:RZkTMWUeIFQME7jCuRZS9jvUYBO/Y4MP5HDgZ+RTduQ= +github.com/Cellularhacker/logger v1.0.3/go.mod h1:NvC5fM/fFWkT+MLgWXpMQhW4oHyZbVz631VRSi44g9o= +github.com/Cellularhacker/logger-go v1.0.4 h1:zF4fjkrvKGOB3IOZ6+H7PIbqShXl3o+F/1iMONRhkAw= +github.com/Cellularhacker/logger-go v1.0.4/go.mod h1:bC3QFDkIg9xsgXisy9Cjc3OTAvOG4VVbWSiS3SQUgLs= +github.com/Cellularhacker/util-go v0.0.8 h1:/NciHDFhFkt5zuc5n6Yqt8i/mM2UBcasw7pQSoHTMqw= +github.com/Cellularhacker/util-go v0.0.8/go.mod h1:TREddBDUbLsbxIVOTlsqBeUEbhNWjR8hjWG7xUfxJ+o= +github.com/bytedance/sonic v1.12.3 h1:W2MGa7RCU1QTeYRTPE3+88mVC0yXmsRQRChiyVocVjU= +github.com/bytedance/sonic v1.12.3/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/bytedance/sonic/loader v0.2.0 h1:zNprn+lsIP06C/IqCHs3gPQIvnvpKbbxyXQP1iU4kWM= +github.com/bytedance/sonic/loader v0.2.0/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= +github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/gabriel-vasile/mimetype v1.4.5 h1:J7wGKdGu33ocBOhGy0z653k/lFKLFDPJMG8Gql0kxn4= +github.com/gabriel-vasile/mimetype v1.4.5/go.mod h1:ibHel+/kbxn9x2407k1izTA1S81ku1z/DlgOW2QE0M4= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= +github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA= +github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= +github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= +github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.23 h1:gbShiuAP1W5j9UOksQ06aiiqPMxYecovVGwmTxWtuw0= +github.com/mattn/go-sqlite3 v1.14.23/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ= +github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= +github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/arch v0.10.0 h1:S3huipmSclq3PJMNe76NGwkBR504WFkQ5dhzWzP8ZW8= +golang.org/x/arch v0.10.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= +golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= +golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= +golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= +golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE= +golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +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/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= diff --git a/history/filter.go b/history/filter.go deleted file mode 100644 index c6d6527..0000000 --- a/history/filter.go +++ /dev/null @@ -1,75 +0,0 @@ -// part of the history package, provides filtering capabilities. -package history - -import ( - "golang.org/x/net/publicsuffix" - "log" - "math/rand" - "net/url" - "regexp" -) - -var ( - internal_re = regexp.MustCompile(`\.corp|\.sandbox\.|\.borg\.|\.hot\.|internal|dmz|\._[ut][dc]p\.|intra|\.\w$|\.\w{5,}$`) -) - -func isPossiblyInternal(addr string) bool { - // note: this happens to reject IPs and anything with a port at the end. - _, icann := publicsuffix.PublicSuffix(addr) - if !icann { - log.Printf("%s does not have a public suffix", addr) - return true - } - if internal_re.MatchString(addr) { - log.Printf("%s may be internal.", addr) - return true - } - return false -} - -// Filter out external hostnames from history -func ExternalHostnames(entries []string) (hostnames []string) { - counter := 0 - - for _, uString := range entries { - u, err := url.ParseRequestURI(uString) - if err != nil { - log.Printf("Error parsing %s: %s", uString, err) - continue - } - if !isPossiblyInternal(u.Host) { - counter += 1 - hostnames = append(hostnames, u.Host) - } - } - return -} - -// Filter input array for unique entries. -func Uniq(input []string) (output []string) { - last := "" - for _, i := range input { - if i != last { - output = append(output, i) - } - } - return -} - -// Randomly select X number of entries. -func Random(count int, input []string) (output []string) { - selected := make(map[int]bool) - - for { - if len(selected) >= count { - return - } - index := rand.Intn(len(input)) - // If we have already picked this number, re-roll. - if _, exists := selected[index]; exists == true { - continue - } - output = append(output, input[index]) - selected[index] = true - } -} diff --git a/history/history.go b/history/history.go deleted file mode 100644 index 37f8c3e..0000000 --- a/history/history.go +++ /dev/null @@ -1,85 +0,0 @@ -// the history package is a collection of functions for reading history files from browsers. -package history - -import ( - "database/sql" - "fmt" - _ "github.com/mattn/go-sqlite3" - "io" - "io/ioutil" - "log" - "os" -) - -// unlockDatabase is a bad hack for opening potentially locked SQLite databases. -func unlockDatabase(path string) (unlocked_path string, err error) { - f, err := os.Open(path) - if err != nil { - return "", err - } - defer f.Close() - - t, err := ioutil.TempFile("", "") - if err != nil { - return "", err - } - defer t.Close() - - written, err := io.Copy(t, f) - if err != nil { - return "", err - } - log.Printf("%d bytes written to %s", written, t.Name()) - return t.Name(), err -} - -// Chrome returns an array of URLs found in Chrome's history within X days -func Chrome(days int) (urls []string, err error) { - paths := []string{ - "${HOME}/Library/Application Support/Google/Chrome/Default/History", - "${HOME}/.config/google-chrome/Default/History", - "${APPDATA}/Google/Chrome/User Data/Default/History", - "${USERPROFILE}/Local Settings/Application Data/Google/Chrome/User Data/Default/History", - } - - query := fmt.Sprintf( - `SELECT urls.url FROM visits - LEFT JOIN urls ON visits.url = urls.id - WHERE (visit_time - 11644473600000000 > - strftime('%%s', date('now', '-%d day')) * 1000000) - ORDER BY visit_time DESC`, days) - - for _, p := range paths { - path := os.ExpandEnv(p) - log.Printf("Checking %s", path) - _, err := os.Stat(path) - if err != nil { - continue - } - - unlocked_path, err := unlockDatabase(path) - if err != nil { - return nil, err - } - defer os.Remove(unlocked_path) - - db, err := sql.Open("sqlite3", unlocked_path) - if err != nil { - return nil, err - } - - rows, err := db.Query(query) - if err != nil { - log.Printf("Query failed: %s", err) - return nil, err - } - var url string - for rows.Next() { - rows.Scan(&url) - urls = append(urls, url) - } - rows.Close() - return urls, err - } - return -} diff --git a/main.go b/main.go new file mode 100644 index 0000000..77f24ea --- /dev/null +++ b/main.go @@ -0,0 +1,79 @@ +package main + +import ( + "flag" + "fmt" + "github.com/Cellularhacker/core-go" + "github.com/Cellularhacker/logger-go" + "github.com/Cellularhacker/util-go/uStrings" + "namebench/ui" + "net" + "net/http" + "os" + "os/exec" +) + +var nwPath = flag.String("nw_path", "/Applications/nwjs.app/Contents/MacOS/nwjs", + "Path to nodejs-webkit binary") +var nwPackage = flag.String("nw_package", "./ui/nw/app.nw", "Path to nw.js package") +var port = flag.Int("port", 0, "Port to listen on") +var mode = flag.String("mode", "", "Use for testing immediately, put '-mode now', '-mode extract_hostname'") +var joinStr = flag.String("join_string", " ", "Use with '-mode now'. default value is ' '") +var dnsFilter = flag.Int("dns_filter", 0, "0: All, 1: Non-ISP Only, 2: ISP Only") + +func init() { + logger.Init(core.IsPM()) + core.SetLoc(core.LocationKST()) +} + +// openWindow opens a nodejs-webkit window, and points it at the given URL. +func openWindow(url string) error { + os.Setenv("APP_URL", url) + cmd := exec.Command(*nwPath, *nwPackage) + if err := cmd.Run(); err != nil { + logger.L.Errorf("error running %s %s: %s", *nwPath, *nwPackage, err) + return err + } + return nil +} + +func main() { + flag.Parse() + ui.RegisterHandlers() + + // MARK: Unescape + *joinStr = uStrings.UnescapeAllEscapingCharacters(*joinStr) + + if *mode == "now" { + result := ui.DoDnsSec(*dnsFilter) + fmt.Println(result.StringWith(*joinStr)) + return + } + + if *mode == "extract_hostname" { + drs, err := ui.DoSubmit() + if err != nil { + logger.L.Fatal(err) + } + + fmt.Println(drs.ExtractRecords().String()) + return + } + + if *port != 0 { + logger.L.Infof("Listening at :%d", *port) + err := http.ListenAndServe(fmt.Sprintf(":%d", *port), nil) + if err != nil { + logger.L.Fatalf("Failed to listen on %d: %s", *port, err) + } + } else { + listener, err := net.Listen("tcp4", "127.0.0.1:0") + if err != nil { + logger.L.Fatalf("Failed to listen: %s", err) + } + url := fmt.Sprintf("http://%s/", listener.Addr().String()) + logger.L.Infof("URL: %s", url) + go openWindow(url) + logger.L.Fatal(http.Serve(listener, nil)) + } +} diff --git a/model/namebench/record/record.go b/model/namebench/record/record.go new file mode 100644 index 0000000..0810f09 --- /dev/null +++ b/model/namebench/record/record.go @@ -0,0 +1,34 @@ +package record + +import ( + "fmt" + "strings" +) + +type Record struct { + Type string `json:"type"` + Name string `json:"name"` +} + +func (r *Record) String() string { + return fmt.Sprintf("%s", r.Name) +} + +func New() *Record { + return &Record{} +} + +type Records []Record + +func (rs *Records) String() string { + return rs.StringWith("\n") +} + +func (rs *Records) StringWith(joinStr string) string { + result := make([]string, 0) + for _, r := range *rs { + result = append(result, r.String()) + } + + return strings.Join(result, joinStr) +} diff --git a/model/namebench/record/store.go b/model/namebench/record/store.go new file mode 100644 index 0000000..ae6e867 --- /dev/null +++ b/model/namebench/record/store.go @@ -0,0 +1,20 @@ +package record + +var Default = &Record{Type: "A", Name: "www.dnssec-failed.org."} + +func Unique(records []Record) []Record { + result := make([]Record, 0) + rHostnameMap := make(map[string]bool) + + for _, r := range records { + n := r.Name + if _, e := rHostnameMap[n]; e { + continue + } + + rHostnameMap[n] = true + result = append(result, r) + } + + return result +} diff --git a/namebench.go b/namebench.go deleted file mode 100644 index c1c4463..0000000 --- a/namebench.go +++ /dev/null @@ -1,51 +0,0 @@ -package main - -import ( - "flag" - "fmt" - "log" - "net" - "net/http" - "os" - "os/exec" - - "github.com/google/namebench/ui" -) - -var nw_path = flag.String("nw_path", "/Applications/node-webkit.app/Contents/MacOS/node-webkit", - "Path to nodejs-webkit binary") -var nw_package = flag.String("nw_package", "./ui/app.nw", "Path to nodejs-webkit package") -var port = flag.Int("port", 0, "Port to listen on") - -// openWindow opens a nodejs-webkit window, and points it at the given URL. -func openWindow(url string) (err error) { - os.Setenv("APP_URL", url) - cmd := exec.Command(*nw_path, *nw_package) - if err := cmd.Run(); err != nil { - log.Printf("error running %s %s: %s", *nw_path, *nw_package, err) - return err - } - return -} - -func main() { - flag.Parse() - ui.RegisterHandlers() - - if *port != 0 { - log.Printf("Listening at :%d", *port) - err := http.ListenAndServe(fmt.Sprintf(":%d", *port), nil) - if err != nil { - log.Fatalf("Failed to listen on %d: %s", *port, err) - } - } else { - listener, err := net.Listen("tcp4", "127.0.0.1:0") - if err != nil { - log.Fatalf("Failed to listen: %s", err) - } - url := fmt.Sprintf("http://%s/", listener.Addr().String()) - log.Printf("URL: %s", url) - go openWindow(url) - panic(http.Serve(listener, nil)) - } -} diff --git a/service/dnschecks/dnschecks.go b/service/dnschecks/dnschecks.go new file mode 100644 index 0000000..aece593 --- /dev/null +++ b/service/dnschecks/dnschecks.go @@ -0,0 +1,221 @@ +package dnschecks + +import ( + "fmt" + "github.com/Cellularhacker/logger-go" + "namebench/model/namebench/record" + "namebench/service/dnsqueue" + "net" + "sort" + "strings" + "time" +) + +const ( + Primary = "Primary" + Secondary = "Secondary" +) + +type DnsServer struct { + IP net.IP `json:"ip"` + Port int `json:"port"` + Name string `json:"name"` + IsPrimary bool `json:"is_primary"` + IsISP bool `json:"is_isp"` +} + +func (ds *DnsServer) Address() string { + return fmt.Sprintf("%s:%d", string(ds.IP), ds.Port) +} + +func (ds *DnsServer) GetName() string { + pStr := Primary + if !ds.IsPrimary { + pStr = Secondary + } + if ds.IsISP { + pStr += ", ISP" + } + + return fmt.Sprintf("%s(%s)", ds.Name, pStr) +} + +type CheckResult struct { + DnsServer *DnsServer `json:"dns_server"` + Timer *Timer `json:"timer"` + DnsSec bool `json:"dns_sec"` + Record *record.Record `json:"record,omitempty"` +} + +func (cr *CheckResult) DNSSEC() string { + if cr.DnsSec { + return "O" + } + return "X" +} + +func (cr *CheckResult) String() string { + return cr.StringWith(" ") +} + +func (cr *CheckResult) StringWith(joinStr string) string { + if cr.DnsServer == nil || cr.Timer == nil { + return "" + } + + addrStr := fmt.Sprintf("Address: %s,", cr.DnsServer.Address()) + if len(addrStr) < 24 && joinStr == "\t" { + if len(addrStr) < 16 { + addrStr += "\t" + } + addrStr += "\t" + } + tookStr := fmt.Sprintf("Took: %s", cr.Timer.Took.String()) + if len(tookStr) < 16 && joinStr == "\t" { + if len(tookStr) < 8 { + tookStr += "\t" + } + tookStr += "\t" + } + + strs := []string{ + addrStr, + fmt.Sprintf("DNSSEC: %s,", cr.DNSSEC()), + tookStr, + fmt.Sprintf("- %s", cr.DnsServer.GetName()), + } + + return strings.Join(strs, joinStr) +} + +type CheckResults []CheckResult + +func (crs *CheckResults) Sort() { + if crs == nil { + return + } + + sort.Slice(*crs, func(i, j int) bool { + return (*crs)[i].Timer.Took < (*crs)[j].Timer.Took + }) +} + +func (crs *CheckResults) String() string { + return crs.StringWith(" ") +} + +func (crs *CheckResults) StringWith(joinStr string) string { + if crs == nil { + return "" + } + strs := make([]string, 0) + + for i := range *crs { + cr := (*crs)[i] + + crStr := fmt.Sprintf("[%02d/%d] %s", i+1, len(*crs), cr.StringWith(joinStr)) + strs = append(strs, crStr) + } + + return strings.Join(strs, "\n") +} + +func NewDnsServerWithValue(ip string, port int, name string, isPrimary bool, isISP bool) *DnsServer { + return &DnsServer{ + IP: []byte(ip), + Port: port, + Name: name, + IsPrimary: isPrimary, + IsISP: isISP, + } +} + +var DnsServers = []*DnsServer{ + NewDnsServerWithValue("8.8.8.8", 53, "Google Public DNS", true, false), + NewDnsServerWithValue("8.8.4.4", 53, "Google Public DNS", false, false), + NewDnsServerWithValue("75.75.75.75", 53, "Comcast DNS", true, true), + NewDnsServerWithValue("4.2.2.1", 53, "Raytheon BBN DNS", true, false), + NewDnsServerWithValue("208.67.222.222", 53, "Cisco OpenDNS", true, false), + NewDnsServerWithValue("208.67.222.220", 53, "Cisco OpenDNS", false, false), + NewDnsServerWithValue("168.126.63.1:53", 53, "KT", true, true), + NewDnsServerWithValue("168.126.63.2:53", 53, "KT", false, true), + NewDnsServerWithValue("210.220.163.82", 53, "SK Broadband", true, true), + NewDnsServerWithValue("219.250.36.130", 53, "SK Broadband", false, true), + NewDnsServerWithValue("61.41.153.2", 53, "LG DACOM", true, true), + NewDnsServerWithValue("1.214.68.2", 53, "LG DACOM", false, true), + NewDnsServerWithValue("164.124.101.2", 53, "LG DACOM(ex-LG Powercomm)", true, true), + NewDnsServerWithValue("203.248.252.2", 53, "LG DACOM(ex-LG Powercomm)", false, true), + NewDnsServerWithValue("180.182.54.1", 53, "LG HelloVision Corp.", true, true), + NewDnsServerWithValue("180.182.54.2", 53, "LG HelloVision Corp.", false, true), + NewDnsServerWithValue("9.9.9.9", 53, "IBM Quad9", true, false), + NewDnsServerWithValue("149.112.112.112", 53, "IBM Quad9", false, false), + NewDnsServerWithValue("194.242.2.2", 53, "Mullvad VPN", true, false), + NewDnsServerWithValue("193.19.108.2", 53, "Mullvad VPN", false, false), + NewDnsServerWithValue("185.222.222.222", 53, "DNS.SB", true, false), + NewDnsServerWithValue("45.11.45.11", 53, "DNS.SB", false, false), + NewDnsServerWithValue("182.172.225.180", 53, "DLive", true, true), + NewDnsServerWithValue("203.246.162.253", 53, "DLive", false, true), + NewDnsServerWithValue("1.1.1.1", 53, "Cloudflare DNS", true, false), + NewDnsServerWithValue("1.0.0.1", 53, "Cloudflare DNS", false, false), +} + +type Timer struct { + StartedAt time.Time `json:"started_at,omitempty"` + EndedAt time.Time `json:"ended_at,omitempty"` + Took time.Duration `json:"took,omitempty"` +} + +func (t *Timer) Stop() { + t.EndedAt = time.Now() +} + +func (t *Timer) StopAndReturn() *Timer { + if t.EndedAt.IsZero() { + t.Stop() + } + t.Took = t.EndedAt.Sub(t.StartedAt) + + return t +} + +func (t *Timer) StopAndReturnToResult(ds *DnsServer, dnsSec bool) *CheckResult { + t.StopAndReturn() + + return &CheckResult{ + DnsServer: ds, + Timer: t, + DnsSec: dnsSec, + } +} + +func NewTimer() *Timer { + return &Timer{ + StartedAt: time.Now(), + } +} + +func DnsSec(ds *DnsServer, records ...*record.Record) (*CheckResult, error) { + t := NewTimer() + + rec := record.Default + if len(records) > 0 { + rec = records[0] + } + + r := &dnsqueue.Request{ + Destination: ds.Address(), + RecordType: rec.Type, + RecordName: rec.Name, + VerifySignature: true, + } + result, err := dnsqueue.SendQuery(r) + for _, answer := range result.Answers { + // TODO(tstromberg): Implement properly. + if strings.Contains(answer.String, "RRSIG") { + logger.L.Debugf("DnsSec for %s: true", ds.Address()) + return t.StopAndReturnToResult(ds, true), err + } + } + logger.L.Debugf("DnsSec for %s: false", ds.Address()) + return t.StopAndReturnToResult(ds, false), err +} diff --git a/service/dnsqueue/dnsqueue.go b/service/dnsqueue/dnsqueue.go new file mode 100644 index 0000000..1122cc6 --- /dev/null +++ b/service/dnsqueue/dnsqueue.go @@ -0,0 +1,175 @@ +// Package dnsqueue is a library for queueing up a large number of DNS requests. +package dnsqueue + +import ( + "fmt" + "github.com/Cellularhacker/logger-go" + "github.com/goccy/go-json" + "github.com/miekg/dns" + "namebench/model/namebench/record" + "time" +) + +// Request contains data for making a DNS request +type Request struct { + Destination string `json:"destination"` + RecordType string `json:"record_type"` + RecordName string `json:"record_name"` + VerifySignature bool `json:"verify_signature"` + + exit bool +} + +func (r *Request) ToJSON() []byte { + bs, _ := json.Marshal(r) + return bs +} + +func (r *Request) String() string { + return string(r.ToJSON()) +} + +// Answer contains a single answer returned by a DNS server. +type Answer struct { + Ttl uint32 `json:"ttl"` + String string `json:"string"` + Record record.Record `json:"record"` +} + +// Result contains metadata relating to a set of DNS server results. +type Result struct { + Request Request `json:"request"` + Duration time.Duration `json:"duration"` + Answers []Answer `json:"answers"` + Error error `json:"error,omitempty"` +} + +func (r *Result) ToJSON() []byte { + bs, _ := json.Marshal(r) + return bs +} + +func (r *Result) String() string { + return string(r.ToJSON()) +} + +func (r *Result) ExtractRecords() []record.Record { + result := make([]record.Record, 0) + for _, a := range r.Answers { + result = append(result, a.Record) + } + + return record.Unique(result) +} + +type Results []Result + +func (rs *Results) ExtractRecords() *record.Records { + result := make(record.Records, 0) + + for _, r := range *rs { + result = append(result, r.ExtractRecords()...) + } + result = record.Unique(result) + + return &result +} + +// Queue contains methods and state for setting up a request queue. +type Queue struct { + Requests chan *Request + Results chan *Result + WorkerCount int + Quit chan bool +} + +// StartQueue starts a new queue with max length of X with worker count Y. +func StartQueue(size, workers int) *Queue { + q := &Queue{ + Requests: make(chan *Request, size), + Results: make(chan *Result, size), + WorkerCount: workers, + } + for i := 0; i < q.WorkerCount; i++ { + go startWorker(q.Requests, q.Results) + } + + return q +} + +// Add Queue.Add adds a request to the queue. Only blocks if queue is full. +func (q *Queue) Add(dest, recordType, recordName string) { + q.Requests <- &Request{ + Destination: dest, + RecordType: recordType, + RecordName: recordName, + } +} + +// SendCompletionSignal Queue.SendDieSignal sends a signal to the workers that they can go home now. +func (q *Queue) SendCompletionSignal() { + logger.L.Infof("Sending completion signal...") + for i := 0; i < q.WorkerCount; i++ { + q.Requests <- &Request{exit: true} + } +} + +// startWorker starts a thread to watch the request channel and populate result channel. +func startWorker(queue <-chan *Request, results chan<- *Result) { + for request := range queue { + if request.exit { + logger.L.Infof("Completion received, worker is done.") + return + } + result, err := SendQuery(request) + if err != nil { + logger.L.Errorf("Error sending query: %s", err) + } + logger.L.Infof("Sending back result: %s", result.String()) + results <- &result + } +} + +// SendQuery Send a DNS query via UDP, configured by a Request object. If successful, +// stores response details in Result object, otherwise, returns Result object +// with an error string. +func SendQuery(request *Request) (Result, error) { + result := Result{} + + logger.L.Debugf("Sending query: %s", request) + result.Request = *request + + recordType, ok := dns.StringToType[request.RecordType] + if !ok { + result.Error = fmt.Errorf("Invalid type: %s", request.RecordType) + return result, result.Error + } + + m := new(dns.Msg) + if request.VerifySignature == true { + logger.L.Debugf("SetEdns0 for %s", request.RecordName) + m.SetEdns0(4096, true) + } + m.SetQuestion(request.RecordName, recordType) + c := new(dns.Client) + in, rtt, err := c.Exchange(m, request.Destination) + // log.Printf("Answer: %s [%d] %s", in, rtt, err) + + result.Duration = rtt + if err != nil { + result.Error = err + } else { + for _, rr := range in.Answer { + answer := Answer{ + Ttl: rr.Header().Ttl, + String: rr.String(), + Record: record.Record{ + Type: dns.Type(rr.Header().Rrtype).String(), + Name: rr.Header().Name, + }, + } + result.Answers = append(result.Answers, answer) + } + } + return result, nil +} diff --git a/service/history/filter.go b/service/history/filter.go new file mode 100644 index 0000000..eebc974 --- /dev/null +++ b/service/history/filter.go @@ -0,0 +1,87 @@ +// Package history part of the history package, provides filtering capabilities. +package history + +import ( + "github.com/Cellularhacker/logger-go" + "golang.org/x/net/publicsuffix" + "math/rand" + "net/url" + "regexp" +) + +var ( + internalRe = regexp.MustCompile(`\.corp|\.sandbox\.|\.borg\.|\.hot\.|internal|dmz|\._[ut][dc]p\.|intra|\.\w$|\.\w{5,}$`) +) + +func isPossiblyInternal(addr string) bool { + // note: this happens to reject IPs and anything with a port at the end. + _, icann := publicsuffix.PublicSuffix(addr) + if !icann { + logger.L.Errorf("%s does not have a public suffix", addr) + return true + } + if internalRe.MatchString(addr) { + logger.L.Warnf("%s may be internal.", addr) + return true + } + return false +} + +// ExternalHostnames Filter out external hostnames from history +func ExternalHostnames(entries []string) []string { + counter := 0 + hostnames := make([]string, 0) + + for _, uString := range entries { + u, err := url.ParseRequestURI(uString) + if err != nil { + logger.L.Errorf("Error parsing %s: %s", uString, err) + continue + } + if !isPossiblyInternal(u.Hostname()) { + counter += 1 + hostnames = append(hostnames, u.Hostname()) + } + } + + return hostnames +} + +// Uniq Filter input array for unique entries. +func Uniq(input []string) []string { + inputMap := make(map[string]bool) + result := make([]string, 0) + + for _, v := range input { + if _, e := inputMap[v]; !e { + result = append(result, v) + inputMap[v] = true + } + } + return result +} + +// Random Randomly select X number of entries. +func Random(count int, input []string) []string { + if count >= len(input) { + return input + } + + selected := make(map[int]bool) + result := make([]string, 0) + + for i := 0; len(selected) < count && i < len(input); i++ { + if len(selected) >= count { + break + } + idx := rand.Intn(len(input)) + // If we have already picked this number, re-roll. + if _, e := selected[idx]; e == true { + continue + } + result = append(result, input[idx]) + selected[idx] = true + } + + return result +} diff --git a/service/history/history.go b/service/history/history.go new file mode 100644 index 0000000..498b50c --- /dev/null +++ b/service/history/history.go @@ -0,0 +1,117 @@ +// Package history the history package is a collection of functions for reading history files from browsers. +package history + +import ( + "database/sql" + "fmt" + "github.com/Cellularhacker/logger-go" + _ "github.com/mattn/go-sqlite3" + "io" + "os" + "sync" +) + +type chromePaths struct { + urls []string + m *sync.Mutex +} + +func (cps *chromePaths) Append(path string) { + cps.m.Lock() + cps.urls = append(cps.urls, path) + cps.m.Unlock() +} + +func (cps *chromePaths) Get() []string { + return cps.urls +} + +func NewChromePaths() *chromePaths { + return &chromePaths{ + urls: make([]string, 0), + m: &sync.Mutex{}, + } +} + +// unlockDatabase is a bad hack for opening potentially locked SQLite databases. +func unlockDatabase(path string) (string, error) { + f, err := os.Open(path) + if err != nil { + return "", err + } + defer f.Close() + + t, err := os.CreateTemp("", "") + if err != nil { + return "", err + } + defer t.Close() + + written, err := io.Copy(t, f) + if err != nil { + return "", err + } + logger.L.Infof("%d bytes written to %s", written, t.Name()) + return t.Name(), err +} + +// Chrome returns an array of URLs found in Chrome's history within X days +func Chrome(days int) ([]string, error) { + paths := []string{ + "${HOME}/Library/Application Support/Google/Chrome/Default/History", + "${HOME}/.config/google-chrome/Default/History", + "${APPDATA}/Google/Chrome/User Data/Default/History", + "${USERPROFILE}/Local Settings/Application Data/Google/Chrome/User Data/Default/History", + } + + cps := NewChromePaths() + query := fmt.Sprintf( + `SELECT urls.url FROM visits + LEFT JOIN urls ON visits.url = urls.id + WHERE (visit_time - 11644473600000000 > + strftime('%%s', date('now', '-%d day')) * 1000000) + ORDER BY visit_time DESC`, days) + + for _, p := range paths { + findAndAppendPath(p, query, cps) + } + return cps.Get(), nil +} + +func findAndAppendPath(cPath string, query string, cps *chromePaths) { + p := os.ExpandEnv(cPath) + logger.L.Infof("Checking %s", cPath) + _, err := os.Stat(p) + if err != nil { + logger.L.Errorln("os.Stat(p)", err) + return + } + + unlockedPath, err := unlockDatabase(p) + if err != nil { + logger.L.Errorln("unlockDatabase(p)", err) + return + } + defer os.Remove(unlockedPath) + + db, err := sql.Open("sqlite3", unlockedPath) + if err != nil { + logger.L.Errorln(`sql.Open("sqlite3", unlockedPath)`, err) + return + } + + rows, err := db.Query(query) + if err != nil { + logger.L.Errorf("Query failed: %s", err) + return + } + defer rows.Close() + + for rows.Next() { + url := "" + if err2 := rows.Scan(&url); err2 != nil { + continue + } + cps.Append(url) + } +} diff --git a/ui/nodejs-webkit/app.nw b/ui/nw/app.nw similarity index 100% rename from ui/nodejs-webkit/app.nw rename to ui/nw/app.nw diff --git a/ui/nodejs-webkit/package.json b/ui/nw/package.json similarity index 100% rename from ui/nodejs-webkit/package.json rename to ui/nw/package.json diff --git a/ui/nodejs-webkit/redirect.html b/ui/nw/redirect.html similarity index 54% rename from ui/nodejs-webkit/redirect.html rename to ui/nw/redirect.html index eb61ab7..4e8396b 100644 --- a/ui/nodejs-webkit/redirect.html +++ b/ui/nw/redirect.html @@ -1,5 +1,6 @@ - - + + + Waiting for page...