Compare commits
46 Commits
Author | SHA1 | Date | |
---|---|---|---|
8c2efdb285 | |||
194d727f4f | |||
8ab65fb55f | |||
5bce912e89 | |||
16910bb637 | |||
e59b9d6663 | |||
efc115e961 | |||
471fa212ef | |||
07ef2f9090 | |||
f7550d649a | |||
dbc25774c2 | |||
430670ab54 | |||
f6789fa245 | |||
81fbfe4cb3 | |||
6931d63250 | |||
71e87d3cd3 | |||
0ffbb618fa | |||
c01f190fc8 | |||
ad98dd7f01 | |||
0d04996f06 | |||
98dacbe849 | |||
fedb425381 | |||
ca707d17ea | |||
13240109b6 | |||
fa41e9c46c | |||
5f4bff0155 | |||
073bcea565 | |||
6244d4aa74 | |||
cf687f0bd3 | |||
6fad74c0a5 | |||
248001ec8a | |||
ba7d88dfc5 | |||
d48f1f058d | |||
46caa05d20 | |||
c5bfb96454 | |||
f97e0526d7 | |||
ce4805452f | |||
b17c70bc0a | |||
8712151e8f | |||
05b08f7097 | |||
5c7165ebf3 | |||
8b04747855 | |||
81ae8f55da | |||
5159c860d1 | |||
76b78d43e2 | |||
83f1439a6a |
@ -1,4 +1,3 @@
|
|||||||
**/rice-box.go
|
|
||||||
/admin/.env
|
/admin/.env
|
||||||
/admin/.next
|
/admin/.next
|
||||||
/admin/dist
|
/admin/dist
|
||||||
|
3
.github/FUNDING.yml
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# These are supported funding model platforms
|
||||||
|
|
||||||
|
patreon: dstotijn
|
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Create a report to help us improve
|
||||||
|
title: ''
|
||||||
|
labels: ''
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Describe the bug**
|
||||||
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
|
**To Reproduce**
|
||||||
|
Steps to reproduce the behavior:
|
||||||
|
1. Go to '...'
|
||||||
|
2. Click on '....'
|
||||||
|
3. Scroll down to '....'
|
||||||
|
4. See error
|
||||||
|
|
||||||
|
**Expected behavior**
|
||||||
|
A clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
|
**Screenshots**
|
||||||
|
If applicable, add screenshots to help explain your problem.
|
||||||
|
|
||||||
|
**Desktop (please complete the following information):**
|
||||||
|
- OS: [e.g. iOS]
|
||||||
|
- Browser [e.g. chrome, safari]
|
||||||
|
- Version [e.g. 22]
|
||||||
|
|
||||||
|
**Smartphone (please complete the following information):**
|
||||||
|
- Device: [e.g. iPhone6]
|
||||||
|
- OS: [e.g. iOS8.1]
|
||||||
|
- Browser [e.g. stock browser, safari]
|
||||||
|
- Version [e.g. 22]
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context about the problem here.
|
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
blank_issues_enabled: false
|
||||||
|
contact_links:
|
||||||
|
- name: Ask a question
|
||||||
|
url: https://github.com/dstotijn/hetty/discussions
|
||||||
|
about: Ask questions and discuss with other community members
|
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
name: Feature request
|
||||||
|
about: Suggest an idea for this project
|
||||||
|
title: ''
|
||||||
|
labels: ''
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Is your feature request related to a problem? Please describe.**
|
||||||
|
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||||
|
|
||||||
|
**Describe the solution you'd like**
|
||||||
|
A clear and concise description of what you want to happen.
|
||||||
|
|
||||||
|
**Describe alternatives you've considered**
|
||||||
|
A clear and concise description of any alternative solutions or features you've considered.
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context or screenshots about the feature request here.
|
5
.gitignore
vendored
@ -1,4 +1,7 @@
|
|||||||
|
.release-env
|
||||||
|
.vscode
|
||||||
**/rice-box.go
|
**/rice-box.go
|
||||||
dist
|
dist
|
||||||
hetty
|
hetty
|
||||||
hetty.bolt
|
*.pem
|
||||||
|
*.test
|
||||||
|
@ -1,29 +1,61 @@
|
|||||||
before:
|
env:
|
||||||
hooks:
|
- GO111MODULE=on
|
||||||
- make clean
|
- CGO_ENABLED=1
|
||||||
- go mod download
|
|
||||||
- go generate ./...
|
|
||||||
builds:
|
builds:
|
||||||
- main: ./cmd/hetty
|
- id: hetty-darwin-amd64
|
||||||
|
main: ./cmd/hetty
|
||||||
|
goarch:
|
||||||
|
- amd64
|
||||||
|
goos:
|
||||||
|
- darwin
|
||||||
env:
|
env:
|
||||||
- CGO_ENABLED=0
|
- CC=o64-clang
|
||||||
|
- CXX=o64-clang++
|
||||||
|
flags:
|
||||||
|
- -mod=readonly
|
||||||
|
|
||||||
|
- id: hetty-linux-amd64
|
||||||
|
main: ./cmd/hetty
|
||||||
|
goarch:
|
||||||
|
- amd64
|
||||||
goos:
|
goos:
|
||||||
- linux
|
- linux
|
||||||
|
flags:
|
||||||
|
- -mod=readonly
|
||||||
|
|
||||||
|
- id: hetty-windows-amd64
|
||||||
|
main: ./cmd/hetty
|
||||||
|
goarch:
|
||||||
|
- amd64
|
||||||
|
goos:
|
||||||
- windows
|
- windows
|
||||||
- darwin
|
env:
|
||||||
hooks:
|
- CC=x86_64-w64-mingw32-gcc
|
||||||
pre: make embed
|
- CXX=x86_64-w64-mingw32-g++
|
||||||
|
flags:
|
||||||
|
- -mod=readonly
|
||||||
|
ldflags:
|
||||||
|
- -buildmode=exe
|
||||||
|
|
||||||
archives:
|
archives:
|
||||||
- replacements:
|
-
|
||||||
darwin: Darwin
|
replacements:
|
||||||
linux: Linux
|
darwin: macOS
|
||||||
windows: Windows
|
linux: Linux
|
||||||
386: i386
|
windows: Windows
|
||||||
amd64: x86_64
|
386: i386
|
||||||
|
amd64: x86_64
|
||||||
|
format_overrides:
|
||||||
|
- goos: windows
|
||||||
|
format: zip
|
||||||
|
|
||||||
checksum:
|
checksum:
|
||||||
name_template: "checksums.txt"
|
name_template: "checksums.txt"
|
||||||
|
|
||||||
snapshot:
|
snapshot:
|
||||||
name_template: "{{ .Tag }}-next"
|
name_template: "{{ .Tag }}-next"
|
||||||
|
|
||||||
changelog:
|
changelog:
|
||||||
sort: asc
|
sort: asc
|
||||||
filters:
|
filters:
|
||||||
|
76
CODE_OF_CONDUCT.md
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
# Contributor Covenant Code of Conduct
|
||||||
|
|
||||||
|
## Our Pledge
|
||||||
|
|
||||||
|
In the interest of fostering an open and welcoming environment, we as
|
||||||
|
contributors and maintainers pledge to making participation in our project and
|
||||||
|
our community a harassment-free experience for everyone, regardless of age, body
|
||||||
|
size, disability, ethnicity, sex characteristics, gender identity and expression,
|
||||||
|
level of experience, education, socio-economic status, nationality, personal
|
||||||
|
appearance, race, religion, or sexual identity and orientation.
|
||||||
|
|
||||||
|
## Our Standards
|
||||||
|
|
||||||
|
Examples of behavior that contributes to creating a positive environment
|
||||||
|
include:
|
||||||
|
|
||||||
|
* Using welcoming and inclusive language
|
||||||
|
* Being respectful of differing viewpoints and experiences
|
||||||
|
* Gracefully accepting constructive criticism
|
||||||
|
* Focusing on what is best for the community
|
||||||
|
* Showing empathy towards other community members
|
||||||
|
|
||||||
|
Examples of unacceptable behavior by participants include:
|
||||||
|
|
||||||
|
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||||
|
advances
|
||||||
|
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||||
|
* Public or private harassment
|
||||||
|
* Publishing others' private information, such as a physical or electronic
|
||||||
|
address, without explicit permission
|
||||||
|
* Other conduct which could reasonably be considered inappropriate in a
|
||||||
|
professional setting
|
||||||
|
|
||||||
|
## Our Responsibilities
|
||||||
|
|
||||||
|
Project maintainers are responsible for clarifying the standards of acceptable
|
||||||
|
behavior and are expected to take appropriate and fair corrective action in
|
||||||
|
response to any instances of unacceptable behavior.
|
||||||
|
|
||||||
|
Project maintainers have the right and responsibility to remove, edit, or
|
||||||
|
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||||
|
that are not aligned to this Code of Conduct, or to ban temporarily or
|
||||||
|
permanently any contributor for other behaviors that they deem inappropriate,
|
||||||
|
threatening, offensive, or harmful.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This Code of Conduct applies both within project spaces and in public spaces
|
||||||
|
when an individual is representing the project or its community. Examples of
|
||||||
|
representing a project or community include using an official project e-mail
|
||||||
|
address, posting via an official social media account, or acting as an appointed
|
||||||
|
representative at an online or offline event. Representation of a project may be
|
||||||
|
further defined and clarified by project maintainers.
|
||||||
|
|
||||||
|
## Enforcement
|
||||||
|
|
||||||
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||||
|
reported by contacting the project team at dstotijn@gmail.com. All
|
||||||
|
complaints will be reviewed and investigated and will result in a response that
|
||||||
|
is deemed necessary and appropriate to the circumstances. The project team is
|
||||||
|
obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||||
|
Further details of specific enforcement policies may be posted separately.
|
||||||
|
|
||||||
|
Project maintainers who do not follow or enforce the Code of Conduct in good
|
||||||
|
faith may face temporary or permanent repercussions as determined by other
|
||||||
|
members of the project's leadership.
|
||||||
|
|
||||||
|
## Attribution
|
||||||
|
|
||||||
|
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
||||||
|
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
||||||
|
|
||||||
|
[homepage]: https://www.contributor-covenant.org
|
||||||
|
|
||||||
|
For answers to common questions about this code of conduct, see
|
||||||
|
https://www.contributor-covenant.org/faq
|
35
CONTRIBUTING.md
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
# Contribution Guidelines
|
||||||
|
|
||||||
|
Thank you for taking an interest in Hetty! If you want to contribute to the
|
||||||
|
project, please read the guidelines below to ensure a smooth develop experience.
|
||||||
|
|
||||||
|
## Code of conduct
|
||||||
|
|
||||||
|
Please first read the [code of conduct](CODE_OF_CONDUCT.md), and abide to it
|
||||||
|
whenever you interact with the community.
|
||||||
|
|
||||||
|
## Issues
|
||||||
|
|
||||||
|
Use [issues](https://github.com/dstotijn/hetty/issues) for reporting bugs,
|
||||||
|
adding feature requests and giving context to PRs you submit. Please use [labels](https://github.com/dstotijn/hetty/labels)
|
||||||
|
in favor of category prefixes in issue titles. To keep the issue tracker
|
||||||
|
focused on development, use [discussions](https://github.com/dstotijn/hetty/discussions)
|
||||||
|
for usage questions and non-code related discourse.
|
||||||
|
|
||||||
|
Before submitting new feature requests, check out the Kanban board for the
|
||||||
|
status of on-going work. There might already be a card/issue.
|
||||||
|
|
||||||
|
## Pull requests
|
||||||
|
|
||||||
|
Before submitting a pull request that introduces a new feature or significantly
|
||||||
|
changes the behavior of Hetty, please consider first using [discussions](https://github.com/dstotijn/hetty/discussions)
|
||||||
|
or commenting on a relevant existing issue to share what you have in mind.
|
||||||
|
Because the project is in an early stage, this is especially important; there
|
||||||
|
are still a lot of major design decisions to be made. Until the foundation has
|
||||||
|
solidified, design and implementation leading up to the first milestone (v1.0)
|
||||||
|
is highly in flux, and your work might not align/be applicable for what the
|
||||||
|
maintainers have envisioned.
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
_Todo: Write steps for setting up local development environment._
|
@ -1,14 +1,16 @@
|
|||||||
ARG GO_VERSION=1.15
|
ARG GO_VERSION=1.15
|
||||||
ARG CGO_ENABLED=0
|
ARG CGO_ENABLED=1
|
||||||
ARG NODE_VERSION=14.11
|
ARG NODE_VERSION=14.11
|
||||||
|
|
||||||
FROM golang:${GO_VERSION}-alpine AS go-builder
|
FROM golang:${GO_VERSION}-alpine AS go-builder
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
RUN apk add --no-cache build-base
|
||||||
COPY go.mod go.sum ./
|
COPY go.mod go.sum ./
|
||||||
RUN go mod download
|
RUN go mod download
|
||||||
COPY cmd ./cmd
|
COPY cmd ./cmd
|
||||||
COPY pkg ./pkg
|
COPY pkg ./pkg
|
||||||
RUN go build -o hetty ./cmd/hetty
|
RUN rm -f cmd/hetty/rice-box.go
|
||||||
|
RUN go build ./cmd/hetty
|
||||||
|
|
||||||
FROM node:${NODE_VERSION}-alpine AS node-builder
|
FROM node:${NODE_VERSION}-alpine AS node-builder
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
@ -25,4 +27,4 @@ COPY --from=node-builder /app/dist admin
|
|||||||
|
|
||||||
ENTRYPOINT ["./hetty", "-adminPath=./admin"]
|
ENTRYPOINT ["./hetty", "-adminPath=./admin"]
|
||||||
|
|
||||||
EXPOSE 80
|
EXPOSE 8080
|
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2020 David Stotijn
|
||||||
|
|
||||||
|
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.
|
42
Makefile
@ -1,20 +1,34 @@
|
|||||||
setup:
|
PACKAGE_NAME := github.com/dstotijn/hetty
|
||||||
go mod download
|
GOLANG_CROSS_VERSION ?= v1.15.2
|
||||||
go generate ./...
|
|
||||||
.PHONY: setup
|
|
||||||
|
|
||||||
embed:
|
|
||||||
go install github.com/GeertJohan/go.rice/rice
|
|
||||||
cd cmd/hetty && rice embed-go
|
|
||||||
.PHONY: embed
|
.PHONY: embed
|
||||||
|
embed:
|
||||||
|
NEXT_TELEMETRY_DISABLED=1 cd admin && yarn install && yarn run export
|
||||||
|
cd cmd/hetty && rice embed-go
|
||||||
|
|
||||||
build: embed
|
|
||||||
go build ./cmd/hetty
|
|
||||||
.PHONY: build
|
.PHONY: build
|
||||||
|
build: embed
|
||||||
|
CGO_ENABLED=1 go build ./cmd/hetty
|
||||||
|
|
||||||
clean:
|
.PHONY: release-dry-run
|
||||||
rm -rf cmd/hetty/rice-box.go
|
release-dry-run: embed
|
||||||
.PHONY: clean
|
@docker run \
|
||||||
|
--rm \
|
||||||
|
-v `pwd`:/go/src/$(PACKAGE_NAME) \
|
||||||
|
-w /go/src/$(PACKAGE_NAME) \
|
||||||
|
troian/golang-cross:${GOLANG_CROSS_VERSION} \
|
||||||
|
--rm-dist --skip-validate --skip-publish
|
||||||
|
|
||||||
release:
|
.PHONY: release
|
||||||
goreleaser -p 1
|
release: embed
|
||||||
|
@if [ ! -f ".release-env" ]; then \
|
||||||
|
echo "\033[91mFile \`.release-env\` is missing.\033[0m";\
|
||||||
|
exit 1;\
|
||||||
|
fi
|
||||||
|
@docker run \
|
||||||
|
--rm \
|
||||||
|
-v `pwd`:/go/src/$(PACKAGE_NAME) \
|
||||||
|
-w /go/src/$(PACKAGE_NAME) \
|
||||||
|
--env-file .release-env \
|
||||||
|
troian/golang-cross:${GOLANG_CROSS_VERSION} \
|
||||||
|
release --rm-dist
|
237
README.md
Normal file
@ -0,0 +1,237 @@
|
|||||||
|
<h1>
|
||||||
|
<a href="https://github.com/dstotijn/hetty">
|
||||||
|
<img src="https://hetty.xyz/assets/logo.png" width="293">
|
||||||
|
</a>
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
[](https://github.com/dstotijn/hetty/releases/latest)
|
||||||
|

|
||||||
|
[](https://github.com/dstotijn/hetty/blob/master/LICENSE)
|
||||||
|
[](https://hetty.xyz/)
|
||||||
|
|
||||||
|
**Hetty** is an HTTP toolkit for security research. It aims to become an open
|
||||||
|
source alternative to commercial software like Burp Suite Pro, with powerful
|
||||||
|
features tailored to the needs of the infosec and bug bounty community.
|
||||||
|
|
||||||
|
<img src="https://hetty.xyz/assets/hetty_v0.2.0_header.png">
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Man-in-the-middle (MITM) HTTP/1.1 proxy with logs
|
||||||
|
- Project based database storage (SQLite)
|
||||||
|
- Scope support
|
||||||
|
- Headless management API using GraphQL
|
||||||
|
- Embedded web interface (Next.js)
|
||||||
|
|
||||||
|
ℹ️ Hetty is in early development. Additional features are planned
|
||||||
|
for a `v1.0` release. Please see the <a href="https://github.com/dstotijn/hetty/projects/1">backlog</a>
|
||||||
|
for details.
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
📖 [Read the docs.](https://hetty.xyz/)
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Hetty compiles to a self-contained binary, with an embedded SQLite database
|
||||||
|
and web based admin interface.
|
||||||
|
|
||||||
|
### Install pre-built release (recommended)
|
||||||
|
|
||||||
|
👉 Downloads for Linux, macOS and Windows are available on the [releases page](https://github.com/dstotijn/hetty/releases).
|
||||||
|
|
||||||
|
### Build from source
|
||||||
|
|
||||||
|
#### Prerequisites
|
||||||
|
|
||||||
|
- [Go](https://golang.org/)
|
||||||
|
- [Yarn](https://yarnpkg.com/)
|
||||||
|
- [go.rice](https://github.com/GeertJohan/go.rice)
|
||||||
|
|
||||||
|
Hetty depends on SQLite (via [mattn/go-sqlite3](https://github.com/mattn/go-sqlite3))
|
||||||
|
and needs `cgo` to compile. Additionally, the static resources for the admin interface
|
||||||
|
(Next.js) need to be generated via [Yarn](https://yarnpkg.com/) and embedded in
|
||||||
|
a `.go` file with [go.rice](https://github.com/GeertJohan/go.rice) beforehand.
|
||||||
|
|
||||||
|
Clone the repository and use the `build` make target to create a binary:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ git clone git@github.com:dstotijn/hetty.git
|
||||||
|
$ cd hetty
|
||||||
|
$ make build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker
|
||||||
|
|
||||||
|
A Docker image is available on Docker Hub: [`dstotijn/hetty`](https://hub.docker.com/r/dstotijn/hetty).
|
||||||
|
For persistent storage of CA certificates and project databases, mount a volume:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ mkdir -p $HOME/.hetty
|
||||||
|
$ docker run -v $HOME/.hetty:/root/.hetty -p 8080:8080 dstotijn/hetty
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
When Hetty is run, by default it listens on `:8080` and is accessible via
|
||||||
|
http://localhost:8080. Depending on incoming HTTP requests, it either acts as a
|
||||||
|
MITM proxy, or it serves the API and web interface.
|
||||||
|
|
||||||
|
By default, project database files and CA certificates are stored in a `.hetty`
|
||||||
|
directory under the user's home directory (`$HOME` on Linux/macOS, `%USERPROFILE%`
|
||||||
|
on Windows).
|
||||||
|
|
||||||
|
To start, ensure `hetty` (downloaded from a release, or manually built) is in your
|
||||||
|
`$PATH` and run:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ hetty
|
||||||
|
```
|
||||||
|
|
||||||
|
An overview of configuration flags:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ hetty -h
|
||||||
|
Usage of ./hetty:
|
||||||
|
-addr string
|
||||||
|
TCP address to listen on, in the form "host:port" (default ":8080")
|
||||||
|
-adminPath string
|
||||||
|
File path to admin build
|
||||||
|
-cert string
|
||||||
|
CA certificate filepath. Creates a new CA certificate is file doesn't exist (default "~/.hetty/hetty_cert.pem")
|
||||||
|
-key string
|
||||||
|
CA private key filepath. Creates a new CA private key if file doesn't exist (default "~/.hetty/hetty_key.pem")
|
||||||
|
-projects string
|
||||||
|
Projects directory path (default "~/.hetty/projects")
|
||||||
|
```
|
||||||
|
|
||||||
|
You should see:
|
||||||
|
|
||||||
|
```
|
||||||
|
2020/11/01 14:47:10 [INFO] Running server on :8080 ...
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, visit [http://localhost:8080](http://localhost:8080) to get started.
|
||||||
|
|
||||||
|
ℹ️ Detailed documentation is under development and will be available soon.
|
||||||
|
|
||||||
|
## Certificate Setup and Installation
|
||||||
|
|
||||||
|
In order for Hetty to proxy requests going to HTTPS endpoints, a root CA certificate for
|
||||||
|
Hetty will need to be set up. Furthermore, the CA certificate may need to be
|
||||||
|
installed to the host for them to be trusted by your browser. The following steps
|
||||||
|
will cover how you can generate your certificate, provide them to hetty, and how
|
||||||
|
you can install them in your local CA store.
|
||||||
|
|
||||||
|
⚠️ _This process was done on a Linux machine but should_
|
||||||
|
_provide guidance on Windows and macOS as well._
|
||||||
|
|
||||||
|
### Generating CA certificates
|
||||||
|
|
||||||
|
You can generate a CA keypair two different ways. The first is bundled directly
|
||||||
|
with Hetty, and simplifies the process immensely. The alternative is using OpenSSL
|
||||||
|
to generate them, which provides more control over expiration time and cryptography
|
||||||
|
used, but requires you install the OpenSSL tooling. The first is suggested for any
|
||||||
|
beginners trying to get started.
|
||||||
|
|
||||||
|
#### Generating CA certificates with hetty
|
||||||
|
|
||||||
|
Hetty will generate the default key and certificate on its own if none are supplied
|
||||||
|
or found in `~/.hetty/` when first running the CLI. To generate a default key and
|
||||||
|
certificate with hetty, simply run the command with no arguments
|
||||||
|
|
||||||
|
```sh
|
||||||
|
hetty
|
||||||
|
```
|
||||||
|
|
||||||
|
You should now have a key and certificate located at `~/.hetty/hetty_key.pem` and
|
||||||
|
`~/.hetty/hetty_cert.pem` respectively.
|
||||||
|
|
||||||
|
#### Generating CA certificates with OpenSSL
|
||||||
|
|
||||||
|
You can start off by generating a new key and CA certificate which will both expire
|
||||||
|
after a month.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
mkdir ~/.hetty
|
||||||
|
openssl req -newkey rsa:2048 -new -nodes -x509 -days 31 -keyout ~/.hetty/hetty_key.pem -out ~/.hetty/hetty_cert.pem
|
||||||
|
```
|
||||||
|
|
||||||
|
The default location which `hetty` will check for the key and CA certificate is under
|
||||||
|
`~/.hetty/`, at `hetty_key.pem` and `hetty_cert.pem` respectively. You can move them
|
||||||
|
here and `hetty` will detect them automatically. Otherwise, you can specify the
|
||||||
|
location of these as arguments to `hetty`.
|
||||||
|
|
||||||
|
```
|
||||||
|
hetty -key key.pem -cert cert.pem
|
||||||
|
```
|
||||||
|
|
||||||
|
### Trusting the CA certificate
|
||||||
|
|
||||||
|
In order for your browser to allow traffic to the local Hetty proxy, you may need
|
||||||
|
to install these certificates to your local CA store.
|
||||||
|
|
||||||
|
On Ubuntu, you can update your local CA store with the certificate by running the
|
||||||
|
following commands:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sudo cp ~/.hetty/hetty_cert.pem /usr/local/share/ca-certificates/hetty.crt
|
||||||
|
sudo update-ca-certificates
|
||||||
|
```
|
||||||
|
|
||||||
|
On Windows, you would add your certificate by using the Certificate Manager. You
|
||||||
|
can launch that by running the command:
|
||||||
|
|
||||||
|
```batch
|
||||||
|
certmgr.msc
|
||||||
|
```
|
||||||
|
|
||||||
|
On macOS, you can add your certificate by using the Keychain Access program. This
|
||||||
|
can be found under `Application/Utilities/Keychain Access.app`. After opening this,
|
||||||
|
drag the certificate into the app. Next, open the certificate in the app, enter the
|
||||||
|
_Trust_ section, and under _When using this certificate_ select _Always Trust_.
|
||||||
|
|
||||||
|
_Note: Various Linux distributions may require other steps or commands for updating_
|
||||||
|
_their certificate authority. See the documentation relevant to your distribution for_
|
||||||
|
_more information on how to update the system to trust your self-signed certificate._
|
||||||
|
|
||||||
|
## Vision and roadmap
|
||||||
|
|
||||||
|
- Fast core/engine, built with Go, with a minimal memory footprint.
|
||||||
|
- Easy to use admin interface, built with Next.js and Material UI.
|
||||||
|
- Headless management, via GraphQL API.
|
||||||
|
- Extensibility is top of mind. All modules are written as Go packages, to
|
||||||
|
be used by Hetty, but also as libraries by other software.
|
||||||
|
- Pluggable architecture for MITM proxy, projects, scope. It should be possible.
|
||||||
|
to build a plugin system in the (near) future.
|
||||||
|
- Based on feedback and real-world usage of pentesters and bug bounty hunters.
|
||||||
|
- Aim for a relatively small core feature set that the majority of security researchers need.
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
Use [issues](https://github.com/dstotijn/hetty/issues) for bug reports and
|
||||||
|
feature requests, and [discussions](https://github.com/dstotijn/hetty/discussions)
|
||||||
|
for questions and troubleshooting.
|
||||||
|
|
||||||
|
## Community
|
||||||
|
|
||||||
|
💬 [Join the Hetty Discord server](https://discord.gg/3HVsj5pTFP).
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Want to contribute? Great! Please check the [Contribution Guidelines](CONTRIBUTING.md)
|
||||||
|
for details.
|
||||||
|
|
||||||
|
## Acknowledgements
|
||||||
|
|
||||||
|
- Thanks to the [Hacker101 community on Discord](https://www.hacker101.com/discord)
|
||||||
|
for all the encouragement and feedback.
|
||||||
|
- The font used in the logo and admin interface is [JetBrains Mono](https://www.jetbrains.com/lp/mono/).
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
[MIT License](LICENSE)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
© 2020 David Stotijn — [Twitter](https://twitter.com/dstotijn), [Email](mailto:dstotijn@gmail.com)
|
@ -6,7 +6,7 @@
|
|||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"export": "next build && next export -o dist"
|
"export": "rm -rf .next && next build && next export -o dist"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apollo/client": "^3.2.0",
|
"@apollo/client": "^3.2.0",
|
||||||
@ -17,7 +17,7 @@
|
|||||||
"graphql": "^15.3.0",
|
"graphql": "^15.3.0",
|
||||||
"monaco-editor": "^0.20.0",
|
"monaco-editor": "^0.20.0",
|
||||||
"monaco-editor-webpack-plugin": "^1.9.0",
|
"monaco-editor-webpack-plugin": "^1.9.0",
|
||||||
"next": "^9.5.3",
|
"next": "^9.5.4",
|
||||||
"next-fonts": "^1.0.3",
|
"next-fonts": "^1.0.3",
|
||||||
"react": "^16.13.1",
|
"react": "^16.13.1",
|
||||||
"react-dom": "^16.13.1",
|
"react-dom": "^16.13.1",
|
||||||
|
@ -21,15 +21,20 @@ import MenuIcon from "@material-ui/icons/Menu";
|
|||||||
import HomeIcon from "@material-ui/icons/Home";
|
import HomeIcon from "@material-ui/icons/Home";
|
||||||
import SettingsEthernetIcon from "@material-ui/icons/SettingsEthernet";
|
import SettingsEthernetIcon from "@material-ui/icons/SettingsEthernet";
|
||||||
import SendIcon from "@material-ui/icons/Send";
|
import SendIcon from "@material-ui/icons/Send";
|
||||||
|
import FolderIcon from "@material-ui/icons/Folder";
|
||||||
|
import LocationSearchingIcon from "@material-ui/icons/LocationSearching";
|
||||||
import ChevronLeftIcon from "@material-ui/icons/ChevronLeft";
|
import ChevronLeftIcon from "@material-ui/icons/ChevronLeft";
|
||||||
import ChevronRightIcon from "@material-ui/icons/ChevronRight";
|
import ChevronRightIcon from "@material-ui/icons/ChevronRight";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
|
||||||
export enum Page {
|
export enum Page {
|
||||||
Home,
|
Home,
|
||||||
|
GetStarted,
|
||||||
|
Projects,
|
||||||
ProxySetup,
|
ProxySetup,
|
||||||
ProxyLogs,
|
ProxyLogs,
|
||||||
Sender,
|
Sender,
|
||||||
|
Scope,
|
||||||
}
|
}
|
||||||
|
|
||||||
const drawerWidth = 240;
|
const drawerWidth = 240;
|
||||||
@ -154,7 +159,7 @@ export function Layout({ title, page, children }: Props): JSX.Element {
|
|||||||
<MenuIcon />
|
<MenuIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<Typography variant="h5" noWrap>
|
<Typography variant="h5" noWrap>
|
||||||
<span className={title !== "" && classes.titleHighlight}>
|
<span className={title !== "" ? classes.titleHighlight : ""}>
|
||||||
Hetty://
|
Hetty://
|
||||||
</span>
|
</span>
|
||||||
{title}
|
{title}
|
||||||
@ -233,6 +238,38 @@ export function Layout({ title, page, children }: Props): JSX.Element {
|
|||||||
<ListItemText primary="Sender" />
|
<ListItemText primary="Sender" />
|
||||||
</ListItem>
|
</ListItem>
|
||||||
</Link>
|
</Link>
|
||||||
|
<Link href="/scope" passHref>
|
||||||
|
<ListItem
|
||||||
|
button
|
||||||
|
component="a"
|
||||||
|
key="scope"
|
||||||
|
selected={page === Page.Scope}
|
||||||
|
className={classes.listItem}
|
||||||
|
>
|
||||||
|
<Tooltip title="Scope">
|
||||||
|
<ListItemIcon className={classes.listItemIcon}>
|
||||||
|
<LocationSearchingIcon />
|
||||||
|
</ListItemIcon>
|
||||||
|
</Tooltip>
|
||||||
|
<ListItemText primary="Scope" />
|
||||||
|
</ListItem>
|
||||||
|
</Link>
|
||||||
|
<Link href="/projects" passHref>
|
||||||
|
<ListItem
|
||||||
|
button
|
||||||
|
component="a"
|
||||||
|
key="projects"
|
||||||
|
selected={page === Page.Projects}
|
||||||
|
className={classes.listItem}
|
||||||
|
>
|
||||||
|
<Tooltip title="Projects">
|
||||||
|
<ListItemIcon className={classes.listItemIcon}>
|
||||||
|
<FolderIcon />
|
||||||
|
</ListItemIcon>
|
||||||
|
</Tooltip>
|
||||||
|
<ListItemText primary="Projects" />
|
||||||
|
</ListItem>
|
||||||
|
</Link>
|
||||||
</List>
|
</List>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
<main className={classes.content}>
|
<main className={classes.content}>
|
||||||
|
122
admin/src/components/projects/NewProject.tsx
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
import { gql, useMutation } from "@apollo/client";
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
CircularProgress,
|
||||||
|
createStyles,
|
||||||
|
makeStyles,
|
||||||
|
TextField,
|
||||||
|
Theme,
|
||||||
|
Typography,
|
||||||
|
} from "@material-ui/core";
|
||||||
|
import AddIcon from "@material-ui/icons/Add";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme: Theme) =>
|
||||||
|
createStyles({
|
||||||
|
projectName: {
|
||||||
|
marginTop: -6,
|
||||||
|
marginRight: theme.spacing(2),
|
||||||
|
},
|
||||||
|
button: {
|
||||||
|
marginRight: theme.spacing(2),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const OPEN_PROJECT = gql`
|
||||||
|
mutation OpenProject($name: String!) {
|
||||||
|
openProject(name: $name) {
|
||||||
|
name
|
||||||
|
isActive
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
function NewProject(): JSX.Element {
|
||||||
|
const classes = useStyles();
|
||||||
|
const [input, setInput] = useState(null);
|
||||||
|
|
||||||
|
const [openProject, { error, loading }] = useMutation(OPEN_PROJECT, {
|
||||||
|
onError: () => {},
|
||||||
|
onCompleted() {
|
||||||
|
input.value = "";
|
||||||
|
},
|
||||||
|
update(cache, { data: { openProject } }) {
|
||||||
|
cache.modify({
|
||||||
|
fields: {
|
||||||
|
activeProject() {
|
||||||
|
const activeProjRef = cache.writeFragment({
|
||||||
|
id: openProject.name,
|
||||||
|
data: openProject,
|
||||||
|
fragment: gql`
|
||||||
|
fragment ActiveProject on Project {
|
||||||
|
name
|
||||||
|
isActive
|
||||||
|
type
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
return activeProjRef;
|
||||||
|
},
|
||||||
|
projects(_, { DELETE }) {
|
||||||
|
cache.writeFragment({
|
||||||
|
id: openProject.name,
|
||||||
|
data: openProject,
|
||||||
|
fragment: gql`
|
||||||
|
fragment OpenProject on Project {
|
||||||
|
name
|
||||||
|
isActive
|
||||||
|
type
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
return DELETE;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleNewProjectForm = (e: React.SyntheticEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
openProject({ variables: { name: input.value } });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Box mb={3}>
|
||||||
|
<Typography variant="h6">New project</Typography>
|
||||||
|
</Box>
|
||||||
|
<form onSubmit={handleNewProjectForm} autoComplete="off">
|
||||||
|
<TextField
|
||||||
|
className={classes.projectName}
|
||||||
|
color="secondary"
|
||||||
|
inputProps={{
|
||||||
|
id: "projectName",
|
||||||
|
ref: (node) => {
|
||||||
|
setInput(node);
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
label="Project name"
|
||||||
|
placeholder="Project name…"
|
||||||
|
error={Boolean(error)}
|
||||||
|
helperText={error && error.message}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
className={classes.button}
|
||||||
|
type="submit"
|
||||||
|
variant="contained"
|
||||||
|
color="secondary"
|
||||||
|
size="large"
|
||||||
|
disabled={loading}
|
||||||
|
startIcon={loading ? <CircularProgress size={22} /> : <AddIcon />}
|
||||||
|
>
|
||||||
|
Create & open project
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NewProject;
|
317
admin/src/components/projects/ProjectList.tsx
Normal file
@ -0,0 +1,317 @@
|
|||||||
|
import { gql, useMutation, useQuery } from "@apollo/client";
|
||||||
|
import {
|
||||||
|
Avatar,
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
CircularProgress,
|
||||||
|
createStyles,
|
||||||
|
Dialog,
|
||||||
|
DialogActions,
|
||||||
|
DialogContent,
|
||||||
|
DialogContentText,
|
||||||
|
DialogTitle,
|
||||||
|
IconButton,
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
ListItemAvatar,
|
||||||
|
ListItemSecondaryAction,
|
||||||
|
ListItemText,
|
||||||
|
makeStyles,
|
||||||
|
Snackbar,
|
||||||
|
Theme,
|
||||||
|
Tooltip,
|
||||||
|
Typography,
|
||||||
|
} from "@material-ui/core";
|
||||||
|
import CloseIcon from "@material-ui/icons/Close";
|
||||||
|
import DescriptionIcon from "@material-ui/icons/Description";
|
||||||
|
import DeleteIcon from "@material-ui/icons/Delete";
|
||||||
|
import LaunchIcon from "@material-ui/icons/Launch";
|
||||||
|
import { Alert } from "@material-ui/lab";
|
||||||
|
|
||||||
|
import React, { useState } from "react";
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme: Theme) =>
|
||||||
|
createStyles({
|
||||||
|
projectsList: {
|
||||||
|
backgroundColor: theme.palette.background.paper,
|
||||||
|
},
|
||||||
|
activeProject: {
|
||||||
|
color: theme.palette.getContrastText(theme.palette.secondary.main),
|
||||||
|
backgroundColor: theme.palette.secondary.main,
|
||||||
|
},
|
||||||
|
deleteProjectButton: {
|
||||||
|
color: theme.palette.error.main,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const PROJECTS = gql`
|
||||||
|
query Projects {
|
||||||
|
projects {
|
||||||
|
name
|
||||||
|
isActive
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const OPEN_PROJECT = gql`
|
||||||
|
mutation OpenProject($name: String!) {
|
||||||
|
openProject(name: $name) {
|
||||||
|
name
|
||||||
|
isActive
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const CLOSE_PROJECT = gql`
|
||||||
|
mutation CloseProject {
|
||||||
|
closeProject {
|
||||||
|
success
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const DELETE_PROJECT = gql`
|
||||||
|
mutation DeleteProject($name: String!) {
|
||||||
|
deleteProject(name: $name) {
|
||||||
|
success
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
function ProjectList(): JSX.Element {
|
||||||
|
const classes = useStyles();
|
||||||
|
const { loading: projLoading, error: projErr, data: projData } = useQuery(
|
||||||
|
PROJECTS
|
||||||
|
);
|
||||||
|
const [
|
||||||
|
openProject,
|
||||||
|
{ error: openProjErr, loading: openProjLoading },
|
||||||
|
] = useMutation(OPEN_PROJECT, {
|
||||||
|
errorPolicy: "all",
|
||||||
|
onError: () => {},
|
||||||
|
update(cache, { data: { openProject } }) {
|
||||||
|
cache.modify({
|
||||||
|
fields: {
|
||||||
|
activeProject() {
|
||||||
|
const activeProjRef = cache.writeFragment({
|
||||||
|
data: openProject,
|
||||||
|
fragment: gql`
|
||||||
|
fragment ActiveProject on Project {
|
||||||
|
name
|
||||||
|
isActive
|
||||||
|
type
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
return activeProjRef;
|
||||||
|
},
|
||||||
|
projects(_, { DELETE }) {
|
||||||
|
cache.writeFragment({
|
||||||
|
id: openProject.name,
|
||||||
|
data: openProject,
|
||||||
|
fragment: gql`
|
||||||
|
fragment OpenProject on Project {
|
||||||
|
name
|
||||||
|
isActive
|
||||||
|
type
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
return DELETE;
|
||||||
|
},
|
||||||
|
httpRequestLogFilter(_, { DELETE }) {
|
||||||
|
return DELETE;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const [closeProject, { error: closeProjErr }] = useMutation(CLOSE_PROJECT, {
|
||||||
|
errorPolicy: "all",
|
||||||
|
onError: () => {},
|
||||||
|
update(cache) {
|
||||||
|
cache.modify({
|
||||||
|
fields: {
|
||||||
|
activeProject() {
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
projects(_, { DELETE }) {
|
||||||
|
return DELETE;
|
||||||
|
},
|
||||||
|
httpRequestLogFilter(_, { DELETE }) {
|
||||||
|
return DELETE;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const [
|
||||||
|
deleteProject,
|
||||||
|
{ loading: deleteProjLoading, error: deleteProjErr },
|
||||||
|
] = useMutation(DELETE_PROJECT, {
|
||||||
|
errorPolicy: "all",
|
||||||
|
onError: () => {},
|
||||||
|
update(cache) {
|
||||||
|
cache.modify({
|
||||||
|
fields: {
|
||||||
|
projects(_, { DELETE }) {
|
||||||
|
return DELETE;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
setDeleteDiagOpen(false);
|
||||||
|
setDeleteNotifOpen(true);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const [deleteProjName, setDeleteProjName] = useState(null);
|
||||||
|
const [deleteDiagOpen, setDeleteDiagOpen] = useState(false);
|
||||||
|
const handleDeleteButtonClick = (name: string) => {
|
||||||
|
setDeleteProjName(name);
|
||||||
|
setDeleteDiagOpen(true);
|
||||||
|
};
|
||||||
|
const handleDeleteConfirm = () => {
|
||||||
|
deleteProject({ variables: { name: deleteProjName } });
|
||||||
|
};
|
||||||
|
const handleDeleteCancel = () => {
|
||||||
|
setDeleteDiagOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const [deleteNotifOpen, setDeleteNotifOpen] = useState(false);
|
||||||
|
const handleCloseDeleteNotif = (_, reason?: string) => {
|
||||||
|
if (reason === "clickaway") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setDeleteNotifOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Dialog open={deleteDiagOpen} onClose={handleDeleteCancel}>
|
||||||
|
<DialogTitle>
|
||||||
|
Delete project “<strong>{deleteProjName}</strong>”?
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogContentText>
|
||||||
|
Deleting a project permanently removes its database file from disk.
|
||||||
|
This action is irreversible.
|
||||||
|
</DialogContentText>
|
||||||
|
{deleteProjErr && (
|
||||||
|
<Alert severity="error">
|
||||||
|
Error closing project: {deleteProjErr.message}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={handleDeleteCancel} autoFocus>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className={classes.deleteProjectButton}
|
||||||
|
onClick={handleDeleteConfirm}
|
||||||
|
disabled={deleteProjLoading}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<Snackbar
|
||||||
|
open={deleteNotifOpen}
|
||||||
|
autoHideDuration={3000}
|
||||||
|
onClose={handleCloseDeleteNotif}
|
||||||
|
>
|
||||||
|
<Alert onClose={handleCloseDeleteNotif} severity="info">
|
||||||
|
Project <strong>{deleteProjName}</strong> was deleted.
|
||||||
|
</Alert>
|
||||||
|
</Snackbar>
|
||||||
|
|
||||||
|
<Box mb={3}>
|
||||||
|
<Typography variant="h6">Manage projects</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box mb={4}>
|
||||||
|
{projLoading && <CircularProgress />}
|
||||||
|
{projErr && (
|
||||||
|
<Alert severity="error">
|
||||||
|
Error fetching projects: {projErr.message}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
{openProjErr && (
|
||||||
|
<Alert severity="error">
|
||||||
|
Error opening project: {openProjErr.message}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
{closeProjErr && (
|
||||||
|
<Alert severity="error">
|
||||||
|
Error closing project: {closeProjErr.message}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{projData?.projects.length > 0 && (
|
||||||
|
<List className={classes.projectsList}>
|
||||||
|
{projData.projects.map((project) => (
|
||||||
|
<ListItem key={project.name}>
|
||||||
|
<ListItemAvatar>
|
||||||
|
<Avatar
|
||||||
|
className={
|
||||||
|
project.isActive ? classes.activeProject : undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<DescriptionIcon />
|
||||||
|
</Avatar>
|
||||||
|
</ListItemAvatar>
|
||||||
|
<ListItemText>
|
||||||
|
{project.name} {project.isActive && <em>(Active)</em>}
|
||||||
|
</ListItemText>
|
||||||
|
<ListItemSecondaryAction>
|
||||||
|
{project.isActive && (
|
||||||
|
<Tooltip title="Close project">
|
||||||
|
<IconButton onClick={() => closeProject()}>
|
||||||
|
<CloseIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
{!project.isActive && (
|
||||||
|
<Tooltip title="Open project">
|
||||||
|
<span>
|
||||||
|
<IconButton
|
||||||
|
disabled={openProjLoading || projLoading}
|
||||||
|
onClick={() =>
|
||||||
|
openProject({
|
||||||
|
variables: { name: project.name },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<LaunchIcon />
|
||||||
|
</IconButton>
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
<Tooltip title="Delete project">
|
||||||
|
<span>
|
||||||
|
<IconButton
|
||||||
|
onClick={() => handleDeleteButtonClick(project.name)}
|
||||||
|
disabled={project.isActive}
|
||||||
|
>
|
||||||
|
<DeleteIcon />
|
||||||
|
</IconButton>
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
</ListItemSecondaryAction>
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
)}
|
||||||
|
{projData?.projects.length === 0 && (
|
||||||
|
<Alert severity="info">
|
||||||
|
There are no projects. Create one to get started.
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ProjectList;
|
53
admin/src/components/reqlog/ConfirmationDialog.tsx
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import Button from "@material-ui/core/Button";
|
||||||
|
import Dialog from "@material-ui/core/Dialog";
|
||||||
|
import DialogActions from "@material-ui/core/DialogActions";
|
||||||
|
import DialogContent from "@material-ui/core/DialogContent";
|
||||||
|
import DialogContentText from "@material-ui/core/DialogContentText";
|
||||||
|
import DialogTitle from "@material-ui/core/DialogTitle";
|
||||||
|
|
||||||
|
export function useConfirmationDialog() {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const close = () => setIsOpen(false);
|
||||||
|
const open = () => setIsOpen(true);
|
||||||
|
|
||||||
|
return { open, close, isOpen };
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ConfirmationDialog {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onConfirm: () => void;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ConfirmationDialog(props: ConfirmationDialog) {
|
||||||
|
const { onClose, onConfirm, isOpen, children } = props;
|
||||||
|
|
||||||
|
function confirm() {
|
||||||
|
onConfirm();
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
open={isOpen}
|
||||||
|
onClose={onClose}
|
||||||
|
aria-labelledby="alert-dialog-title"
|
||||||
|
aria-describedby="alert-dialog-description"
|
||||||
|
>
|
||||||
|
<DialogTitle id="alert-dialog-title">Are you sure?</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogContentText id="alert-dialog-description">
|
||||||
|
{children}
|
||||||
|
</DialogContentText>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={onClose}>Abort</Button>
|
||||||
|
<Button onClick={confirm} autoFocus>
|
||||||
|
Confirm
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
@ -23,8 +23,8 @@ const HTTP_REQUEST_LOG = gql`
|
|||||||
key
|
key
|
||||||
value
|
value
|
||||||
}
|
}
|
||||||
status
|
|
||||||
statusCode
|
statusCode
|
||||||
|
statusReason
|
||||||
body
|
body
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -32,7 +32,7 @@ const HTTP_REQUEST_LOG = gql`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
requestId: string;
|
requestId: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
function LogDetail({ requestId: id }: Props): JSX.Element {
|
function LogDetail({ requestId: id }: Props): JSX.Element {
|
||||||
|
@ -1,38 +1,26 @@
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { gql, useQuery } from "@apollo/client";
|
import Link from "next/link";
|
||||||
import { useState } from "react";
|
import {
|
||||||
import { Box, Typography, CircularProgress } from "@material-ui/core";
|
Box,
|
||||||
|
CircularProgress,
|
||||||
|
Link as MaterialLink,
|
||||||
|
Typography,
|
||||||
|
} from "@material-ui/core";
|
||||||
import Alert from "@material-ui/lab/Alert";
|
import Alert from "@material-ui/lab/Alert";
|
||||||
|
|
||||||
import RequestList from "./RequestList";
|
import RequestList from "./RequestList";
|
||||||
import LogDetail from "./LogDetail";
|
import LogDetail from "./LogDetail";
|
||||||
import CenteredPaper from "../CenteredPaper";
|
import CenteredPaper from "../CenteredPaper";
|
||||||
|
import { useHttpRequestLogs } from "./hooks/useHttpRequestLogs";
|
||||||
const HTTP_REQUEST_LOGS = gql`
|
|
||||||
query HttpRequestLogs {
|
|
||||||
httpRequestLogs {
|
|
||||||
id
|
|
||||||
method
|
|
||||||
url
|
|
||||||
timestamp
|
|
||||||
response {
|
|
||||||
status
|
|
||||||
statusCode
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
function LogsOverview(): JSX.Element {
|
function LogsOverview(): JSX.Element {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const detailReqLogId = router.query.id as string;
|
const detailReqLogId =
|
||||||
console.log(detailReqLogId);
|
router.query.id && parseInt(router.query.id as string, 10);
|
||||||
|
|
||||||
const { loading, error, data } = useQuery(HTTP_REQUEST_LOGS, {
|
const { loading, error, data } = useHttpRequestLogs();
|
||||||
pollInterval: 1000,
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleLogClick = (reqId: string) => {
|
const handleLogClick = (reqId: number) => {
|
||||||
router.push("/proxy/logs?id=" + reqId, undefined, {
|
router.push("/proxy/logs?id=" + reqId, undefined, {
|
||||||
shallow: false,
|
shallow: false,
|
||||||
});
|
});
|
||||||
@ -42,6 +30,17 @@ function LogsOverview(): JSX.Element {
|
|||||||
return <CircularProgress />;
|
return <CircularProgress />;
|
||||||
}
|
}
|
||||||
if (error) {
|
if (error) {
|
||||||
|
if (error.graphQLErrors[0]?.extensions?.code === "no_active_project") {
|
||||||
|
return (
|
||||||
|
<Alert severity="info">
|
||||||
|
There is no project active.{" "}
|
||||||
|
<Link href="/projects" passHref>
|
||||||
|
<MaterialLink color="secondary">Create or open</MaterialLink>
|
||||||
|
</Link>{" "}
|
||||||
|
one first.
|
||||||
|
</Alert>
|
||||||
|
);
|
||||||
|
}
|
||||||
return <Alert severity="error">Error fetching logs: {error.message}</Alert>;
|
return <Alert severity="error">Error fetching logs: {error.message}</Alert>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,8 +31,8 @@ const useStyles = makeStyles((theme: Theme) =>
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
logs: Array<any>;
|
logs: Array<any>;
|
||||||
selectedReqLogId?: string;
|
selectedReqLogId?: number;
|
||||||
onLogClick(requestId: string): void;
|
onLogClick(requestId: number): void;
|
||||||
theme: Theme;
|
theme: Theme;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,8 +63,8 @@ function RequestList({
|
|||||||
|
|
||||||
interface RequestListTableProps {
|
interface RequestListTableProps {
|
||||||
logs?: any;
|
logs?: any;
|
||||||
selectedReqLogId?: string;
|
selectedReqLogId?: number;
|
||||||
onLogClick(requestId: string): void;
|
onLogClick(requestId: number): void;
|
||||||
theme: Theme;
|
theme: Theme;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -128,7 +128,9 @@ function RequestListTable({
|
|||||||
{response && (
|
{response && (
|
||||||
<div>
|
<div>
|
||||||
<HttpStatusIcon status={response.statusCode} />{" "}
|
<HttpStatusIcon status={response.statusCode} />{" "}
|
||||||
<code>{response.status}</code>
|
<code>
|
||||||
|
{response.statusCode} {response.statusReason}
|
||||||
|
</code>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
@ -8,7 +8,7 @@ interface Props {
|
|||||||
response: {
|
response: {
|
||||||
proto: string;
|
proto: string;
|
||||||
statusCode: number;
|
statusCode: number;
|
||||||
status: string;
|
statusReason: string;
|
||||||
headers: Array<{ key: string; value: string }>;
|
headers: Array<{ key: string; value: string }>;
|
||||||
body?: string;
|
body?: string;
|
||||||
};
|
};
|
||||||
@ -42,7 +42,7 @@ function ResponseDetail({ response }: Props): JSX.Element {
|
|||||||
{response.proto}
|
{response.proto}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Typography>{" "}
|
</Typography>{" "}
|
||||||
{response.status}
|
{response.statusCode} {response.statusReason}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
253
admin/src/components/reqlog/Search.tsx
Normal file
@ -0,0 +1,253 @@
|
|||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Checkbox,
|
||||||
|
CircularProgress,
|
||||||
|
ClickAwayListener,
|
||||||
|
createStyles,
|
||||||
|
FormControlLabel,
|
||||||
|
InputBase,
|
||||||
|
makeStyles,
|
||||||
|
Paper,
|
||||||
|
Popper,
|
||||||
|
Theme,
|
||||||
|
Tooltip,
|
||||||
|
useTheme,
|
||||||
|
} from "@material-ui/core";
|
||||||
|
import IconButton from "@material-ui/core/IconButton";
|
||||||
|
import SearchIcon from "@material-ui/icons/Search";
|
||||||
|
import FilterListIcon from "@material-ui/icons/FilterList";
|
||||||
|
import DeleteIcon from "@material-ui/icons/Delete";
|
||||||
|
import React, { useRef, useState } from "react";
|
||||||
|
import { gql, useMutation, useQuery } from "@apollo/client";
|
||||||
|
import { withoutTypename } from "../../lib/omitTypename";
|
||||||
|
import { Alert } from "@material-ui/lab";
|
||||||
|
import { useClearHTTPRequestLog } from "./hooks/useClearHTTPRequestLog";
|
||||||
|
import {
|
||||||
|
ConfirmationDialog,
|
||||||
|
useConfirmationDialog,
|
||||||
|
} from "./ConfirmationDialog";
|
||||||
|
|
||||||
|
const FILTER = gql`
|
||||||
|
query HttpRequestLogFilter {
|
||||||
|
httpRequestLogFilter {
|
||||||
|
onlyInScope
|
||||||
|
searchExpression
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const SET_FILTER = gql`
|
||||||
|
mutation SetHttpRequestLogFilter($filter: HttpRequestLogFilterInput) {
|
||||||
|
setHttpRequestLogFilter(filter: $filter) {
|
||||||
|
onlyInScope
|
||||||
|
searchExpression
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme: Theme) =>
|
||||||
|
createStyles({
|
||||||
|
root: {
|
||||||
|
padding: "2px 4px",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
width: 400,
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
marginLeft: theme.spacing(1),
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
iconButton: {
|
||||||
|
padding: 10,
|
||||||
|
},
|
||||||
|
filterPopper: {
|
||||||
|
width: 400,
|
||||||
|
marginTop: 6,
|
||||||
|
zIndex: 99,
|
||||||
|
},
|
||||||
|
filterOptions: {
|
||||||
|
padding: theme.spacing(2),
|
||||||
|
},
|
||||||
|
filterLoading: {
|
||||||
|
marginRight: 1,
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
export interface SearchFilter {
|
||||||
|
onlyInScope: boolean;
|
||||||
|
searchExpression: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Search(): JSX.Element {
|
||||||
|
const classes = useStyles();
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
const [searchExpr, setSearchExpr] = useState("");
|
||||||
|
const { loading: filterLoading, error: filterErr, data: filter } = useQuery(
|
||||||
|
FILTER,
|
||||||
|
{
|
||||||
|
onCompleted: (data) => {
|
||||||
|
setSearchExpr(data.httpRequestLogFilter.searchExpression || "");
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const [
|
||||||
|
setFilterMutate,
|
||||||
|
{ error: setFilterErr, loading: setFilterLoading },
|
||||||
|
] = useMutation<{
|
||||||
|
setHttpRequestLogFilter: SearchFilter | null;
|
||||||
|
}>(SET_FILTER, {
|
||||||
|
update(cache, { data: { setHttpRequestLogFilter } }) {
|
||||||
|
cache.writeQuery({
|
||||||
|
query: FILTER,
|
||||||
|
data: {
|
||||||
|
httpRequestLogFilter: setHttpRequestLogFilter,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const [
|
||||||
|
clearHTTPRequestLog,
|
||||||
|
clearHTTPRequestLogResult,
|
||||||
|
] = useClearHTTPRequestLog();
|
||||||
|
const clearHTTPConfirmationDialog = useConfirmationDialog();
|
||||||
|
|
||||||
|
const filterRef = useRef<HTMLElement | null>();
|
||||||
|
const [filterOpen, setFilterOpen] = useState(false);
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.SyntheticEvent) => {
|
||||||
|
setFilterMutate({
|
||||||
|
variables: {
|
||||||
|
filter: {
|
||||||
|
...withoutTypename(filter?.httpRequestLogFilter),
|
||||||
|
searchExpression: searchExpr,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
setFilterOpen(false);
|
||||||
|
e.preventDefault();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClickAway = (event: React.MouseEvent<EventTarget>) => {
|
||||||
|
if (filterRef.current.contains(event.target as HTMLElement)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setFilterOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Error prefix="Error fetching filter" error={filterErr} />
|
||||||
|
<Error prefix="Error setting filter" error={setFilterErr} />
|
||||||
|
<Error
|
||||||
|
prefix="Error clearing all HTTP logs"
|
||||||
|
error={clearHTTPRequestLogResult.error}
|
||||||
|
/>
|
||||||
|
<Box style={{ display: "flex", flex: 1 }}>
|
||||||
|
<ClickAwayListener onClickAway={handleClickAway}>
|
||||||
|
<Paper
|
||||||
|
component="form"
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
ref={filterRef}
|
||||||
|
className={classes.root}
|
||||||
|
>
|
||||||
|
<Tooltip title="Toggle filter options">
|
||||||
|
<IconButton
|
||||||
|
className={classes.iconButton}
|
||||||
|
onClick={() => setFilterOpen(!filterOpen)}
|
||||||
|
style={{
|
||||||
|
color: filter?.httpRequestLogFilter?.onlyInScope
|
||||||
|
? theme.palette.secondary.main
|
||||||
|
: "inherit",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{filterLoading || setFilterLoading ? (
|
||||||
|
<CircularProgress
|
||||||
|
className={classes.filterLoading}
|
||||||
|
size={23}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<FilterListIcon />
|
||||||
|
)}
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
<InputBase
|
||||||
|
className={classes.input}
|
||||||
|
placeholder="Search proxy logs…"
|
||||||
|
value={searchExpr}
|
||||||
|
onChange={(e) => setSearchExpr(e.target.value)}
|
||||||
|
onFocus={() => setFilterOpen(true)}
|
||||||
|
/>
|
||||||
|
<Tooltip title="Search">
|
||||||
|
<IconButton type="submit" className={classes.iconButton}>
|
||||||
|
<SearchIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
<Popper
|
||||||
|
className={classes.filterPopper}
|
||||||
|
open={filterOpen}
|
||||||
|
anchorEl={filterRef.current}
|
||||||
|
placement="bottom-start"
|
||||||
|
>
|
||||||
|
<Paper className={classes.filterOptions}>
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={
|
||||||
|
filter?.httpRequestLogFilter?.onlyInScope ? true : false
|
||||||
|
}
|
||||||
|
disabled={filterLoading || setFilterLoading}
|
||||||
|
onChange={(e) =>
|
||||||
|
setFilterMutate({
|
||||||
|
variables: {
|
||||||
|
filter: {
|
||||||
|
...withoutTypename(filter?.httpRequestLogFilter),
|
||||||
|
onlyInScope: e.target.checked,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="Only show in-scope requests"
|
||||||
|
/>
|
||||||
|
</Paper>
|
||||||
|
</Popper>
|
||||||
|
</Paper>
|
||||||
|
</ClickAwayListener>
|
||||||
|
<Box style={{ marginLeft: "auto" }}>
|
||||||
|
<Tooltip title="Clear all">
|
||||||
|
<IconButton onClick={clearHTTPConfirmationDialog.open}>
|
||||||
|
<DeleteIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<ConfirmationDialog
|
||||||
|
isOpen={clearHTTPConfirmationDialog.isOpen}
|
||||||
|
onClose={clearHTTPConfirmationDialog.close}
|
||||||
|
onConfirm={clearHTTPRequestLog}
|
||||||
|
>
|
||||||
|
All proxy logs are going to be removed. This action cannot be undone.
|
||||||
|
</ConfirmationDialog>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Error(props: { prefix: string; error?: Error }) {
|
||||||
|
if (!props.error) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box mb={4}>
|
||||||
|
<Alert severity="error">
|
||||||
|
{props.prefix}: {props.error.message}
|
||||||
|
</Alert>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Search;
|
16
admin/src/components/reqlog/hooks/useClearHTTPRequestLog.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { gql, useMutation } from "@apollo/client";
|
||||||
|
import { HTTP_REQUEST_LOGS } from "./useHttpRequestLogs";
|
||||||
|
|
||||||
|
const CLEAR_HTTP_REQUEST_LOG = gql`
|
||||||
|
mutation ClearHTTPRequestLog {
|
||||||
|
clearHTTPRequestLog {
|
||||||
|
success
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export function useClearHTTPRequestLog() {
|
||||||
|
return useMutation(CLEAR_HTTP_REQUEST_LOG, {
|
||||||
|
refetchQueries: [{ query: HTTP_REQUEST_LOGS }],
|
||||||
|
});
|
||||||
|
}
|
22
admin/src/components/reqlog/hooks/useHttpRequestLogs.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { gql, useQuery } from "@apollo/client";
|
||||||
|
|
||||||
|
export const HTTP_REQUEST_LOGS = gql`
|
||||||
|
query HttpRequestLogs {
|
||||||
|
httpRequestLogs {
|
||||||
|
id
|
||||||
|
method
|
||||||
|
url
|
||||||
|
timestamp
|
||||||
|
response {
|
||||||
|
statusCode
|
||||||
|
statusReason
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export function useHttpRequestLogs() {
|
||||||
|
return useQuery(HTTP_REQUEST_LOGS, {
|
||||||
|
pollInterval: 1000,
|
||||||
|
});
|
||||||
|
}
|
140
admin/src/components/scope/AddRule.tsx
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
import { gql, useApolloClient, useMutation } from "@apollo/client";
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
CircularProgress,
|
||||||
|
createStyles,
|
||||||
|
FormControl,
|
||||||
|
FormControlLabel,
|
||||||
|
FormLabel,
|
||||||
|
makeStyles,
|
||||||
|
Radio,
|
||||||
|
RadioGroup,
|
||||||
|
TextField,
|
||||||
|
Theme,
|
||||||
|
} from "@material-ui/core";
|
||||||
|
import AddIcon from "@material-ui/icons/Add";
|
||||||
|
import { Alert } from "@material-ui/lab";
|
||||||
|
import React from "react";
|
||||||
|
import { SCOPE } from "./Rules";
|
||||||
|
|
||||||
|
const SET_SCOPE = gql`
|
||||||
|
mutation SetScope($scope: [ScopeRuleInput!]!) {
|
||||||
|
setScope(scope: $scope) {
|
||||||
|
url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme: Theme) =>
|
||||||
|
createStyles({
|
||||||
|
ruleExpression: {
|
||||||
|
fontFamily: "'JetBrains Mono', monospace",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
function AddRule(): JSX.Element {
|
||||||
|
const classes = useStyles();
|
||||||
|
|
||||||
|
const [ruleType, setRuleType] = React.useState("url");
|
||||||
|
const [expression, setExpression] = React.useState(null);
|
||||||
|
|
||||||
|
const client = useApolloClient();
|
||||||
|
const [setScope, { error, loading }] = useMutation(SET_SCOPE, {
|
||||||
|
onError() {},
|
||||||
|
onCompleted() {
|
||||||
|
expression.value = "";
|
||||||
|
},
|
||||||
|
update(_, { data: { setScope } }) {
|
||||||
|
client.writeQuery({
|
||||||
|
query: SCOPE,
|
||||||
|
data: { scope: setScope },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleTypeChange = (e: React.ChangeEvent, value: string) => {
|
||||||
|
setRuleType(value);
|
||||||
|
};
|
||||||
|
const handleSubmit = (e: React.SyntheticEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
let scope = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = client.readQuery({
|
||||||
|
query: SCOPE,
|
||||||
|
});
|
||||||
|
scope = data.scope;
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
setScope({
|
||||||
|
variables: {
|
||||||
|
scope: [
|
||||||
|
...scope.map(({ url }) => ({ url })),
|
||||||
|
{ url: expression.value },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{error && (
|
||||||
|
<Box mb={4}>
|
||||||
|
<Alert severity="error">Error adding rule: {error.message}</Alert>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
<form onSubmit={handleSubmit} autoComplete="off">
|
||||||
|
<FormControl fullWidth>
|
||||||
|
<FormLabel color="secondary" component="legend">
|
||||||
|
Rule Type
|
||||||
|
</FormLabel>
|
||||||
|
<RadioGroup
|
||||||
|
row
|
||||||
|
name="ruleType"
|
||||||
|
value={ruleType}
|
||||||
|
onChange={handleTypeChange}
|
||||||
|
>
|
||||||
|
<FormControlLabel value="url" control={<Radio />} label="URL" />
|
||||||
|
</RadioGroup>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl fullWidth>
|
||||||
|
<TextField
|
||||||
|
label="Expression"
|
||||||
|
placeholder="^https:\/\/(.*)example.com(.*)"
|
||||||
|
helperText="Regular expression to match on."
|
||||||
|
color="secondary"
|
||||||
|
variant="outlined"
|
||||||
|
required
|
||||||
|
InputProps={{
|
||||||
|
className: classes.ruleExpression,
|
||||||
|
}}
|
||||||
|
InputLabelProps={{
|
||||||
|
shrink: true,
|
||||||
|
}}
|
||||||
|
inputProps={{
|
||||||
|
ref: (node) => {
|
||||||
|
setExpression(node);
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<Box my={2}>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
variant="contained"
|
||||||
|
color="secondary"
|
||||||
|
disabled={loading}
|
||||||
|
startIcon={loading ? <CircularProgress size={22} /> : <AddIcon />}
|
||||||
|
>
|
||||||
|
Add rule
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AddRule;
|
86
admin/src/components/scope/RuleListItem.tsx
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import { gql, useApolloClient, useMutation, useQuery } from "@apollo/client";
|
||||||
|
import {
|
||||||
|
Avatar,
|
||||||
|
Chip,
|
||||||
|
IconButton,
|
||||||
|
ListItem,
|
||||||
|
ListItemAvatar,
|
||||||
|
ListItemSecondaryAction,
|
||||||
|
ListItemText,
|
||||||
|
Tooltip,
|
||||||
|
} from "@material-ui/core";
|
||||||
|
import CodeIcon from "@material-ui/icons/Code";
|
||||||
|
import DeleteIcon from "@material-ui/icons/Delete";
|
||||||
|
import React from "react";
|
||||||
|
import { SCOPE } from "./Rules";
|
||||||
|
|
||||||
|
const SET_SCOPE = gql`
|
||||||
|
mutation SetScope($scope: [ScopeRuleInput!]!) {
|
||||||
|
setScope(scope: $scope) {
|
||||||
|
url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
function RuleListItem({ scope, rule, index }): JSX.Element {
|
||||||
|
const client = useApolloClient();
|
||||||
|
const [setScope, { loading }] = useMutation(SET_SCOPE, {
|
||||||
|
update(_, { data: { setScope } }) {
|
||||||
|
client.writeQuery({
|
||||||
|
query: SCOPE,
|
||||||
|
data: { scope: setScope },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleDelete = (index: number) => {
|
||||||
|
const clone = [...scope];
|
||||||
|
clone.splice(index, 1);
|
||||||
|
setScope({
|
||||||
|
variables: {
|
||||||
|
scope: clone.map(({ url }) => ({ url })),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ListItem>
|
||||||
|
<ListItemAvatar>
|
||||||
|
<Avatar>
|
||||||
|
<CodeIcon />
|
||||||
|
</Avatar>
|
||||||
|
</ListItemAvatar>
|
||||||
|
<RuleListItemText rule={rule} />
|
||||||
|
<ListItemSecondaryAction>
|
||||||
|
<RuleTypeChip rule={rule} />
|
||||||
|
<Tooltip title="Delete rule">
|
||||||
|
<span style={{ marginLeft: 8 }}>
|
||||||
|
<IconButton onClick={() => handleDelete(index)} disabled={loading}>
|
||||||
|
<DeleteIcon />
|
||||||
|
</IconButton>
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
</ListItemSecondaryAction>
|
||||||
|
</ListItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function RuleListItemText({ rule }): JSX.Element {
|
||||||
|
let text: JSX.Element;
|
||||||
|
|
||||||
|
if (rule.url) {
|
||||||
|
text = <code>{rule.url}</code>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Parse and handle rule.header and rule.body.
|
||||||
|
|
||||||
|
return <ListItemText>{text}</ListItemText>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function RuleTypeChip({ rule }): JSX.Element {
|
||||||
|
if (rule.url) {
|
||||||
|
return <Chip label="URL" variant="outlined" />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RuleListItem;
|
55
admin/src/components/scope/Rules.tsx
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import { gql, useQuery } from "@apollo/client";
|
||||||
|
import {
|
||||||
|
CircularProgress,
|
||||||
|
createStyles,
|
||||||
|
List,
|
||||||
|
makeStyles,
|
||||||
|
Theme,
|
||||||
|
} from "@material-ui/core";
|
||||||
|
import { Alert } from "@material-ui/lab";
|
||||||
|
import React from "react";
|
||||||
|
import RuleListItem from "./RuleListItem";
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme: Theme) =>
|
||||||
|
createStyles({
|
||||||
|
rulesList: {
|
||||||
|
backgroundColor: theme.palette.background.paper,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
export const SCOPE = gql`
|
||||||
|
query Scope {
|
||||||
|
scope {
|
||||||
|
url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
function Rules(): JSX.Element {
|
||||||
|
const classes = useStyles();
|
||||||
|
const { loading, error, data } = useQuery(SCOPE);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{loading && <CircularProgress />}
|
||||||
|
{error && (
|
||||||
|
<Alert severity="error">Error fetching scope: {error.message}</Alert>
|
||||||
|
)}
|
||||||
|
{data?.scope.length > 0 && (
|
||||||
|
<List className={classes.rulesList}>
|
||||||
|
{data.scope.map((rule, index) => (
|
||||||
|
<RuleListItem
|
||||||
|
key={index}
|
||||||
|
rule={rule}
|
||||||
|
scope={data.scope}
|
||||||
|
index={index}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Rules;
|
@ -1,6 +1,5 @@
|
|||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { ApolloClient, HttpLink, InMemoryCache } from "@apollo/client";
|
import { ApolloClient, HttpLink, InMemoryCache } from "@apollo/client";
|
||||||
import { concatPagination } from "@apollo/client/utilities";
|
|
||||||
|
|
||||||
let apolloClient;
|
let apolloClient;
|
||||||
|
|
||||||
@ -12,10 +11,8 @@ function createApolloClient() {
|
|||||||
}),
|
}),
|
||||||
cache: new InMemoryCache({
|
cache: new InMemoryCache({
|
||||||
typePolicies: {
|
typePolicies: {
|
||||||
Query: {
|
Project: {
|
||||||
fields: {
|
keyFields: ["name"],
|
||||||
allPosts: concatPagination(),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
5
admin/src/lib/omitTypename.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
const omitTypename = (key, value) => (key === "__typename" ? undefined : value);
|
||||||
|
|
||||||
|
export function withoutTypename(input: any): any {
|
||||||
|
return JSON.parse(JSON.stringify(input), omitTypename);
|
||||||
|
}
|
@ -11,6 +11,12 @@ const theme = createMuiTheme({
|
|||||||
secondary: {
|
secondary: {
|
||||||
main: teal["A400"],
|
main: teal["A400"],
|
||||||
},
|
},
|
||||||
|
info: {
|
||||||
|
main: teal["A400"],
|
||||||
|
},
|
||||||
|
success: {
|
||||||
|
main: teal["A400"],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
typography: {
|
typography: {
|
||||||
h2: {
|
h2: {
|
||||||
|
36
admin/src/pages/get-started/index.tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { Box, Link as MaterialLink, Typography } from "@material-ui/core";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import Layout, { Page } from "../../components/Layout";
|
||||||
|
|
||||||
|
function Index(): JSX.Element {
|
||||||
|
return (
|
||||||
|
<Layout page={Page.GetStarted} title="Get started">
|
||||||
|
<Box p={4}>
|
||||||
|
<Box mb={3}>
|
||||||
|
<Typography variant="h4">Get started</Typography>
|
||||||
|
</Box>
|
||||||
|
<Typography paragraph>
|
||||||
|
You’ve loaded a (new) project. What’s next? You can now use the MITM
|
||||||
|
proxy and review HTTP requests and responses via the{" "}
|
||||||
|
<Link href="/proxy/logs" passHref>
|
||||||
|
<MaterialLink color="secondary">Proxy logs</MaterialLink>
|
||||||
|
</Link>
|
||||||
|
. Stuck? Ask for help on the{" "}
|
||||||
|
<MaterialLink
|
||||||
|
href="https://github.com/dstotijn/hetty/discussions"
|
||||||
|
color="secondary"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
Discussions forum
|
||||||
|
</MaterialLink>
|
||||||
|
.
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Index;
|
@ -1,17 +1,30 @@
|
|||||||
import {
|
import {
|
||||||
|
Avatar,
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
|
CircularProgress,
|
||||||
createStyles,
|
createStyles,
|
||||||
IconButton,
|
List,
|
||||||
|
ListItem,
|
||||||
|
ListItemAvatar,
|
||||||
|
ListItemText,
|
||||||
makeStyles,
|
makeStyles,
|
||||||
|
TextField,
|
||||||
Theme,
|
Theme,
|
||||||
Typography,
|
Typography,
|
||||||
} from "@material-ui/core";
|
} from "@material-ui/core";
|
||||||
import SettingsEthernetIcon from "@material-ui/icons/SettingsEthernet";
|
import AddIcon from "@material-ui/icons/Add";
|
||||||
import SendIcon from "@material-ui/icons/Send";
|
import FolderIcon from "@material-ui/icons/Folder";
|
||||||
|
import DescriptionIcon from "@material-ui/icons/Description";
|
||||||
|
import PlayArrowIcon from "@material-ui/icons/PlayArrow";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { gql, useMutation, useQuery } from "@apollo/client";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
import Layout, { Page } from "../components/Layout";
|
import Layout, { Page } from "../components/Layout";
|
||||||
|
import { Alert } from "@material-ui/lab";
|
||||||
|
|
||||||
const useStyles = makeStyles((theme: Theme) =>
|
const useStyles = makeStyles((theme: Theme) =>
|
||||||
createStyles({
|
createStyles({
|
||||||
@ -24,14 +37,108 @@ const useStyles = makeStyles((theme: Theme) =>
|
|||||||
lineHeight: 2,
|
lineHeight: 2,
|
||||||
marginBottom: theme.spacing(5),
|
marginBottom: theme.spacing(5),
|
||||||
},
|
},
|
||||||
|
projectName: {
|
||||||
|
marginTop: -6,
|
||||||
|
marginRight: theme.spacing(2),
|
||||||
|
},
|
||||||
button: {
|
button: {
|
||||||
marginRight: theme.spacing(2),
|
marginRight: theme.spacing(2),
|
||||||
},
|
},
|
||||||
|
activeProject: {
|
||||||
|
color: theme.palette.getContrastText(theme.palette.secondary.main),
|
||||||
|
backgroundColor: theme.palette.secondary.main,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const ACTIVE_PROJECT = gql`
|
||||||
|
query ActiveProject {
|
||||||
|
activeProject {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const OPEN_PROJECT = gql`
|
||||||
|
mutation OpenProject($name: String!) {
|
||||||
|
openProject(name: $name) {
|
||||||
|
name
|
||||||
|
isActive
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
function Index(): JSX.Element {
|
function Index(): JSX.Element {
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
|
const router = useRouter();
|
||||||
|
const [input, setInput] = useState(null);
|
||||||
|
const { error: activeProjErr, data: activeProjData } = useQuery(
|
||||||
|
ACTIVE_PROJECT,
|
||||||
|
{
|
||||||
|
pollInterval: 1000,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const [
|
||||||
|
openProject,
|
||||||
|
{ error: openProjErr, data: openProjData, loading: openProjLoading },
|
||||||
|
] = useMutation(OPEN_PROJECT, {
|
||||||
|
onError: () => {},
|
||||||
|
onCompleted({ openProject }) {
|
||||||
|
if (openProject) {
|
||||||
|
router.push("/get-started");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
update(cache, { data: { openProject } }) {
|
||||||
|
cache.modify({
|
||||||
|
fields: {
|
||||||
|
activeProject() {
|
||||||
|
const activeProjRef = cache.writeFragment({
|
||||||
|
id: openProject.name,
|
||||||
|
data: openProject,
|
||||||
|
fragment: gql`
|
||||||
|
fragment ActiveProject on Project {
|
||||||
|
name
|
||||||
|
isActive
|
||||||
|
type
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
return activeProjRef;
|
||||||
|
},
|
||||||
|
projects(_, { DELETE }) {
|
||||||
|
cache.writeFragment({
|
||||||
|
id: openProject.name,
|
||||||
|
data: openProject,
|
||||||
|
fragment: gql`
|
||||||
|
fragment OpenProject on Project {
|
||||||
|
name
|
||||||
|
isActive
|
||||||
|
type
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
return DELETE;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleForm = (e: React.SyntheticEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
openProject({ variables: { name: input.value } });
|
||||||
|
};
|
||||||
|
|
||||||
|
if (activeProjErr) {
|
||||||
|
return (
|
||||||
|
<Layout page={Page.Home} title="">
|
||||||
|
<Alert severity="error">
|
||||||
|
Error fetching active project: {activeProjErr.message}
|
||||||
|
</Alert>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout page={Page.Home} title="">
|
<Layout page={Page.Home} title="">
|
||||||
<Box p={4}>
|
<Box p={4}>
|
||||||
@ -42,38 +149,105 @@ function Index(): JSX.Element {
|
|||||||
The simple HTTP toolkit for security research.
|
The simple HTTP toolkit for security research.
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Typography className={classes.subtitle} paragraph>
|
<Typography className={classes.subtitle} paragraph>
|
||||||
What if security testing was intuitive, powerful, and good looking?
|
What if security testing was intuitive, powerful, and good looking?
|
||||||
What if it was <strong>free</strong>, instead of $400 per year?{" "}
|
What if it was <strong>free</strong>, instead of $400 per year?{" "}
|
||||||
<span className={classes.titleHighlight}>Hetty</span> is listening on{" "}
|
<span className={classes.titleHighlight}>Hetty</span> is listening on{" "}
|
||||||
<code>:8080</code>…
|
<code>:8080</code>…
|
||||||
</Typography>
|
</Typography>
|
||||||
<Box>
|
|
||||||
<Link href="/proxy" passHref>
|
{activeProjData?.activeProject?.name ? (
|
||||||
|
<div>
|
||||||
|
<Box mb={1}>
|
||||||
|
<Typography variant="h6">Active project:</Typography>
|
||||||
|
</Box>
|
||||||
|
<Box ml={-2} mb={2}>
|
||||||
|
<List>
|
||||||
|
<ListItem>
|
||||||
|
<ListItemAvatar>
|
||||||
|
<Avatar className={classes.activeProject}>
|
||||||
|
<DescriptionIcon />
|
||||||
|
</Avatar>
|
||||||
|
</ListItemAvatar>
|
||||||
|
<ListItemText primary={activeProjData.activeProject.name} />
|
||||||
|
</ListItem>
|
||||||
|
</List>
|
||||||
|
</Box>
|
||||||
|
<div>
|
||||||
|
<Link href="/get-started" passHref>
|
||||||
|
<Button
|
||||||
|
className={classes.button}
|
||||||
|
variant="outlined"
|
||||||
|
component="a"
|
||||||
|
color="secondary"
|
||||||
|
size="large"
|
||||||
|
startIcon={<PlayArrowIcon />}
|
||||||
|
>
|
||||||
|
Get started
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
<Link href="/projects" passHref>
|
||||||
|
<Button
|
||||||
|
className={classes.button}
|
||||||
|
variant="outlined"
|
||||||
|
component="a"
|
||||||
|
size="large"
|
||||||
|
startIcon={<FolderIcon />}
|
||||||
|
>
|
||||||
|
Manage projects
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<form onSubmit={handleForm} autoComplete="off">
|
||||||
|
<TextField
|
||||||
|
className={classes.projectName}
|
||||||
|
color="secondary"
|
||||||
|
inputProps={{
|
||||||
|
id: "projectName",
|
||||||
|
ref: (node) => {
|
||||||
|
setInput(node);
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
label="Project name"
|
||||||
|
placeholder="Project name…"
|
||||||
|
error={Boolean(openProjErr)}
|
||||||
|
helperText={openProjErr && openProjErr.message}
|
||||||
|
/>
|
||||||
<Button
|
<Button
|
||||||
className={classes.button}
|
className={classes.button}
|
||||||
|
type="submit"
|
||||||
variant="contained"
|
variant="contained"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
component="a"
|
|
||||||
size="large"
|
size="large"
|
||||||
startIcon={<SettingsEthernetIcon />}
|
disabled={
|
||||||
|
openProjLoading || Boolean(openProjData?.openProject?.name)
|
||||||
|
}
|
||||||
|
startIcon={
|
||||||
|
openProjLoading || openProjData?.openProject ? (
|
||||||
|
<CircularProgress size={22} />
|
||||||
|
) : (
|
||||||
|
<AddIcon />
|
||||||
|
)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
Setup proxy
|
Create project
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
<Link href="/projects" passHref>
|
||||||
<Link href="/proxy" passHref>
|
<Button
|
||||||
<Button
|
className={classes.button}
|
||||||
className={classes.button}
|
variant="outlined"
|
||||||
variant="contained"
|
component="a"
|
||||||
color="primary"
|
size="large"
|
||||||
component="a"
|
startIcon={<FolderIcon />}
|
||||||
size="large"
|
>
|
||||||
startIcon={<SendIcon />}
|
Open project…
|
||||||
>
|
</Button>
|
||||||
Send HTTP requests
|
</Link>
|
||||||
</Button>
|
</form>
|
||||||
</Link>
|
)}
|
||||||
</Box>
|
|
||||||
</Box>
|
</Box>
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
|
33
admin/src/pages/projects/index.tsx
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { Box, Divider, Grid, Typography } from "@material-ui/core";
|
||||||
|
import Layout, { Page } from "../../components/Layout";
|
||||||
|
import NewProject from "../../components/projects/NewProject";
|
||||||
|
import ProjectList from "../../components/projects/ProjectList";
|
||||||
|
|
||||||
|
function Index(): JSX.Element {
|
||||||
|
return (
|
||||||
|
<Layout page={Page.Projects} title="Projects">
|
||||||
|
<Box p={4}>
|
||||||
|
<Box mb={3}>
|
||||||
|
<Typography variant="h4">Projects</Typography>
|
||||||
|
</Box>
|
||||||
|
<Typography paragraph>
|
||||||
|
Projects contain settings and data generated/processed by Hetty. They
|
||||||
|
are stored as SQLite database files on disk.
|
||||||
|
</Typography>
|
||||||
|
<Box my={4}>
|
||||||
|
<Divider />
|
||||||
|
</Box>
|
||||||
|
<Box mb={8}>
|
||||||
|
<NewProject />
|
||||||
|
</Box>
|
||||||
|
<Grid container>
|
||||||
|
<Grid item xs={12} sm={8} md={6} lg={6}>
|
||||||
|
<ProjectList />
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Box>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Index;
|
@ -1,9 +1,15 @@
|
|||||||
|
import { Box } from "@material-ui/core";
|
||||||
|
|
||||||
import LogsOverview from "../../../components/reqlog/LogsOverview";
|
import LogsOverview from "../../../components/reqlog/LogsOverview";
|
||||||
import Layout, { Page } from "../../../components/Layout";
|
import Layout, { Page } from "../../../components/Layout";
|
||||||
|
import Search from "../../../components/reqlog/Search";
|
||||||
|
|
||||||
function ProxyLogs(): JSX.Element {
|
function ProxyLogs(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<Layout page={Page.ProxyLogs} title="Proxy logs">
|
<Layout page={Page.ProxyLogs} title="Proxy logs">
|
||||||
|
<Box mb={2}>
|
||||||
|
<Search />
|
||||||
|
</Box>
|
||||||
<LogsOverview />
|
<LogsOverview />
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
|
39
admin/src/pages/scope/index.tsx
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { Box, Divider, Grid, Typography } from "@material-ui/core";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import Layout, { Page } from "../../components/Layout";
|
||||||
|
import AddRule from "../../components/scope/AddRule";
|
||||||
|
import Rules from "../../components/scope/Rules";
|
||||||
|
|
||||||
|
function Index(): JSX.Element {
|
||||||
|
return (
|
||||||
|
<Layout page={Page.Scope} title="Scope">
|
||||||
|
<Box p={4}>
|
||||||
|
<Box mb={3}>
|
||||||
|
<Typography variant="h4">Scope</Typography>
|
||||||
|
</Box>
|
||||||
|
<Typography paragraph>
|
||||||
|
Scope rules are used by various modules in Hetty and can influence
|
||||||
|
their behavior. For example: the Proxy logs module can match incoming
|
||||||
|
requests against scope rules and decide its behavior (e.g. log or
|
||||||
|
bypass) based on the outcome of the match. All scope configuration is
|
||||||
|
stored per project.
|
||||||
|
</Typography>
|
||||||
|
<Box my={4}>
|
||||||
|
<Divider />
|
||||||
|
</Box>
|
||||||
|
<Grid container>
|
||||||
|
<Grid item xs={12} sm={12} md={8} lg={6}>
|
||||||
|
<AddRule />
|
||||||
|
<Box my={4}>
|
||||||
|
<Divider />
|
||||||
|
</Box>
|
||||||
|
<Rules />
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Box>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Index;
|
274
admin/yarn.lock
@ -396,7 +396,7 @@
|
|||||||
"@babel/helper-plugin-utils" "^7.10.4"
|
"@babel/helper-plugin-utils" "^7.10.4"
|
||||||
"@babel/plugin-syntax-logical-assignment-operators" "^7.10.4"
|
"@babel/plugin-syntax-logical-assignment-operators" "^7.10.4"
|
||||||
|
|
||||||
"@babel/plugin-proposal-nullish-coalescing-operator@7.10.4", "@babel/plugin-proposal-nullish-coalescing-operator@^7.10.4":
|
"@babel/plugin-proposal-nullish-coalescing-operator@^7.10.4":
|
||||||
version "7.10.4"
|
version "7.10.4"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.10.4.tgz#02a7e961fc32e6d5b2db0649e01bf80ddee7e04a"
|
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.10.4.tgz#02a7e961fc32e6d5b2db0649e01bf80ddee7e04a"
|
||||||
integrity sha512-wq5n1M3ZUlHl9sqT2ok1T2/MTt6AXE0e1Lz4WzWBr95LsAZ5qDXe4KnFuauYyEyLiohvXFMdbsOTMyLZs91Zlw==
|
integrity sha512-wq5n1M3ZUlHl9sqT2ok1T2/MTt6AXE0e1Lz4WzWBr95LsAZ5qDXe4KnFuauYyEyLiohvXFMdbsOTMyLZs91Zlw==
|
||||||
@ -429,7 +429,7 @@
|
|||||||
"@babel/helper-plugin-utils" "^7.10.4"
|
"@babel/helper-plugin-utils" "^7.10.4"
|
||||||
"@babel/plugin-syntax-optional-catch-binding" "^7.8.0"
|
"@babel/plugin-syntax-optional-catch-binding" "^7.8.0"
|
||||||
|
|
||||||
"@babel/plugin-proposal-optional-chaining@7.11.0", "@babel/plugin-proposal-optional-chaining@^7.11.0":
|
"@babel/plugin-proposal-optional-chaining@^7.11.0":
|
||||||
version "7.11.0"
|
version "7.11.0"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.11.0.tgz#de5866d0646f6afdaab8a566382fe3a221755076"
|
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.11.0.tgz#de5866d0646f6afdaab8a566382fe3a221755076"
|
||||||
integrity sha512-v9fZIu3Y8562RRwhm1BbMRxtqZNFmFA2EG+pT2diuU8PT3H6T/KXoZ54KgYisfOFZHV6PfvAiBIZ9Rcz+/JCxA==
|
integrity sha512-v9fZIu3Y8562RRwhm1BbMRxtqZNFmFA2EG+pT2diuU8PT3H6T/KXoZ54KgYisfOFZHV6PfvAiBIZ9Rcz+/JCxA==
|
||||||
@ -496,7 +496,7 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@babel/helper-plugin-utils" "^7.8.0"
|
"@babel/helper-plugin-utils" "^7.8.0"
|
||||||
|
|
||||||
"@babel/plugin-syntax-jsx@^7.10.4":
|
"@babel/plugin-syntax-jsx@7.10.4", "@babel/plugin-syntax-jsx@^7.10.4":
|
||||||
version "7.10.4"
|
version "7.10.4"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.10.4.tgz#39abaae3cbf710c4373d8429484e6ba21340166c"
|
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.10.4.tgz#39abaae3cbf710c4373d8429484e6ba21340166c"
|
||||||
integrity sha512-KCg9mio9jwiARCB7WAcQ7Y1q+qicILjoK8LP/VkPkEKaf5dkaZZK1EcTe91a3JJlZ3qy6L5s9X52boEYi8DM9g==
|
integrity sha512-KCg9mio9jwiARCB7WAcQ7Y1q+qicILjoK8LP/VkPkEKaf5dkaZZK1EcTe91a3JJlZ3qy6L5s9X52boEYi8DM9g==
|
||||||
@ -1142,10 +1142,20 @@
|
|||||||
prop-types "^15.7.2"
|
prop-types "^15.7.2"
|
||||||
react-is "^16.8.0"
|
react-is "^16.8.0"
|
||||||
|
|
||||||
"@next/react-dev-overlay@9.5.3":
|
"@next/env@9.5.4":
|
||||||
version "9.5.3"
|
version "9.5.4"
|
||||||
resolved "https://registry.yarnpkg.com/@next/react-dev-overlay/-/react-dev-overlay-9.5.3.tgz#3275301f08045ecc709e3273031973a1f5e81427"
|
resolved "https://registry.yarnpkg.com/@next/env/-/env-9.5.4.tgz#950f3370151a940ecac6e7e19cf125e6113e101e"
|
||||||
integrity sha512-R2ZAyFjHHaMTBVi19ZZNRJNXiwn46paRi7EZvKNvMxbrzBcUYtSFj/edU3jQoF1UOcC6vGeMhtPqH55ONrIjCQ==
|
integrity sha512-uGnUO68/u9C8bqHj5obIvyGRDqe/jh1dFSLx03mJmlESjcCmV4umXYJOnt3XzU1VhVntSE+jUZtnS5bjYmmLfQ==
|
||||||
|
|
||||||
|
"@next/polyfill-module@9.5.4":
|
||||||
|
version "9.5.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@next/polyfill-module/-/polyfill-module-9.5.4.tgz#35ea31ce5f6bbf0ac31aac483b60d4ba17a79861"
|
||||||
|
integrity sha512-GA2sW7gs33s7RGPFqkMiT9asYpaV/Hhw9+XM9/UlPrkNdTaxZWaPa2iHgmqJ7k6OHiOmy+CBLFrUBgzqKNhs3Q==
|
||||||
|
|
||||||
|
"@next/react-dev-overlay@9.5.4":
|
||||||
|
version "9.5.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@next/react-dev-overlay/-/react-dev-overlay-9.5.4.tgz#7d88a710d23021020cca213bc77106df18950b2b"
|
||||||
|
integrity sha512-tYvNmOQ0inykSvcimkTiONMv4ZyFB2G2clsy9FKLLRZ2OA+Jiov6T7Pq6YpKbBwTLu/BQGVc7Qn4BZ5CDHR8ig==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/code-frame" "7.10.4"
|
"@babel/code-frame" "7.10.4"
|
||||||
ally.js "1.4.1"
|
ally.js "1.4.1"
|
||||||
@ -1158,10 +1168,17 @@
|
|||||||
stacktrace-parser "0.1.10"
|
stacktrace-parser "0.1.10"
|
||||||
strip-ansi "6.0.0"
|
strip-ansi "6.0.0"
|
||||||
|
|
||||||
"@next/react-refresh-utils@9.5.3":
|
"@next/react-refresh-utils@9.5.4":
|
||||||
version "9.5.3"
|
version "9.5.4"
|
||||||
resolved "https://registry.yarnpkg.com/@next/react-refresh-utils/-/react-refresh-utils-9.5.3.tgz#a14fb6489d412b201b98aa44716fb8727ca4c6ae"
|
resolved "https://registry.yarnpkg.com/@next/react-refresh-utils/-/react-refresh-utils-9.5.4.tgz#3bfe067f0cfc717f079482d956211708c9e81126"
|
||||||
integrity sha512-W3VKOqbg+4Kw+k6M/SODf+WIzwcx60nAemGV1nNPa/yrDtAS2YcJfqiswrJ3+2nJHzqefAFWn4XOfM0fy8ww2Q==
|
integrity sha512-TPhEiYxK5YlEuzVuTzgZiDN7SDh4drvUAqsO9Yccd8WLcfYqOLRN2fCALremW5mNLAZQZW3iFgW8PW8Gckq4EQ==
|
||||||
|
|
||||||
|
"@npmcli/move-file@^1.0.1":
|
||||||
|
version "1.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@npmcli/move-file/-/move-file-1.0.1.tgz#de103070dac0f48ce49cf6693c23af59c0f70464"
|
||||||
|
integrity sha512-Uv6h1sT+0DrblvIrolFtbvM1FgWm+/sy4B3pvLp67Zys+thcukzS5ekn7HsZFGpWP4Q3fYJCljbWQE/XivMRLw==
|
||||||
|
dependencies:
|
||||||
|
mkdirp "^1.0.4"
|
||||||
|
|
||||||
"@types/color-name@^1.1.1":
|
"@types/color-name@^1.1.1":
|
||||||
version "1.1.1"
|
version "1.1.1"
|
||||||
@ -1442,7 +1459,7 @@ ajv-keywords@^3.1.0, ajv-keywords@^3.4.1, ajv-keywords@^3.5.2:
|
|||||||
resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d"
|
resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d"
|
||||||
integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==
|
integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==
|
||||||
|
|
||||||
ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.0, ajv@^6.12.4:
|
ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.4:
|
||||||
version "6.12.5"
|
version "6.12.5"
|
||||||
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.5.tgz#19b0e8bae8f476e5ba666300387775fb1a00a4da"
|
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.5.tgz#19b0e8bae8f476e5ba666300387775fb1a00a4da"
|
||||||
integrity sha512-lRF8RORchjpKG50/WFf8xmg7sgCLFiYNNnqdKflk63whMQcWR5ngGjiSXkL9bjxy6B2npOK2HSMN49jEBMSkag==
|
integrity sha512-lRF8RORchjpKG50/WFf8xmg7sgCLFiYNNnqdKflk63whMQcWR5ngGjiSXkL9bjxy6B2npOK2HSMN49jEBMSkag==
|
||||||
@ -1851,28 +1868,27 @@ builtin-status-codes@^3.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8"
|
resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8"
|
||||||
integrity sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=
|
integrity sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=
|
||||||
|
|
||||||
cacache@13.0.1:
|
cacache@15.0.5:
|
||||||
version "13.0.1"
|
version "15.0.5"
|
||||||
resolved "https://registry.yarnpkg.com/cacache/-/cacache-13.0.1.tgz#a8000c21697089082f85287a1aec6e382024a71c"
|
resolved "https://registry.yarnpkg.com/cacache/-/cacache-15.0.5.tgz#69162833da29170d6732334643c60e005f5f17d0"
|
||||||
integrity sha512-5ZvAxd05HDDU+y9BVvcqYu2LLXmPnQ0hW62h32g4xBTgL/MppR4/04NHfj/ycM2y6lmTnbw6HVi+1eN0Psba6w==
|
integrity sha512-lloiL22n7sOjEEXdL8NAjTgv9a1u43xICE9/203qonkZUCj5X1UEWIdf2/Y0d6QcCtMzbKQyhrcDbdvlZTs/+A==
|
||||||
dependencies:
|
dependencies:
|
||||||
chownr "^1.1.2"
|
"@npmcli/move-file" "^1.0.1"
|
||||||
figgy-pudding "^3.5.1"
|
chownr "^2.0.0"
|
||||||
fs-minipass "^2.0.0"
|
fs-minipass "^2.0.0"
|
||||||
glob "^7.1.4"
|
glob "^7.1.4"
|
||||||
graceful-fs "^4.2.2"
|
|
||||||
infer-owner "^1.0.4"
|
infer-owner "^1.0.4"
|
||||||
lru-cache "^5.1.1"
|
lru-cache "^6.0.0"
|
||||||
minipass "^3.0.0"
|
minipass "^3.1.1"
|
||||||
minipass-collect "^1.0.2"
|
minipass-collect "^1.0.2"
|
||||||
minipass-flush "^1.0.5"
|
minipass-flush "^1.0.5"
|
||||||
minipass-pipeline "^1.2.2"
|
minipass-pipeline "^1.2.2"
|
||||||
mkdirp "^0.5.1"
|
mkdirp "^1.0.3"
|
||||||
move-concurrently "^1.0.1"
|
p-map "^4.0.0"
|
||||||
p-map "^3.0.0"
|
|
||||||
promise-inflight "^1.0.1"
|
promise-inflight "^1.0.1"
|
||||||
rimraf "^2.7.1"
|
rimraf "^3.0.2"
|
||||||
ssri "^7.0.0"
|
ssri "^8.0.0"
|
||||||
|
tar "^6.0.2"
|
||||||
unique-filename "^1.1.1"
|
unique-filename "^1.1.1"
|
||||||
|
|
||||||
cacache@^12.0.2:
|
cacache@^12.0.2:
|
||||||
@ -1940,17 +1956,17 @@ camelcase@5.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.0.0.tgz#03295527d58bd3cd4aa75363f35b2e8d97be2f42"
|
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.0.0.tgz#03295527d58bd3cd4aa75363f35b2e8d97be2f42"
|
||||||
integrity sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA==
|
integrity sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA==
|
||||||
|
|
||||||
camelcase@5.3.1, camelcase@^5.3.1:
|
camelcase@5.3.1:
|
||||||
version "5.3.1"
|
version "5.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
|
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
|
||||||
integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
|
integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
|
||||||
|
|
||||||
caniuse-lite@^1.0.30001093, caniuse-lite@^1.0.30001131:
|
camelcase@^6.0.0:
|
||||||
version "1.0.30001132"
|
version "6.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001132.tgz#309279274f10d3aa736aa91fa269fcc8d0cd7ef9"
|
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.0.0.tgz#5259f7c30e35e278f1bdc2a4d91230b37cad981e"
|
||||||
integrity sha512-zk5FXbnsmHa0Ktc/NOZJRr+ilXva+2KFJuRiQfnjkxJfV/7DYP5C27lSQF++/veCUzVWE5xecZnSBJjf6fSwJA==
|
integrity sha512-8KMDF1Vz2gzOq54ONPJS65IvTUaB1cHJ2DMM7MbPmLZljDH1qpzzLsWdiN9pHh6qvkRVDTi/07+eNGch/oLU4w==
|
||||||
|
|
||||||
caniuse-lite@^1.0.30001113:
|
caniuse-lite@^1.0.30001093, caniuse-lite@^1.0.30001113, caniuse-lite@^1.0.30001131:
|
||||||
version "1.0.30001135"
|
version "1.0.30001135"
|
||||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001135.tgz#995b1eb94404a3c9a0d7600c113c9bb27f2cd8aa"
|
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001135.tgz#995b1eb94404a3c9a0d7600c113c9bb27f2cd8aa"
|
||||||
integrity sha512-ziNcheTGTHlu9g34EVoHQdIu5g4foc8EsxMGC7Xkokmvw0dqNtX8BS8RgCgFBaAiSp2IdjvBxNdh0ssib28eVQ==
|
integrity sha512-ziNcheTGTHlu9g34EVoHQdIu5g4foc8EsxMGC7Xkokmvw0dqNtX8BS8RgCgFBaAiSp2IdjvBxNdh0ssib28eVQ==
|
||||||
@ -2040,11 +2056,16 @@ chokidar@^3.4.1:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
fsevents "~2.1.2"
|
fsevents "~2.1.2"
|
||||||
|
|
||||||
chownr@^1.1.1, chownr@^1.1.2:
|
chownr@^1.1.1:
|
||||||
version "1.1.4"
|
version "1.1.4"
|
||||||
resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b"
|
resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b"
|
||||||
integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==
|
integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==
|
||||||
|
|
||||||
|
chownr@^2.0.0:
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece"
|
||||||
|
integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==
|
||||||
|
|
||||||
chrome-trace-event@^1.0.2:
|
chrome-trace-event@^1.0.2:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz#234090ee97c7d4ad1a2c4beae27505deffc608a4"
|
resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz#234090ee97c7d4ad1a2c4beae27505deffc608a4"
|
||||||
@ -2089,15 +2110,6 @@ clipboard@^2.0.0:
|
|||||||
select "^1.1.2"
|
select "^1.1.2"
|
||||||
tiny-emitter "^2.0.0"
|
tiny-emitter "^2.0.0"
|
||||||
|
|
||||||
clone-deep@^4.0.1:
|
|
||||||
version "4.0.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387"
|
|
||||||
integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==
|
|
||||||
dependencies:
|
|
||||||
is-plain-object "^2.0.4"
|
|
||||||
kind-of "^6.0.2"
|
|
||||||
shallow-clone "^3.0.0"
|
|
||||||
|
|
||||||
clsx@^1.0.4:
|
clsx@^1.0.4:
|
||||||
version "1.1.1"
|
version "1.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.1.tgz#98b3134f9abbdf23b2663491ace13c5c03a73188"
|
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.1.tgz#98b3134f9abbdf23b2663491ace13c5c03a73188"
|
||||||
@ -2321,24 +2333,23 @@ css-loader@1.0.0:
|
|||||||
postcss-value-parser "^3.3.0"
|
postcss-value-parser "^3.3.0"
|
||||||
source-list-map "^2.0.0"
|
source-list-map "^2.0.0"
|
||||||
|
|
||||||
css-loader@3.5.3:
|
css-loader@4.3.0:
|
||||||
version "3.5.3"
|
version "4.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-3.5.3.tgz#95ac16468e1adcd95c844729e0bb167639eb0bcf"
|
resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-4.3.0.tgz#c888af64b2a5b2e85462c72c0f4a85c7e2e0821e"
|
||||||
integrity sha512-UEr9NH5Lmi7+dguAm+/JSPovNjYbm2k3TK58EiwQHzOHH5Jfq1Y+XoP2bQO6TMn7PptMd0opxxedAWcaSTRKHw==
|
integrity sha512-rdezjCjScIrsL8BSYszgT4s476IcNKt6yX69t0pHjJVnPUTDpn4WfIpDQTN3wCJvUvfsz/mFjuGOekf3PY3NUg==
|
||||||
dependencies:
|
dependencies:
|
||||||
camelcase "^5.3.1"
|
camelcase "^6.0.0"
|
||||||
cssesc "^3.0.0"
|
cssesc "^3.0.0"
|
||||||
icss-utils "^4.1.1"
|
icss-utils "^4.1.1"
|
||||||
loader-utils "^1.2.3"
|
loader-utils "^2.0.0"
|
||||||
normalize-path "^3.0.0"
|
postcss "^7.0.32"
|
||||||
postcss "^7.0.27"
|
|
||||||
postcss-modules-extract-imports "^2.0.0"
|
postcss-modules-extract-imports "^2.0.0"
|
||||||
postcss-modules-local-by-default "^3.0.2"
|
postcss-modules-local-by-default "^3.0.3"
|
||||||
postcss-modules-scope "^2.2.0"
|
postcss-modules-scope "^2.2.0"
|
||||||
postcss-modules-values "^3.0.0"
|
postcss-modules-values "^3.0.0"
|
||||||
postcss-value-parser "^4.0.3"
|
postcss-value-parser "^4.1.0"
|
||||||
schema-utils "^2.6.6"
|
schema-utils "^2.7.1"
|
||||||
semver "^6.3.0"
|
semver "^7.3.2"
|
||||||
|
|
||||||
css-selector-tokenizer@^0.7.0:
|
css-selector-tokenizer@^0.7.0:
|
||||||
version "0.7.3"
|
version "0.7.3"
|
||||||
@ -3225,7 +3236,7 @@ good-listener@^1.2.2:
|
|||||||
dependencies:
|
dependencies:
|
||||||
delegate "^3.1.2"
|
delegate "^3.1.2"
|
||||||
|
|
||||||
graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.2.2:
|
graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2:
|
||||||
version "4.2.4"
|
version "4.2.4"
|
||||||
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb"
|
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb"
|
||||||
integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==
|
integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==
|
||||||
@ -3888,6 +3899,11 @@ kind-of@^6.0.0, kind-of@^6.0.2:
|
|||||||
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd"
|
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd"
|
||||||
integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==
|
integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==
|
||||||
|
|
||||||
|
klona@^2.0.3:
|
||||||
|
version "2.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.4.tgz#7bb1e3affb0cb8624547ef7e8f6708ea2e39dfc0"
|
||||||
|
integrity sha512-ZRbnvdg/NxqzC7L9Uyqzf4psi1OM4Cuc+sJAkQPjO6XkQIJTNbfK2Rsmbw8fx1p2mkZdp2FZYo2+LwXYY/uwIA==
|
||||||
|
|
||||||
leven@^3.1.0:
|
leven@^3.1.0:
|
||||||
version "3.1.0"
|
version "3.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2"
|
resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2"
|
||||||
@ -3993,7 +4009,7 @@ lowlight@^1.14.0:
|
|||||||
fault "^1.0.0"
|
fault "^1.0.0"
|
||||||
highlight.js "~10.1.0"
|
highlight.js "~10.1.0"
|
||||||
|
|
||||||
lru-cache@6.0.0:
|
lru-cache@6.0.0, lru-cache@^6.0.0:
|
||||||
version "6.0.0"
|
version "6.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
|
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
|
||||||
integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==
|
integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==
|
||||||
@ -4155,6 +4171,14 @@ minipass@^3.0.0, minipass@^3.1.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
yallist "^4.0.0"
|
yallist "^4.0.0"
|
||||||
|
|
||||||
|
minizlib@^2.1.1:
|
||||||
|
version "2.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931"
|
||||||
|
integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==
|
||||||
|
dependencies:
|
||||||
|
minipass "^3.0.0"
|
||||||
|
yallist "^4.0.0"
|
||||||
|
|
||||||
mississippi@^3.0.0:
|
mississippi@^3.0.0:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-3.0.0.tgz#ea0a3291f97e0b5e8776b363d5f0a12d94c67022"
|
resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-3.0.0.tgz#ea0a3291f97e0b5e8776b363d5f0a12d94c67022"
|
||||||
@ -4193,6 +4217,11 @@ mkdirp@^0.5.1, mkdirp@^0.5.3:
|
|||||||
dependencies:
|
dependencies:
|
||||||
minimist "^1.2.5"
|
minimist "^1.2.5"
|
||||||
|
|
||||||
|
mkdirp@^1.0.3, mkdirp@^1.0.4:
|
||||||
|
version "1.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
|
||||||
|
integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
|
||||||
|
|
||||||
monaco-editor-webpack-plugin@^1.9.0:
|
monaco-editor-webpack-plugin@^1.9.0:
|
||||||
version "1.9.1"
|
version "1.9.1"
|
||||||
resolved "https://registry.yarnpkg.com/monaco-editor-webpack-plugin/-/monaco-editor-webpack-plugin-1.9.1.tgz#eb4bbb1c5e5bfb554541c1ae1542e74c2a9f43fd"
|
resolved "https://registry.yarnpkg.com/monaco-editor-webpack-plugin/-/monaco-editor-webpack-plugin-1.9.1.tgz#eb4bbb1c5e5bfb554541c1ae1542e74c2a9f43fd"
|
||||||
@ -4266,7 +4295,7 @@ neo-async@2.6.1:
|
|||||||
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.1.tgz#ac27ada66167fa8849a6addd837f6b189ad2081c"
|
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.1.tgz#ac27ada66167fa8849a6addd837f6b189ad2081c"
|
||||||
integrity sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==
|
integrity sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==
|
||||||
|
|
||||||
neo-async@^2.5.0, neo-async@^2.6.1:
|
neo-async@^2.5.0, neo-async@^2.6.1, neo-async@^2.6.2:
|
||||||
version "2.6.2"
|
version "2.6.2"
|
||||||
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
|
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
|
||||||
integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
|
integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
|
||||||
@ -4284,22 +4313,21 @@ next-tick@~1.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c"
|
resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c"
|
||||||
integrity sha1-yobR/ogoFpsBICCOPchCS524NCw=
|
integrity sha1-yobR/ogoFpsBICCOPchCS524NCw=
|
||||||
|
|
||||||
next@^9.5.3:
|
next@^9.5.4:
|
||||||
version "9.5.3"
|
version "9.5.4"
|
||||||
resolved "https://registry.yarnpkg.com/next/-/next-9.5.3.tgz#7af5270631f98d330a7f75a6e8e1ac202aa155e2"
|
resolved "https://registry.yarnpkg.com/next/-/next-9.5.4.tgz#3c6aa3fd38ff1711e956ea2b6833475e0262ec35"
|
||||||
integrity sha512-DGrpTNGV2RNMwLaSzpgbkbaUuVk30X71/roXHS10isSXo2Gm+qWcjonDyOxf1KmOvHZRHA/Fa+LaAR7ysdYS3A==
|
integrity sha512-dicsJSxiUFcRjeZ/rNMAO3HS5ttFFuRHhdAn5g7lHnWUZ3MnEX4ggBIihaoUr6qu2So9KoqUPXpS91MuSXUmBw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@ampproject/toolbox-optimizer" "2.6.0"
|
"@ampproject/toolbox-optimizer" "2.6.0"
|
||||||
"@babel/code-frame" "7.10.4"
|
"@babel/code-frame" "7.10.4"
|
||||||
"@babel/core" "7.7.7"
|
"@babel/core" "7.7.7"
|
||||||
"@babel/plugin-proposal-class-properties" "7.10.4"
|
"@babel/plugin-proposal-class-properties" "7.10.4"
|
||||||
"@babel/plugin-proposal-export-namespace-from" "7.10.4"
|
"@babel/plugin-proposal-export-namespace-from" "7.10.4"
|
||||||
"@babel/plugin-proposal-nullish-coalescing-operator" "7.10.4"
|
|
||||||
"@babel/plugin-proposal-numeric-separator" "7.10.4"
|
"@babel/plugin-proposal-numeric-separator" "7.10.4"
|
||||||
"@babel/plugin-proposal-object-rest-spread" "7.11.0"
|
"@babel/plugin-proposal-object-rest-spread" "7.11.0"
|
||||||
"@babel/plugin-proposal-optional-chaining" "7.11.0"
|
|
||||||
"@babel/plugin-syntax-bigint" "7.8.3"
|
"@babel/plugin-syntax-bigint" "7.8.3"
|
||||||
"@babel/plugin-syntax-dynamic-import" "7.8.3"
|
"@babel/plugin-syntax-dynamic-import" "7.8.3"
|
||||||
|
"@babel/plugin-syntax-jsx" "7.10.4"
|
||||||
"@babel/plugin-transform-modules-commonjs" "7.10.4"
|
"@babel/plugin-transform-modules-commonjs" "7.10.4"
|
||||||
"@babel/plugin-transform-runtime" "7.11.5"
|
"@babel/plugin-transform-runtime" "7.11.5"
|
||||||
"@babel/preset-env" "7.11.5"
|
"@babel/preset-env" "7.11.5"
|
||||||
@ -4308,19 +4336,20 @@ next@^9.5.3:
|
|||||||
"@babel/preset-typescript" "7.10.4"
|
"@babel/preset-typescript" "7.10.4"
|
||||||
"@babel/runtime" "7.11.2"
|
"@babel/runtime" "7.11.2"
|
||||||
"@babel/types" "7.11.5"
|
"@babel/types" "7.11.5"
|
||||||
"@next/react-dev-overlay" "9.5.3"
|
"@next/env" "9.5.4"
|
||||||
"@next/react-refresh-utils" "9.5.3"
|
"@next/polyfill-module" "9.5.4"
|
||||||
|
"@next/react-dev-overlay" "9.5.4"
|
||||||
|
"@next/react-refresh-utils" "9.5.4"
|
||||||
ast-types "0.13.2"
|
ast-types "0.13.2"
|
||||||
babel-plugin-syntax-jsx "6.18.0"
|
|
||||||
babel-plugin-transform-define "2.0.0"
|
babel-plugin-transform-define "2.0.0"
|
||||||
babel-plugin-transform-react-remove-prop-types "0.4.24"
|
babel-plugin-transform-react-remove-prop-types "0.4.24"
|
||||||
browserslist "4.13.0"
|
browserslist "4.13.0"
|
||||||
buffer "5.6.0"
|
buffer "5.6.0"
|
||||||
cacache "13.0.1"
|
cacache "15.0.5"
|
||||||
caniuse-lite "^1.0.30001113"
|
caniuse-lite "^1.0.30001113"
|
||||||
chokidar "2.1.8"
|
chokidar "2.1.8"
|
||||||
crypto-browserify "3.12.0"
|
crypto-browserify "3.12.0"
|
||||||
css-loader "3.5.3"
|
css-loader "4.3.0"
|
||||||
cssnano-simple "1.2.0"
|
cssnano-simple "1.2.0"
|
||||||
find-cache-dir "3.3.1"
|
find-cache-dir "3.3.1"
|
||||||
jest-worker "24.9.0"
|
jest-worker "24.9.0"
|
||||||
@ -4337,15 +4366,15 @@ next@^9.5.3:
|
|||||||
react-is "16.13.1"
|
react-is "16.13.1"
|
||||||
react-refresh "0.8.3"
|
react-refresh "0.8.3"
|
||||||
resolve-url-loader "3.1.1"
|
resolve-url-loader "3.1.1"
|
||||||
sass-loader "8.0.2"
|
sass-loader "10.0.2"
|
||||||
schema-utils "2.6.6"
|
schema-utils "2.7.1"
|
||||||
stream-browserify "3.0.0"
|
stream-browserify "3.0.0"
|
||||||
style-loader "1.2.1"
|
style-loader "1.2.1"
|
||||||
styled-jsx "3.3.0"
|
styled-jsx "3.3.0"
|
||||||
use-subscription "1.4.1"
|
use-subscription "1.4.1"
|
||||||
vm-browserify "1.1.2"
|
vm-browserify "1.1.2"
|
||||||
watchpack "2.0.0-beta.13"
|
watchpack "2.0.0-beta.13"
|
||||||
web-vitals "0.2.1"
|
web-vitals "0.2.4"
|
||||||
webpack "4.44.1"
|
webpack "4.44.1"
|
||||||
webpack-sources "1.4.3"
|
webpack-sources "1.4.3"
|
||||||
|
|
||||||
@ -4531,10 +4560,10 @@ p-locate@^4.1.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
p-limit "^2.2.0"
|
p-limit "^2.2.0"
|
||||||
|
|
||||||
p-map@^3.0.0:
|
p-map@^4.0.0:
|
||||||
version "3.0.0"
|
version "4.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/p-map/-/p-map-3.0.0.tgz#d704d9af8a2ba684e2600d9a215983d4141a979d"
|
resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b"
|
||||||
integrity sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==
|
integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
aggregate-error "^3.0.0"
|
aggregate-error "^3.0.0"
|
||||||
|
|
||||||
@ -4742,7 +4771,7 @@ postcss-modules-local-by-default@^1.2.0:
|
|||||||
css-selector-tokenizer "^0.7.0"
|
css-selector-tokenizer "^0.7.0"
|
||||||
postcss "^6.0.1"
|
postcss "^6.0.1"
|
||||||
|
|
||||||
postcss-modules-local-by-default@^3.0.2:
|
postcss-modules-local-by-default@^3.0.3:
|
||||||
version "3.0.3"
|
version "3.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-3.0.3.tgz#bb14e0cc78279d504dbdcbfd7e0ca28993ffbbb0"
|
resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-3.0.3.tgz#bb14e0cc78279d504dbdcbfd7e0ca28993ffbbb0"
|
||||||
integrity sha512-e3xDq+LotiGesympRlKNgaJ0PCzoUIdpH0dj47iWAui/kyTgh3CiAr1qP54uodmJhl6p9rN6BoNcdEDVJx9RDw==
|
integrity sha512-e3xDq+LotiGesympRlKNgaJ0PCzoUIdpH0dj47iWAui/kyTgh3CiAr1qP54uodmJhl6p9rN6BoNcdEDVJx9RDw==
|
||||||
@ -4805,7 +4834,7 @@ postcss-value-parser@^3.3.0:
|
|||||||
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281"
|
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281"
|
||||||
integrity sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==
|
integrity sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==
|
||||||
|
|
||||||
postcss-value-parser@^4.0.3, postcss-value-parser@^4.1.0:
|
postcss-value-parser@^4.1.0:
|
||||||
version "4.1.0"
|
version "4.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz#443f6a20ced6481a2bda4fa8532a6e55d789a2cb"
|
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz#443f6a20ced6481a2bda4fa8532a6e55d789a2cb"
|
||||||
integrity sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==
|
integrity sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==
|
||||||
@ -4837,7 +4866,7 @@ postcss@^6.0.1, postcss@^6.0.23:
|
|||||||
source-map "^0.6.1"
|
source-map "^0.6.1"
|
||||||
supports-color "^5.4.0"
|
supports-color "^5.4.0"
|
||||||
|
|
||||||
postcss@^7.0.0, postcss@^7.0.14, postcss@^7.0.26, postcss@^7.0.27, postcss@^7.0.32, postcss@^7.0.5, postcss@^7.0.6:
|
postcss@^7.0.0, postcss@^7.0.14, postcss@^7.0.26, postcss@^7.0.32, postcss@^7.0.5, postcss@^7.0.6:
|
||||||
version "7.0.34"
|
version "7.0.34"
|
||||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.34.tgz#f2baf57c36010df7de4009940f21532c16d65c20"
|
resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.34.tgz#f2baf57c36010df7de4009940f21532c16d65c20"
|
||||||
integrity sha512-H/7V2VeNScX9KE83GDrDZNiGT1m2H+UTnlinIzhjlLX9hfMUn1mHNnGeX81a1c8JSBdBvqk7c2ZOG6ZPn5itGw==
|
integrity sha512-H/7V2VeNScX9KE83GDrDZNiGT1m2H+UTnlinIzhjlLX9hfMUn1mHNnGeX81a1c8JSBdBvqk7c2ZOG6ZPn5itGw==
|
||||||
@ -5237,13 +5266,20 @@ rimraf@2.6.3:
|
|||||||
dependencies:
|
dependencies:
|
||||||
glob "^7.1.3"
|
glob "^7.1.3"
|
||||||
|
|
||||||
rimraf@^2.5.4, rimraf@^2.6.3, rimraf@^2.7.1:
|
rimraf@^2.5.4, rimraf@^2.6.3:
|
||||||
version "2.7.1"
|
version "2.7.1"
|
||||||
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
|
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
|
||||||
integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==
|
integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==
|
||||||
dependencies:
|
dependencies:
|
||||||
glob "^7.1.3"
|
glob "^7.1.3"
|
||||||
|
|
||||||
|
rimraf@^3.0.2:
|
||||||
|
version "3.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
|
||||||
|
integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
|
||||||
|
dependencies:
|
||||||
|
glob "^7.1.3"
|
||||||
|
|
||||||
ripemd160@^2.0.0, ripemd160@^2.0.1:
|
ripemd160@^2.0.0, ripemd160@^2.0.1:
|
||||||
version "2.0.2"
|
version "2.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c"
|
resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c"
|
||||||
@ -5281,16 +5317,16 @@ safer-buffer@^2.1.0:
|
|||||||
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
|
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
|
||||||
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
|
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
|
||||||
|
|
||||||
sass-loader@8.0.2:
|
sass-loader@10.0.2:
|
||||||
version "8.0.2"
|
version "10.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-8.0.2.tgz#debecd8c3ce243c76454f2e8290482150380090d"
|
resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-10.0.2.tgz#c7b73010848b264792dd45372eea0b87cba4401e"
|
||||||
integrity sha512-7o4dbSK8/Ol2KflEmSco4jTjQoV988bM82P9CZdmo9hR3RLnvNc0ufMNdMrB0caq38JQ/FgF4/7RcbcfKzxoFQ==
|
integrity sha512-wV6NDUVB8/iEYMalV/+139+vl2LaRFlZGEd5/xmdcdzQcgmis+npyco6NsDTVOlNA3y2NV9Gcz+vHyFMIT+ffg==
|
||||||
dependencies:
|
dependencies:
|
||||||
clone-deep "^4.0.1"
|
klona "^2.0.3"
|
||||||
loader-utils "^1.2.3"
|
loader-utils "^2.0.0"
|
||||||
neo-async "^2.6.1"
|
neo-async "^2.6.2"
|
||||||
schema-utils "^2.6.1"
|
schema-utils "^2.7.1"
|
||||||
semver "^6.3.0"
|
semver "^7.3.2"
|
||||||
|
|
||||||
scheduler@^0.19.1:
|
scheduler@^0.19.1:
|
||||||
version "0.19.1"
|
version "0.19.1"
|
||||||
@ -5300,13 +5336,14 @@ scheduler@^0.19.1:
|
|||||||
loose-envify "^1.1.0"
|
loose-envify "^1.1.0"
|
||||||
object-assign "^4.1.1"
|
object-assign "^4.1.1"
|
||||||
|
|
||||||
schema-utils@2.6.6:
|
schema-utils@2.7.1, schema-utils@^2.6.6, schema-utils@^2.7.1:
|
||||||
version "2.6.6"
|
version "2.7.1"
|
||||||
resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.6.6.tgz#299fe6bd4a3365dc23d99fd446caff8f1d6c330c"
|
resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.1.tgz#1ca4f32d1b24c590c203b8e7a50bf0ea4cd394d7"
|
||||||
integrity sha512-wHutF/WPSbIi9x6ctjGGk2Hvl0VOz5l3EKEuKbjPlB30mKZUzb9A5k9yEXRX3pwyqVLPvpfZZEllaFq/M718hA==
|
integrity sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==
|
||||||
dependencies:
|
dependencies:
|
||||||
ajv "^6.12.0"
|
"@types/json-schema" "^7.0.5"
|
||||||
ajv-keywords "^3.4.1"
|
ajv "^6.12.4"
|
||||||
|
ajv-keywords "^3.5.2"
|
||||||
|
|
||||||
schema-utils@^1.0.0:
|
schema-utils@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
@ -5317,15 +5354,6 @@ schema-utils@^1.0.0:
|
|||||||
ajv-errors "^1.0.0"
|
ajv-errors "^1.0.0"
|
||||||
ajv-keywords "^3.1.0"
|
ajv-keywords "^3.1.0"
|
||||||
|
|
||||||
schema-utils@^2.6.1, schema-utils@^2.6.6:
|
|
||||||
version "2.7.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.1.tgz#1ca4f32d1b24c590c203b8e7a50bf0ea4cd394d7"
|
|
||||||
integrity sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==
|
|
||||||
dependencies:
|
|
||||||
"@types/json-schema" "^7.0.5"
|
|
||||||
ajv "^6.12.4"
|
|
||||||
ajv-keywords "^3.5.2"
|
|
||||||
|
|
||||||
select@^1.1.2:
|
select@^1.1.2:
|
||||||
version "1.1.2"
|
version "1.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/select/-/select-1.1.2.tgz#0e7350acdec80b1108528786ec1d4418d11b396d"
|
resolved "https://registry.yarnpkg.com/select/-/select-1.1.2.tgz#0e7350acdec80b1108528786ec1d4418d11b396d"
|
||||||
@ -5341,12 +5369,12 @@ semver@^5.4.1, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0:
|
|||||||
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
|
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
|
||||||
integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
|
integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
|
||||||
|
|
||||||
semver@^6.0.0, semver@^6.3.0:
|
semver@^6.0.0:
|
||||||
version "6.3.0"
|
version "6.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
|
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
|
||||||
integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
|
integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
|
||||||
|
|
||||||
semver@^7.2.1:
|
semver@^7.2.1, semver@^7.3.2:
|
||||||
version "7.3.2"
|
version "7.3.2"
|
||||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.2.tgz#604962b052b81ed0786aae84389ffba70ffd3938"
|
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.2.tgz#604962b052b81ed0786aae84389ffba70ffd3938"
|
||||||
integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==
|
integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==
|
||||||
@ -5381,13 +5409,6 @@ sha.js@^2.4.0, sha.js@^2.4.8:
|
|||||||
inherits "^2.0.1"
|
inherits "^2.0.1"
|
||||||
safe-buffer "^5.0.1"
|
safe-buffer "^5.0.1"
|
||||||
|
|
||||||
shallow-clone@^3.0.0:
|
|
||||||
version "3.0.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3"
|
|
||||||
integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==
|
|
||||||
dependencies:
|
|
||||||
kind-of "^6.0.2"
|
|
||||||
|
|
||||||
shebang-command@^2.0.0:
|
shebang-command@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
|
resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
|
||||||
@ -5519,12 +5540,11 @@ ssri@^6.0.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
figgy-pudding "^3.5.1"
|
figgy-pudding "^3.5.1"
|
||||||
|
|
||||||
ssri@^7.0.0:
|
ssri@^8.0.0:
|
||||||
version "7.1.0"
|
version "8.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/ssri/-/ssri-7.1.0.tgz#92c241bf6de82365b5c7fb4bd76e975522e1294d"
|
resolved "https://registry.yarnpkg.com/ssri/-/ssri-8.0.0.tgz#79ca74e21f8ceaeddfcb4b90143c458b8d988808"
|
||||||
integrity sha512-77/WrDZUWocK0mvA5NTRQyveUf+wsrIc6vyrxpS8tVvYBcX215QbafrJR3KtkpskIzoFLqqNuuYQvxaMjXJ/0g==
|
integrity sha512-aq/pz989nxVYwn16Tsbj1TqFpD5LLrQxHf5zaHuieFV+R0Bbr4y8qUsOA45hXT/N4/9UNXTarBjnjVmjSOVaAA==
|
||||||
dependencies:
|
dependencies:
|
||||||
figgy-pudding "^3.5.1"
|
|
||||||
minipass "^3.1.1"
|
minipass "^3.1.1"
|
||||||
|
|
||||||
stacktrace-parser@0.1.10:
|
stacktrace-parser@0.1.10:
|
||||||
@ -5730,6 +5750,18 @@ tapable@^1.0.0, tapable@^1.1.3:
|
|||||||
resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2"
|
resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2"
|
||||||
integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==
|
integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==
|
||||||
|
|
||||||
|
tar@^6.0.2:
|
||||||
|
version "6.0.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/tar/-/tar-6.0.5.tgz#bde815086e10b39f1dcd298e89d596e1535e200f"
|
||||||
|
integrity sha512-0b4HOimQHj9nXNEAA7zWwMM91Zhhba3pspja6sQbgTpynOJf+bkjBnfybNYzbpLbnwXnbyB4LOREvlyXLkCHSg==
|
||||||
|
dependencies:
|
||||||
|
chownr "^2.0.0"
|
||||||
|
fs-minipass "^2.0.0"
|
||||||
|
minipass "^3.0.0"
|
||||||
|
minizlib "^2.1.1"
|
||||||
|
mkdirp "^1.0.3"
|
||||||
|
yallist "^4.0.0"
|
||||||
|
|
||||||
terser-webpack-plugin@^1.4.3:
|
terser-webpack-plugin@^1.4.3:
|
||||||
version "1.4.5"
|
version "1.4.5"
|
||||||
resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.4.5.tgz#a217aefaea330e734ffacb6120ec1fa312d6040b"
|
resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.4.5.tgz#a217aefaea330e734ffacb6120ec1fa312d6040b"
|
||||||
@ -6067,10 +6099,10 @@ watchpack@^1.7.4:
|
|||||||
chokidar "^3.4.1"
|
chokidar "^3.4.1"
|
||||||
watchpack-chokidar2 "^2.0.0"
|
watchpack-chokidar2 "^2.0.0"
|
||||||
|
|
||||||
web-vitals@0.2.1:
|
web-vitals@0.2.4:
|
||||||
version "0.2.1"
|
version "0.2.4"
|
||||||
resolved "https://registry.yarnpkg.com/web-vitals/-/web-vitals-0.2.1.tgz#60782fa690243fe35613759a0c26431f57ba7b2d"
|
resolved "https://registry.yarnpkg.com/web-vitals/-/web-vitals-0.2.4.tgz#ec3df43c834a207fd7cdefd732b2987896e08511"
|
||||||
integrity sha512-2pdRlp6gJpOCg0oMMqwFF0axjk5D9WInc09RSYtqFgPXQ15+YKNQ7YnBBEqAL5jvmfH9WvoXDMb8DHwux7pIew==
|
integrity sha512-6BjspCO9VriYy12z356nL6JBS0GYeEcA457YyRzD+dD6XYCQ75NKhcOHUMHentOE7OcVCIXXDvOm0jKFfQG2Gg==
|
||||||
|
|
||||||
webidl-conversions@^4.0.2:
|
webidl-conversions@^4.0.2:
|
||||||
version "4.0.2"
|
version "4.0.2"
|
||||||
|
@ -2,7 +2,6 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
|
||||||
"flag"
|
"flag"
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
@ -12,50 +11,75 @@ import (
|
|||||||
|
|
||||||
rice "github.com/GeertJohan/go.rice"
|
rice "github.com/GeertJohan/go.rice"
|
||||||
"github.com/dstotijn/hetty/pkg/api"
|
"github.com/dstotijn/hetty/pkg/api"
|
||||||
"github.com/dstotijn/hetty/pkg/db/cayley"
|
"github.com/dstotijn/hetty/pkg/db/sqlite"
|
||||||
|
"github.com/dstotijn/hetty/pkg/proj"
|
||||||
"github.com/dstotijn/hetty/pkg/proxy"
|
"github.com/dstotijn/hetty/pkg/proxy"
|
||||||
"github.com/dstotijn/hetty/pkg/reqlog"
|
"github.com/dstotijn/hetty/pkg/reqlog"
|
||||||
|
"github.com/dstotijn/hetty/pkg/scope"
|
||||||
|
|
||||||
"github.com/99designs/gqlgen/graphql/handler"
|
"github.com/99designs/gqlgen/graphql/handler"
|
||||||
"github.com/99designs/gqlgen/graphql/playground"
|
"github.com/99designs/gqlgen/graphql/playground"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/mitchellh/go-homedir"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
caCertFile string
|
caCertFile string
|
||||||
caKeyFile string
|
caKeyFile string
|
||||||
dbFile string
|
projPath string
|
||||||
addr string
|
addr string
|
||||||
adminPath string
|
adminPath string
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
flag.StringVar(&caCertFile, "cert", "", "CA certificate file path")
|
flag.StringVar(&caCertFile, "cert", "~/.hetty/hetty_cert.pem", "CA certificate filepath. Creates a new CA certificate is file doesn't exist")
|
||||||
flag.StringVar(&caKeyFile, "key", "", "CA private key file path")
|
flag.StringVar(&caKeyFile, "key", "~/.hetty/hetty_key.pem", "CA private key filepath. Creates a new CA private key if file doesn't exist")
|
||||||
flag.StringVar(&dbFile, "db", "hetty.db", "Database file path")
|
flag.StringVar(&projPath, "projects", "~/.hetty/projects", "Projects directory path")
|
||||||
flag.StringVar(&addr, "addr", ":80", "TCP address to listen on, in the form \"host:port\"")
|
flag.StringVar(&addr, "addr", ":8080", "TCP address to listen on, in the form \"host:port\"")
|
||||||
flag.StringVar(&adminPath, "adminPath", "", "File path to admin build")
|
flag.StringVar(&adminPath, "adminPath", "", "File path to admin build")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
tlsCA, err := tls.LoadX509KeyPair(caCertFile, caKeyFile)
|
// Expand `~` in filepaths.
|
||||||
|
caCertFile, err := homedir.Expand(caCertFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("[FATAL] Could not load CA key pair: %v", err)
|
log.Fatalf("[FATAL] Could not parse CA certificate filepath: %v", err)
|
||||||
|
}
|
||||||
|
caKeyFile, err := homedir.Expand(caKeyFile)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("[FATAL] Could not parse CA private key filepath: %v", err)
|
||||||
|
}
|
||||||
|
projPath, err := homedir.Expand(projPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("[FATAL] Could not parse projects filepath: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
caCert, err := x509.ParseCertificate(tlsCA.Certificate[0])
|
// Load existing CA certificate and key from disk, or generate and write
|
||||||
|
// to disk if no files exist yet.
|
||||||
|
caCert, caKey, err := proxy.LoadOrCreateCA(caKeyFile, caCertFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("[FATAL] Could not parse CA: %v", err)
|
log.Fatalf("[FATAL] Could not create/load CA key pair: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
db, err := cayley.NewDatabase(dbFile)
|
db, err := sqlite.New(projPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("[FATAL] Could not initialize database: %v", err)
|
log.Fatalf("[FATAL] Could not initialize database client: %v", err)
|
||||||
}
|
}
|
||||||
defer db.Close()
|
|
||||||
|
|
||||||
reqLogService := reqlog.NewService(db)
|
projService, err := proj.NewService(db)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("[FATAL] Could not create new project service: %v", err)
|
||||||
|
}
|
||||||
|
defer projService.Close()
|
||||||
|
|
||||||
p, err := proxy.NewProxy(caCert, tlsCA.PrivateKey)
|
scope := scope.New(db, projService)
|
||||||
|
|
||||||
|
reqLogService := reqlog.NewService(reqlog.Config{
|
||||||
|
Scope: scope,
|
||||||
|
ProjectService: projService,
|
||||||
|
Repository: db,
|
||||||
|
})
|
||||||
|
|
||||||
|
p, err := proxy.NewProxy(caCert, caKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("[FATAL] Could not create Proxy: %v", err)
|
log.Fatalf("[FATAL] Could not create Proxy: %v", err)
|
||||||
}
|
}
|
||||||
@ -87,6 +111,8 @@ func main() {
|
|||||||
adminRouter.Path("/api/playground/").Handler(playground.Handler("GraphQL Playground", "/api/graphql/"))
|
adminRouter.Path("/api/playground/").Handler(playground.Handler("GraphQL Playground", "/api/graphql/"))
|
||||||
adminRouter.Path("/api/graphql/").Handler(handler.NewDefaultServer(api.NewExecutableSchema(api.Config{Resolvers: &api.Resolver{
|
adminRouter.Path("/api/graphql/").Handler(handler.NewDefaultServer(api.NewExecutableSchema(api.Config{Resolvers: &api.Resolver{
|
||||||
RequestLogService: reqLogService,
|
RequestLogService: reqLogService,
|
||||||
|
ProjectService: projService,
|
||||||
|
ScopeService: scope,
|
||||||
}})))
|
}})))
|
||||||
|
|
||||||
// Admin interface.
|
// Admin interface.
|
||||||
|
12
docs/.gitignore
vendored
Executable file
@ -0,0 +1,12 @@
|
|||||||
|
pids
|
||||||
|
logs
|
||||||
|
node_modules
|
||||||
|
npm-debug.log
|
||||||
|
coverage/
|
||||||
|
run
|
||||||
|
dist
|
||||||
|
.DS_Store
|
||||||
|
.nyc_output
|
||||||
|
.basement
|
||||||
|
config.local.js
|
||||||
|
basement_dist
|
21
docs/package.json
Executable file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"name": "hetty-docs",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "An HTTP toolkit for security research.",
|
||||||
|
"main": "index.js",
|
||||||
|
"authors": {
|
||||||
|
"name": "David Stotijn",
|
||||||
|
"email": "dstotijn@gmail.com"
|
||||||
|
},
|
||||||
|
"repository": "github.com/dstotijn/hetty/docs",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vuepress dev src",
|
||||||
|
"build": "vuepress build src"
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"devDependencies": {
|
||||||
|
"markdown-it-imsize": "^2.0.1",
|
||||||
|
"vuepress": "^1.5.3"
|
||||||
|
},
|
||||||
|
"dependencies": {}
|
||||||
|
}
|
77
docs/src/.vuepress/config.js
Executable file
@ -0,0 +1,77 @@
|
|||||||
|
const { description } = require("../../package");
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
port: 3000,
|
||||||
|
title: "Hetty",
|
||||||
|
description: description,
|
||||||
|
head: [
|
||||||
|
["meta", { name: "theme-color", content: "#30e3b7" }],
|
||||||
|
["meta", { name: "apple-mobile-web-app-capable", content: "yes" }],
|
||||||
|
[
|
||||||
|
"meta",
|
||||||
|
{ name: "apple-mobile-web-app-status-bar-style", content: "black" },
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"meta",
|
||||||
|
{
|
||||||
|
property: "og:title",
|
||||||
|
content: "Hetty",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"meta",
|
||||||
|
{
|
||||||
|
property: "og:description",
|
||||||
|
content: "An HTTP toolkit for security research.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"meta",
|
||||||
|
{
|
||||||
|
property: "og:image",
|
||||||
|
content: "https://hetty.xyz/assets/hetty_v0.2.0_header.png",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
themeConfig: {
|
||||||
|
repo: "dstotijn/hetty",
|
||||||
|
editLinks: true,
|
||||||
|
docsDir: "docs/src",
|
||||||
|
editLinkText: "",
|
||||||
|
lastUpdated: true,
|
||||||
|
logo: "/assets/logo.png",
|
||||||
|
nav: [
|
||||||
|
{
|
||||||
|
text: "Guide",
|
||||||
|
link: "/guide/",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "Appendix",
|
||||||
|
link: "/appendix/",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
sidebar: {
|
||||||
|
"/guide/": [
|
||||||
|
{
|
||||||
|
title: "Guide",
|
||||||
|
collapsable: false,
|
||||||
|
children: ["", "getting-started", "modules"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"/appendix/": [
|
||||||
|
{
|
||||||
|
title: "Appendix",
|
||||||
|
collapsable: false,
|
||||||
|
children: [""],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: ["@vuepress/plugin-back-to-top", "@vuepress/plugin-medium-zoom"],
|
||||||
|
markdown: {
|
||||||
|
toc: { includeLevel: [2] },
|
||||||
|
extendMarkdown: (md) => {
|
||||||
|
md.use(require("markdown-it-imsize"));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
14
docs/src/.vuepress/enhanceApp.js
Executable file
@ -0,0 +1,14 @@
|
|||||||
|
/**
|
||||||
|
* Client app enhancement file.
|
||||||
|
*
|
||||||
|
* https://v1.vuepress.vuejs.org/guide/basic-config.html#app-level-enhancements
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default ({
|
||||||
|
Vue, // the version of Vue being used in the VuePress app
|
||||||
|
options, // the options for the root Vue instance
|
||||||
|
router, // the router instance for the app
|
||||||
|
siteData // site metadata
|
||||||
|
}) => {
|
||||||
|
// ...apply enhancements for the site.
|
||||||
|
}
|
BIN
docs/src/.vuepress/public/assets/hetty_v0.2.0_header.png
Normal file
After Width: | Height: | Size: 144 KiB |
BIN
docs/src/.vuepress/public/assets/logo.png
Normal file
After Width: | Height: | Size: 13 KiB |
9
docs/src/.vuepress/styles/index.styl
Executable file
@ -0,0 +1,9 @@
|
|||||||
|
/**
|
||||||
|
* Custom Styles here.
|
||||||
|
*
|
||||||
|
* ref:https://v1.vuepress.vuejs.org/config/#index-styl
|
||||||
|
*/
|
||||||
|
|
||||||
|
.home .hero img
|
||||||
|
width 450px
|
||||||
|
max-width 100%!important
|
11
docs/src/.vuepress/styles/palette.styl
Executable file
@ -0,0 +1,11 @@
|
|||||||
|
/**
|
||||||
|
* Custom palette here.
|
||||||
|
*
|
||||||
|
* ref:https://v1.vuepress.vuejs.org/zh/config/#palette-styl
|
||||||
|
*/
|
||||||
|
|
||||||
|
$accentColor = #2CC09B
|
||||||
|
$textColor = #2c3e50
|
||||||
|
$borderColor = #eaecef
|
||||||
|
$codeBgColor = #282c34
|
||||||
|
$badgeTipColor = #2CC09B
|
21
docs/src/.vuepress/theme/LICENSE
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2018-present, Yuxi (Evan) You
|
||||||
|
|
||||||
|
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.
|
171
docs/src/.vuepress/theme/components/AlgoliaSearchBox.vue
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
<template>
|
||||||
|
<form
|
||||||
|
id="search-form"
|
||||||
|
class="algolia-search-wrapper search-box"
|
||||||
|
role="search"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
id="algolia-search-input"
|
||||||
|
class="search-query"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'AlgoliaSearchBox',
|
||||||
|
|
||||||
|
props: ['options'],
|
||||||
|
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
placeholder: undefined
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
$lang (newValue) {
|
||||||
|
this.update(this.options, newValue)
|
||||||
|
},
|
||||||
|
|
||||||
|
options (newValue) {
|
||||||
|
this.update(newValue, this.$lang)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted () {
|
||||||
|
this.initialize(this.options, this.$lang)
|
||||||
|
this.placeholder = this.$site.themeConfig.searchPlaceholder || ''
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
initialize (userOptions, lang) {
|
||||||
|
Promise.all([
|
||||||
|
import(/* webpackChunkName: "docsearch" */ 'docsearch.js/dist/cdn/docsearch.min.js'),
|
||||||
|
import(/* webpackChunkName: "docsearch" */ 'docsearch.js/dist/cdn/docsearch.min.css')
|
||||||
|
]).then(([docsearch]) => {
|
||||||
|
docsearch = docsearch.default
|
||||||
|
const { algoliaOptions = {}} = userOptions
|
||||||
|
docsearch(Object.assign(
|
||||||
|
{},
|
||||||
|
userOptions,
|
||||||
|
{
|
||||||
|
inputSelector: '#algolia-search-input',
|
||||||
|
// #697 Make docsearch work well at i18n mode.
|
||||||
|
algoliaOptions: Object.assign({
|
||||||
|
'facetFilters': [`lang:${lang}`].concat(algoliaOptions.facetFilters || [])
|
||||||
|
}, algoliaOptions),
|
||||||
|
handleSelected: (input, event, suggestion) => {
|
||||||
|
const { pathname, hash } = new URL(suggestion.url)
|
||||||
|
const routepath = pathname.replace(this.$site.base, '/')
|
||||||
|
const _hash = decodeURIComponent(hash)
|
||||||
|
this.$router.push(`${routepath}${_hash}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
))
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
update (options, lang) {
|
||||||
|
this.$el.innerHTML = '<input id="algolia-search-input" class="search-query">'
|
||||||
|
this.initialize(options, lang)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="stylus">
|
||||||
|
.algolia-search-wrapper
|
||||||
|
& > span
|
||||||
|
vertical-align middle
|
||||||
|
.algolia-autocomplete
|
||||||
|
line-height normal
|
||||||
|
.ds-dropdown-menu
|
||||||
|
background-color #fff
|
||||||
|
border 1px solid #999
|
||||||
|
border-radius 4px
|
||||||
|
font-size 16px
|
||||||
|
margin 6px 0 0
|
||||||
|
padding 4px
|
||||||
|
text-align left
|
||||||
|
&:before
|
||||||
|
border-color #999
|
||||||
|
[class*=ds-dataset-]
|
||||||
|
border none
|
||||||
|
padding 0
|
||||||
|
.ds-suggestions
|
||||||
|
margin-top 0
|
||||||
|
.ds-suggestion
|
||||||
|
border-bottom 1px solid $borderColor
|
||||||
|
.algolia-docsearch-suggestion--highlight
|
||||||
|
color #2c815b
|
||||||
|
.algolia-docsearch-suggestion
|
||||||
|
border-color $borderColor
|
||||||
|
padding 0
|
||||||
|
.algolia-docsearch-suggestion--category-header
|
||||||
|
padding 5px 10px
|
||||||
|
margin-top 0
|
||||||
|
background $accentColor
|
||||||
|
color #fff
|
||||||
|
font-weight 600
|
||||||
|
.algolia-docsearch-suggestion--highlight
|
||||||
|
background rgba(255, 255, 255, 0.6)
|
||||||
|
.algolia-docsearch-suggestion--wrapper
|
||||||
|
padding 0
|
||||||
|
.algolia-docsearch-suggestion--title
|
||||||
|
font-weight 600
|
||||||
|
margin-bottom 0
|
||||||
|
color $textColor
|
||||||
|
.algolia-docsearch-suggestion--subcategory-column
|
||||||
|
vertical-align top
|
||||||
|
padding 5px 7px 5px 5px
|
||||||
|
border-color $borderColor
|
||||||
|
background #f1f3f5
|
||||||
|
&:after
|
||||||
|
display none
|
||||||
|
.algolia-docsearch-suggestion--subcategory-column-text
|
||||||
|
color #555
|
||||||
|
.algolia-docsearch-footer
|
||||||
|
border-color $borderColor
|
||||||
|
.ds-cursor .algolia-docsearch-suggestion--content
|
||||||
|
background-color #e7edf3 !important
|
||||||
|
color $textColor
|
||||||
|
|
||||||
|
@media (min-width: $MQMobile)
|
||||||
|
.algolia-search-wrapper
|
||||||
|
.algolia-autocomplete
|
||||||
|
.algolia-docsearch-suggestion
|
||||||
|
.algolia-docsearch-suggestion--subcategory-column
|
||||||
|
float none
|
||||||
|
width 150px
|
||||||
|
min-width 150px
|
||||||
|
display table-cell
|
||||||
|
.algolia-docsearch-suggestion--content
|
||||||
|
float none
|
||||||
|
display table-cell
|
||||||
|
width 100%
|
||||||
|
vertical-align top
|
||||||
|
.ds-dropdown-menu
|
||||||
|
min-width 515px !important
|
||||||
|
|
||||||
|
@media (max-width: $MQMobile)
|
||||||
|
.algolia-search-wrapper
|
||||||
|
.ds-dropdown-menu
|
||||||
|
min-width calc(100vw - 4rem) !important
|
||||||
|
max-width calc(100vw - 4rem) !important
|
||||||
|
.algolia-docsearch-suggestion--wrapper
|
||||||
|
padding 5px 7px 5px 5px !important
|
||||||
|
.algolia-docsearch-suggestion--subcategory-column
|
||||||
|
padding 0 !important
|
||||||
|
background white !important
|
||||||
|
.algolia-docsearch-suggestion--subcategory-column-text:after
|
||||||
|
content " > "
|
||||||
|
font-size 10px
|
||||||
|
line-height 14.4px
|
||||||
|
display inline-block
|
||||||
|
width 5px
|
||||||
|
margin -3px 3px 0
|
||||||
|
vertical-align middle
|
||||||
|
|
||||||
|
</style>
|
252
docs/src/.vuepress/theme/components/DropdownLink.vue
Normal file
@ -0,0 +1,252 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="dropdown-wrapper"
|
||||||
|
:class="{ open }"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="dropdown-title"
|
||||||
|
type="button"
|
||||||
|
:aria-label="dropdownAriaLabel"
|
||||||
|
@click="handleDropdown"
|
||||||
|
>
|
||||||
|
<span class="title">{{ item.text }}</span>
|
||||||
|
<span
|
||||||
|
class="arrow down"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="mobile-dropdown-title"
|
||||||
|
type="button"
|
||||||
|
:aria-label="dropdownAriaLabel"
|
||||||
|
@click="setOpen(!open)"
|
||||||
|
>
|
||||||
|
<span class="title">{{ item.text }}</span>
|
||||||
|
<span
|
||||||
|
class="arrow"
|
||||||
|
:class="open ? 'down' : 'right'"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<DropdownTransition>
|
||||||
|
<ul
|
||||||
|
v-show="open"
|
||||||
|
class="nav-dropdown"
|
||||||
|
>
|
||||||
|
<li
|
||||||
|
v-for="(subItem, index) in item.items"
|
||||||
|
:key="subItem.link || index"
|
||||||
|
class="dropdown-item"
|
||||||
|
>
|
||||||
|
<h4 v-if="subItem.type === 'links'">
|
||||||
|
{{ subItem.text }}
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<ul
|
||||||
|
v-if="subItem.type === 'links'"
|
||||||
|
class="dropdown-subitem-wrapper"
|
||||||
|
>
|
||||||
|
<li
|
||||||
|
v-for="childSubItem in subItem.items"
|
||||||
|
:key="childSubItem.link"
|
||||||
|
class="dropdown-subitem"
|
||||||
|
>
|
||||||
|
<NavLink
|
||||||
|
:item="childSubItem"
|
||||||
|
@focusout="
|
||||||
|
isLastItemOfArray(childSubItem, subItem.items) &&
|
||||||
|
isLastItemOfArray(subItem, item.items) &&
|
||||||
|
setOpen(false)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<NavLink
|
||||||
|
v-else
|
||||||
|
:item="subItem"
|
||||||
|
@focusout="isLastItemOfArray(subItem, item.items) && setOpen(false)"
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</DropdownTransition>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import NavLink from '@theme/components/NavLink.vue'
|
||||||
|
import DropdownTransition from '@theme/components/DropdownTransition.vue'
|
||||||
|
import last from 'lodash/last'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'DropdownLink',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
NavLink,
|
||||||
|
DropdownTransition
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
item: {
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
open: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
dropdownAriaLabel () {
|
||||||
|
return this.item.ariaLabel || this.item.text
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
$route () {
|
||||||
|
this.open = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
setOpen (value) {
|
||||||
|
this.open = value
|
||||||
|
},
|
||||||
|
|
||||||
|
isLastItemOfArray (item, array) {
|
||||||
|
return last(array) === item
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open the dropdown when user tab and click from keyboard.
|
||||||
|
*
|
||||||
|
* Use event.detail to detect tab and click from keyboard. Ref: https://developer.mozilla.org/en-US/docs/Web/API/UIEvent/detail
|
||||||
|
* The Tab + Click is UIEvent > KeyboardEvent, so the detail is 0.
|
||||||
|
*/
|
||||||
|
handleDropdown () {
|
||||||
|
const isTriggerByTab = event.detail === 0
|
||||||
|
if (isTriggerByTab) this.setOpen(!this.open)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="stylus">
|
||||||
|
.dropdown-wrapper
|
||||||
|
cursor pointer
|
||||||
|
.dropdown-title
|
||||||
|
display block
|
||||||
|
font-size 0.9rem
|
||||||
|
font-family inherit
|
||||||
|
cursor inherit
|
||||||
|
padding inherit
|
||||||
|
line-height 1.4rem
|
||||||
|
background transparent
|
||||||
|
border none
|
||||||
|
font-weight 500
|
||||||
|
color $textColor
|
||||||
|
&:hover
|
||||||
|
border-color transparent
|
||||||
|
.arrow
|
||||||
|
vertical-align middle
|
||||||
|
margin-top -1px
|
||||||
|
margin-left 0.4rem
|
||||||
|
.mobile-dropdown-title
|
||||||
|
@extends .dropdown-title
|
||||||
|
display none
|
||||||
|
font-weight 600
|
||||||
|
font-size inherit
|
||||||
|
&:hover
|
||||||
|
color $accentColor
|
||||||
|
.nav-dropdown
|
||||||
|
.dropdown-item
|
||||||
|
color inherit
|
||||||
|
line-height 1.7rem
|
||||||
|
h4
|
||||||
|
margin 0.45rem 0 0
|
||||||
|
border-top 1px solid #eee
|
||||||
|
padding 1rem 1.5rem 0.45rem 1.25rem
|
||||||
|
.dropdown-subitem-wrapper
|
||||||
|
padding 0
|
||||||
|
list-style none
|
||||||
|
.dropdown-subitem
|
||||||
|
font-size 0.9em
|
||||||
|
a
|
||||||
|
display block
|
||||||
|
line-height 1.7rem
|
||||||
|
position relative
|
||||||
|
border-bottom none
|
||||||
|
font-weight 400
|
||||||
|
margin-bottom 0
|
||||||
|
padding 0 1.5rem 0 1.25rem
|
||||||
|
&:hover
|
||||||
|
color $accentColor
|
||||||
|
&.router-link-active
|
||||||
|
color $accentColor
|
||||||
|
&::after
|
||||||
|
content ""
|
||||||
|
width 0
|
||||||
|
height 0
|
||||||
|
border-left 5px solid $accentColor
|
||||||
|
border-top 3px solid transparent
|
||||||
|
border-bottom 3px solid transparent
|
||||||
|
position absolute
|
||||||
|
top calc(50% - 2px)
|
||||||
|
left 9px
|
||||||
|
&:first-child h4
|
||||||
|
margin-top 0
|
||||||
|
padding-top 0
|
||||||
|
border-top 0
|
||||||
|
|
||||||
|
@media (max-width: $MQMobile)
|
||||||
|
.dropdown-wrapper
|
||||||
|
&.open .dropdown-title
|
||||||
|
margin-bottom 0.5rem
|
||||||
|
.dropdown-title
|
||||||
|
display: none
|
||||||
|
.mobile-dropdown-title
|
||||||
|
display: block
|
||||||
|
.nav-dropdown
|
||||||
|
transition height .1s ease-out
|
||||||
|
overflow hidden
|
||||||
|
.dropdown-item
|
||||||
|
h4
|
||||||
|
border-top 0
|
||||||
|
margin-top 0
|
||||||
|
padding-top 0
|
||||||
|
h4, & > a
|
||||||
|
font-size 15px
|
||||||
|
line-height 2rem
|
||||||
|
.dropdown-subitem
|
||||||
|
font-size 14px
|
||||||
|
padding-left 1rem
|
||||||
|
|
||||||
|
@media (min-width: $MQMobile)
|
||||||
|
.dropdown-wrapper
|
||||||
|
height 1.8rem
|
||||||
|
&:hover .nav-dropdown,
|
||||||
|
&.open .nav-dropdown
|
||||||
|
// override the inline style.
|
||||||
|
display block !important
|
||||||
|
&.open:blur
|
||||||
|
display none
|
||||||
|
.nav-dropdown
|
||||||
|
display none
|
||||||
|
// Avoid height shaked by clicking
|
||||||
|
height auto !important
|
||||||
|
box-sizing border-box;
|
||||||
|
max-height calc(100vh - 2.7rem)
|
||||||
|
overflow-y auto
|
||||||
|
position absolute
|
||||||
|
top 100%
|
||||||
|
right 0
|
||||||
|
background-color #fff
|
||||||
|
padding 0.6rem 0
|
||||||
|
border 1px solid #ddd
|
||||||
|
border-bottom-color #ccc
|
||||||
|
text-align left
|
||||||
|
border-radius 0.25rem
|
||||||
|
white-space nowrap
|
||||||
|
margin 0
|
||||||
|
</style>
|
33
docs/src/.vuepress/theme/components/DropdownTransition.vue
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<template>
|
||||||
|
<transition
|
||||||
|
name="dropdown"
|
||||||
|
@enter="setHeight"
|
||||||
|
@after-enter="unsetHeight"
|
||||||
|
@before-leave="setHeight"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</transition>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'DropdownTransition',
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
setHeight (items) {
|
||||||
|
// explicitly set height so that it can be transitioned
|
||||||
|
items.style.height = items.scrollHeight + 'px'
|
||||||
|
},
|
||||||
|
|
||||||
|
unsetHeight (items) {
|
||||||
|
items.style.height = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="stylus">
|
||||||
|
.dropdown-enter, .dropdown-leave-to
|
||||||
|
height 0 !important
|
||||||
|
|
||||||
|
</style>
|
197
docs/src/.vuepress/theme/components/Home.vue
Normal file
@ -0,0 +1,197 @@
|
|||||||
|
<template>
|
||||||
|
<main
|
||||||
|
class="home"
|
||||||
|
:aria-labelledby="data.heroText !== null ? 'main-title' : null"
|
||||||
|
>
|
||||||
|
<header class="hero">
|
||||||
|
<h1 v-if="data.heroImage" id="main-title">
|
||||||
|
<img :src="$withBase(data.heroImage)" :alt="data.heroAlt || 'hero'" />
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<p v-if="data.tagline !== null" class="description">
|
||||||
|
{{ data.tagline || $description || "Welcome to your VuePress site" }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p v-if="data.actionText && data.actionLink" class="action">
|
||||||
|
<NavLink class="action-button" :item="actionLink" />
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div v-if="data.features && data.features.length" class="features">
|
||||||
|
<div
|
||||||
|
v-for="(feature, index) in data.features"
|
||||||
|
:key="index"
|
||||||
|
class="feature"
|
||||||
|
>
|
||||||
|
<h2>{{ feature.title }}</h2>
|
||||||
|
<p>{{ feature.details }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Content class="theme-default-content custom" />
|
||||||
|
|
||||||
|
<div v-if="data.footer" class="footer">
|
||||||
|
{{ data.footer }}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import NavLink from "@theme/components/NavLink.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "Home",
|
||||||
|
|
||||||
|
components: { NavLink },
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
data() {
|
||||||
|
return this.$page.frontmatter;
|
||||||
|
},
|
||||||
|
|
||||||
|
actionLink() {
|
||||||
|
return {
|
||||||
|
link: this.data.actionLink,
|
||||||
|
text: this.data.actionText,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="stylus">
|
||||||
|
.home {
|
||||||
|
padding: $navbarHeight 2rem 0;
|
||||||
|
max-width: $homePageWidth;
|
||||||
|
margin: 0px auto;
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
.hero {
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 280px;
|
||||||
|
display: block;
|
||||||
|
margin: 3rem auto 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, .description, .action {
|
||||||
|
margin: 1.8rem auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
max-width: 35rem;
|
||||||
|
font-size: 1.6rem;
|
||||||
|
line-height: 1.3;
|
||||||
|
color: lighten($textColor, 40%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
color: #fff;
|
||||||
|
background-color: $accentColor;
|
||||||
|
padding: 0.8rem 1.6rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background-color 0.1s ease;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-bottom: 1px solid darken($accentColor, 10%);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: lighten($accentColor, 10%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.features {
|
||||||
|
border-top: 1px solid $borderColor;
|
||||||
|
padding: 1.2rem 0;
|
||||||
|
margin-top: 2.5rem;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: flex-start;
|
||||||
|
align-content: stretch;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature {
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-basis: 30%;
|
||||||
|
max-width: 30%;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
font-weight: 500;
|
||||||
|
border-bottom: none;
|
||||||
|
padding-bottom: 0;
|
||||||
|
color: lighten($textColor, 10%);
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
color: lighten($textColor, 25%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
padding: 2.5rem;
|
||||||
|
border-top: 1px solid $borderColor;
|
||||||
|
text-align: center;
|
||||||
|
color: lighten($textColor, 25%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: $MQMobile) {
|
||||||
|
.home {
|
||||||
|
.features {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature {
|
||||||
|
max-width: 100%;
|
||||||
|
padding: 0 2.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: $MQMobileNarrow) {
|
||||||
|
.home {
|
||||||
|
padding-left: 1.5rem;
|
||||||
|
padding-right: 1.5rem;
|
||||||
|
|
||||||
|
.hero {
|
||||||
|
img {
|
||||||
|
max-height: 210px;
|
||||||
|
margin: 2rem auto 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, .description, .action {
|
||||||
|
margin: 1.2rem auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button {
|
||||||
|
font-size: 1rem;
|
||||||
|
padding: 0.6rem 1.2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature {
|
||||||
|
h2 {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
90
docs/src/.vuepress/theme/components/NavLink.vue
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
<template>
|
||||||
|
<RouterLink
|
||||||
|
v-if="isInternal"
|
||||||
|
class="nav-link"
|
||||||
|
:to="link"
|
||||||
|
:exact="exact"
|
||||||
|
@focusout.native="focusoutAction"
|
||||||
|
>
|
||||||
|
{{ item.text }}
|
||||||
|
</RouterLink>
|
||||||
|
<a
|
||||||
|
v-else
|
||||||
|
:href="link"
|
||||||
|
class="nav-link external"
|
||||||
|
:target="target"
|
||||||
|
:rel="rel"
|
||||||
|
@focusout="focusoutAction"
|
||||||
|
>
|
||||||
|
{{ item.text }}
|
||||||
|
<OutboundLink v-if="isBlankTarget" />
|
||||||
|
</a>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { isExternal, isMailto, isTel, ensureExt } from '../util'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'NavLink',
|
||||||
|
|
||||||
|
props: {
|
||||||
|
item: {
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
link () {
|
||||||
|
return ensureExt(this.item.link)
|
||||||
|
},
|
||||||
|
|
||||||
|
exact () {
|
||||||
|
if (this.$site.locales) {
|
||||||
|
return Object.keys(this.$site.locales).some(rootLink => rootLink === this.link)
|
||||||
|
}
|
||||||
|
return this.link === '/'
|
||||||
|
},
|
||||||
|
|
||||||
|
isNonHttpURI () {
|
||||||
|
return isMailto(this.link) || isTel(this.link)
|
||||||
|
},
|
||||||
|
|
||||||
|
isBlankTarget () {
|
||||||
|
return this.target === '_blank'
|
||||||
|
},
|
||||||
|
|
||||||
|
isInternal () {
|
||||||
|
return !isExternal(this.link) && !this.isBlankTarget
|
||||||
|
},
|
||||||
|
|
||||||
|
target () {
|
||||||
|
if (this.isNonHttpURI) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (this.item.target) {
|
||||||
|
return this.item.target
|
||||||
|
}
|
||||||
|
return isExternal(this.link) ? '_blank' : ''
|
||||||
|
},
|
||||||
|
|
||||||
|
rel () {
|
||||||
|
if (this.isNonHttpURI) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (this.item.rel === false) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (this.item.rel) {
|
||||||
|
return this.item.rel
|
||||||
|
}
|
||||||
|
return this.isBlankTarget ? 'noopener noreferrer' : null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
focusoutAction () {
|
||||||
|
this.$emit('focusout')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
156
docs/src/.vuepress/theme/components/NavLinks.vue
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
<template>
|
||||||
|
<nav
|
||||||
|
v-if="userLinks.length || repoLink"
|
||||||
|
class="nav-links"
|
||||||
|
>
|
||||||
|
<!-- user links -->
|
||||||
|
<div
|
||||||
|
v-for="item in userLinks"
|
||||||
|
:key="item.link"
|
||||||
|
class="nav-item"
|
||||||
|
>
|
||||||
|
<DropdownLink
|
||||||
|
v-if="item.type === 'links'"
|
||||||
|
:item="item"
|
||||||
|
/>
|
||||||
|
<NavLink
|
||||||
|
v-else
|
||||||
|
:item="item"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- repo link -->
|
||||||
|
<a
|
||||||
|
v-if="repoLink"
|
||||||
|
:href="repoLink"
|
||||||
|
class="repo-link"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
{{ repoLabel }}
|
||||||
|
<OutboundLink />
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import DropdownLink from '@theme/components/DropdownLink.vue'
|
||||||
|
import { resolveNavLinkItem } from '../util'
|
||||||
|
import NavLink from '@theme/components/NavLink.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'NavLinks',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
NavLink,
|
||||||
|
DropdownLink
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
userNav () {
|
||||||
|
return this.$themeLocaleConfig.nav || this.$site.themeConfig.nav || []
|
||||||
|
},
|
||||||
|
|
||||||
|
nav () {
|
||||||
|
const { locales } = this.$site
|
||||||
|
if (locales && Object.keys(locales).length > 1) {
|
||||||
|
const currentLink = this.$page.path
|
||||||
|
const routes = this.$router.options.routes
|
||||||
|
const themeLocales = this.$site.themeConfig.locales || {}
|
||||||
|
const languageDropdown = {
|
||||||
|
text: this.$themeLocaleConfig.selectText || 'Languages',
|
||||||
|
ariaLabel: this.$themeLocaleConfig.ariaLabel || 'Select language',
|
||||||
|
items: Object.keys(locales).map(path => {
|
||||||
|
const locale = locales[path]
|
||||||
|
const text = themeLocales[path] && themeLocales[path].label || locale.lang
|
||||||
|
let link
|
||||||
|
// Stay on the current page
|
||||||
|
if (locale.lang === this.$lang) {
|
||||||
|
link = currentLink
|
||||||
|
} else {
|
||||||
|
// Try to stay on the same page
|
||||||
|
link = currentLink.replace(this.$localeConfig.path, path)
|
||||||
|
// fallback to homepage
|
||||||
|
if (!routes.some(route => route.path === link)) {
|
||||||
|
link = path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { text, link }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return [...this.userNav, languageDropdown]
|
||||||
|
}
|
||||||
|
return this.userNav
|
||||||
|
},
|
||||||
|
|
||||||
|
userLinks () {
|
||||||
|
return (this.nav || []).map(link => {
|
||||||
|
return Object.assign(resolveNavLinkItem(link), {
|
||||||
|
items: (link.items || []).map(resolveNavLinkItem)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
repoLink () {
|
||||||
|
const { repo } = this.$site.themeConfig
|
||||||
|
if (repo) {
|
||||||
|
return /^https?:/.test(repo)
|
||||||
|
? repo
|
||||||
|
: `https://github.com/${repo}`
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
|
||||||
|
repoLabel () {
|
||||||
|
if (!this.repoLink) return
|
||||||
|
if (this.$site.themeConfig.repoLabel) {
|
||||||
|
return this.$site.themeConfig.repoLabel
|
||||||
|
}
|
||||||
|
|
||||||
|
const repoHost = this.repoLink.match(/^https?:\/\/[^/]+/)[0]
|
||||||
|
const platforms = ['GitHub', 'GitLab', 'Bitbucket']
|
||||||
|
for (let i = 0; i < platforms.length; i++) {
|
||||||
|
const platform = platforms[i]
|
||||||
|
if (new RegExp(platform, 'i').test(repoHost)) {
|
||||||
|
return platform
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Source'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="stylus">
|
||||||
|
.nav-links
|
||||||
|
display inline-block
|
||||||
|
a
|
||||||
|
line-height 1.4rem
|
||||||
|
color inherit
|
||||||
|
&:hover, &.router-link-active
|
||||||
|
color $accentColor
|
||||||
|
.nav-item
|
||||||
|
position relative
|
||||||
|
display inline-block
|
||||||
|
margin-left 1.5rem
|
||||||
|
line-height 2rem
|
||||||
|
&:first-child
|
||||||
|
margin-left 0
|
||||||
|
.repo-link
|
||||||
|
margin-left 1.5rem
|
||||||
|
|
||||||
|
@media (max-width: $MQMobile)
|
||||||
|
.nav-links
|
||||||
|
.nav-item, .repo-link
|
||||||
|
margin-left 0
|
||||||
|
|
||||||
|
@media (min-width: $MQMobile)
|
||||||
|
.nav-links a
|
||||||
|
&:hover, &.router-link-active
|
||||||
|
color $textColor
|
||||||
|
.nav-item > a:not(.external)
|
||||||
|
&:hover, &.router-link-active
|
||||||
|
margin-bottom -2px
|
||||||
|
border-bottom 2px solid lighten($accentColor, 8%)
|
||||||
|
</style>
|
162
docs/src/.vuepress/theme/components/Navbar.vue
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
<template>
|
||||||
|
<header class="navbar">
|
||||||
|
<SidebarButton @toggle-sidebar="$emit('toggle-sidebar')" />
|
||||||
|
|
||||||
|
<RouterLink :to="$localePath" class="home-link">
|
||||||
|
<img
|
||||||
|
v-if="$site.themeConfig.logo"
|
||||||
|
class="logo"
|
||||||
|
:src="$withBase($site.themeConfig.logo)"
|
||||||
|
:alt="$siteTitle"
|
||||||
|
/>
|
||||||
|
</RouterLink>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="links"
|
||||||
|
:style="
|
||||||
|
linksWrapMaxWidth
|
||||||
|
? {
|
||||||
|
'max-width': linksWrapMaxWidth + 'px',
|
||||||
|
}
|
||||||
|
: {}
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<AlgoliaSearchBox v-if="isAlgoliaSearch" :options="algolia" />
|
||||||
|
<SearchBox
|
||||||
|
v-else-if="
|
||||||
|
$site.themeConfig.search !== false &&
|
||||||
|
$page.frontmatter.search !== false
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<NavLinks class="can-hide" />
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import AlgoliaSearchBox from "@AlgoliaSearchBox";
|
||||||
|
import SearchBox from "@SearchBox";
|
||||||
|
import SidebarButton from "@theme/components/SidebarButton.vue";
|
||||||
|
import NavLinks from "@theme/components/NavLinks.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "Navbar",
|
||||||
|
|
||||||
|
components: {
|
||||||
|
SidebarButton,
|
||||||
|
NavLinks,
|
||||||
|
SearchBox,
|
||||||
|
AlgoliaSearchBox,
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
linksWrapMaxWidth: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
algolia() {
|
||||||
|
return (
|
||||||
|
this.$themeLocaleConfig.algolia || this.$site.themeConfig.algolia || {}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
isAlgoliaSearch() {
|
||||||
|
return this.algolia && this.algolia.apiKey && this.algolia.indexName;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
const MOBILE_DESKTOP_BREAKPOINT = 719; // refer to config.styl
|
||||||
|
const NAVBAR_VERTICAL_PADDING =
|
||||||
|
parseInt(css(this.$el, "paddingLeft")) +
|
||||||
|
parseInt(css(this.$el, "paddingRight"));
|
||||||
|
const handleLinksWrapWidth = () => {
|
||||||
|
if (document.documentElement.clientWidth < MOBILE_DESKTOP_BREAKPOINT) {
|
||||||
|
this.linksWrapMaxWidth = null;
|
||||||
|
} else {
|
||||||
|
this.linksWrapMaxWidth =
|
||||||
|
this.$el.offsetWidth -
|
||||||
|
NAVBAR_VERTICAL_PADDING -
|
||||||
|
((this.$refs.siteName && this.$refs.siteName.offsetWidth) || 0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
handleLinksWrapWidth();
|
||||||
|
window.addEventListener("resize", handleLinksWrapWidth, false);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function css(el, property) {
|
||||||
|
// NOTE: Known bug, will return 'auto' if style value is 'auto'
|
||||||
|
const win = el.ownerDocument.defaultView;
|
||||||
|
// null means not to return pseudo styles
|
||||||
|
return win.getComputedStyle(el, null)[property];
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="stylus">
|
||||||
|
$navbar-vertical-padding = 0.7rem;
|
||||||
|
$navbar-horizontal-padding = 1.5rem;
|
||||||
|
|
||||||
|
.navbar {
|
||||||
|
padding: $navbar-vertical-padding $navbar-horizontal-padding;
|
||||||
|
line-height: $navbarHeight - 1.4rem;
|
||||||
|
|
||||||
|
a, span, img {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
height: $navbarHeight - 1.4rem;
|
||||||
|
min-width: $navbarHeight - 1.4rem;
|
||||||
|
margin-right: 0.8rem;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-name {
|
||||||
|
font-size: 1.3rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: $textColor;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.links {
|
||||||
|
padding-left: 1.5rem;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background-color: white;
|
||||||
|
white-space: nowrap;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
position: absolute;
|
||||||
|
right: $navbar-horizontal-padding;
|
||||||
|
top: $navbar-vertical-padding;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
.search-box {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: $MQMobile) {
|
||||||
|
.navbar {
|
||||||
|
padding-left: 4rem;
|
||||||
|
|
||||||
|
.can-hide {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.links {
|
||||||
|
padding-left: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-name {
|
||||||
|
width: calc(100vw - 9.4rem);
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
31
docs/src/.vuepress/theme/components/Page.vue
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<template>
|
||||||
|
<main class="page">
|
||||||
|
<slot name="top" />
|
||||||
|
|
||||||
|
<Content class="theme-default-content" />
|
||||||
|
<PageEdit />
|
||||||
|
|
||||||
|
<PageNav v-bind="{ sidebarItems }" />
|
||||||
|
|
||||||
|
<slot name="bottom" />
|
||||||
|
</main>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import PageEdit from '@theme/components/PageEdit.vue'
|
||||||
|
import PageNav from '@theme/components/PageNav.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: { PageEdit, PageNav },
|
||||||
|
props: ['sidebarItems']
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="stylus">
|
||||||
|
@require '../styles/wrapper.styl'
|
||||||
|
|
||||||
|
.page
|
||||||
|
padding-bottom 2rem
|
||||||
|
display block
|
||||||
|
|
||||||
|
</style>
|
155
docs/src/.vuepress/theme/components/PageEdit.vue
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
<template>
|
||||||
|
<footer class="page-edit">
|
||||||
|
<div
|
||||||
|
v-if="editLink"
|
||||||
|
class="edit-link"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
:href="editLink"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>{{ editLinkText }}</a>
|
||||||
|
<OutboundLink />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="lastUpdated"
|
||||||
|
class="last-updated"
|
||||||
|
>
|
||||||
|
<span class="prefix">{{ lastUpdatedText }}:</span>
|
||||||
|
<span class="time">{{ lastUpdated }}</span>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import isNil from 'lodash/isNil'
|
||||||
|
import { endingSlashRE, outboundRE } from '../util'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'PageEdit',
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
lastUpdated () {
|
||||||
|
return this.$page.lastUpdated
|
||||||
|
},
|
||||||
|
|
||||||
|
lastUpdatedText () {
|
||||||
|
if (typeof this.$themeLocaleConfig.lastUpdated === 'string') {
|
||||||
|
return this.$themeLocaleConfig.lastUpdated
|
||||||
|
}
|
||||||
|
if (typeof this.$site.themeConfig.lastUpdated === 'string') {
|
||||||
|
return this.$site.themeConfig.lastUpdated
|
||||||
|
}
|
||||||
|
return 'Last Updated'
|
||||||
|
},
|
||||||
|
|
||||||
|
editLink () {
|
||||||
|
const showEditLink = isNil(this.$page.frontmatter.editLink)
|
||||||
|
? this.$site.themeConfig.editLinks
|
||||||
|
: this.$page.frontmatter.editLink
|
||||||
|
|
||||||
|
const {
|
||||||
|
repo,
|
||||||
|
docsDir = '',
|
||||||
|
docsBranch = 'master',
|
||||||
|
docsRepo = repo
|
||||||
|
} = this.$site.themeConfig
|
||||||
|
|
||||||
|
if (showEditLink && docsRepo && this.$page.relativePath) {
|
||||||
|
return this.createEditLink(
|
||||||
|
repo,
|
||||||
|
docsRepo,
|
||||||
|
docsDir,
|
||||||
|
docsBranch,
|
||||||
|
this.$page.relativePath
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
|
||||||
|
editLinkText () {
|
||||||
|
return (
|
||||||
|
this.$themeLocaleConfig.editLinkText
|
||||||
|
|| this.$site.themeConfig.editLinkText
|
||||||
|
|| `Edit this page`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
createEditLink (repo, docsRepo, docsDir, docsBranch, path) {
|
||||||
|
const bitbucket = /bitbucket.org/
|
||||||
|
if (bitbucket.test(docsRepo)) {
|
||||||
|
const base = docsRepo
|
||||||
|
return (
|
||||||
|
base.replace(endingSlashRE, '')
|
||||||
|
+ `/src`
|
||||||
|
+ `/${docsBranch}/`
|
||||||
|
+ (docsDir ? docsDir.replace(endingSlashRE, '') + '/' : '')
|
||||||
|
+ path
|
||||||
|
+ `?mode=edit&spa=0&at=${docsBranch}&fileviewer=file-view-default`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const gitlab = /gitlab.com/
|
||||||
|
if (gitlab.test(docsRepo)) {
|
||||||
|
const base = docsRepo
|
||||||
|
return (
|
||||||
|
base.replace(endingSlashRE, '')
|
||||||
|
+ `/-/edit`
|
||||||
|
+ `/${docsBranch}/`
|
||||||
|
+ (docsDir ? docsDir.replace(endingSlashRE, '') + '/' : '')
|
||||||
|
+ path
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const base = outboundRE.test(docsRepo)
|
||||||
|
? docsRepo
|
||||||
|
: `https://github.com/${docsRepo}`
|
||||||
|
return (
|
||||||
|
base.replace(endingSlashRE, '')
|
||||||
|
+ '/edit'
|
||||||
|
+ `/${docsBranch}/`
|
||||||
|
+ (docsDir ? docsDir.replace(endingSlashRE, '') + '/' : '')
|
||||||
|
+ path
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="stylus">
|
||||||
|
@require '../styles/wrapper.styl'
|
||||||
|
|
||||||
|
.page-edit
|
||||||
|
@extend $wrapper
|
||||||
|
padding-top 1rem
|
||||||
|
padding-bottom 1rem
|
||||||
|
overflow auto
|
||||||
|
|
||||||
|
.edit-link
|
||||||
|
display inline-block
|
||||||
|
a
|
||||||
|
color lighten($textColor, 25%)
|
||||||
|
margin-right 0.25rem
|
||||||
|
.last-updated
|
||||||
|
float right
|
||||||
|
font-size 0.9em
|
||||||
|
.prefix
|
||||||
|
font-weight 500
|
||||||
|
color lighten($textColor, 25%)
|
||||||
|
.time
|
||||||
|
font-weight 400
|
||||||
|
color #767676
|
||||||
|
|
||||||
|
@media (max-width: $MQMobile)
|
||||||
|
.page-edit
|
||||||
|
.edit-link
|
||||||
|
margin-bottom 0.5rem
|
||||||
|
.last-updated
|
||||||
|
font-size 0.8em
|
||||||
|
float none
|
||||||
|
text-align left
|
||||||
|
|
||||||
|
</style>
|
163
docs/src/.vuepress/theme/components/PageNav.vue
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="prev || next"
|
||||||
|
class="page-nav"
|
||||||
|
>
|
||||||
|
<p class="inner">
|
||||||
|
<span
|
||||||
|
v-if="prev"
|
||||||
|
class="prev"
|
||||||
|
>
|
||||||
|
←
|
||||||
|
<a
|
||||||
|
v-if="prev.type === 'external'"
|
||||||
|
class="prev"
|
||||||
|
:href="prev.path"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
{{ prev.title || prev.path }}
|
||||||
|
|
||||||
|
<OutboundLink />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<RouterLink
|
||||||
|
v-else
|
||||||
|
class="prev"
|
||||||
|
:to="prev.path"
|
||||||
|
>
|
||||||
|
{{ prev.title || prev.path }}
|
||||||
|
</RouterLink>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span
|
||||||
|
v-if="next"
|
||||||
|
class="next"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
v-if="next.type === 'external'"
|
||||||
|
:href="next.path"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
{{ next.title || next.path }}
|
||||||
|
|
||||||
|
<OutboundLink />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<RouterLink
|
||||||
|
v-else
|
||||||
|
:to="next.path"
|
||||||
|
>
|
||||||
|
{{ next.title || next.path }}
|
||||||
|
</RouterLink>
|
||||||
|
→
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { resolvePage } from '../util'
|
||||||
|
import isString from 'lodash/isString'
|
||||||
|
import isNil from 'lodash/isNil'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'PageNav',
|
||||||
|
|
||||||
|
props: ['sidebarItems'],
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
prev () {
|
||||||
|
return resolvePageLink(LINK_TYPES.PREV, this)
|
||||||
|
},
|
||||||
|
|
||||||
|
next () {
|
||||||
|
return resolvePageLink(LINK_TYPES.NEXT, this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolvePrev (page, items) {
|
||||||
|
return find(page, items, -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveNext (page, items) {
|
||||||
|
return find(page, items, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const LINK_TYPES = {
|
||||||
|
NEXT: {
|
||||||
|
resolveLink: resolveNext,
|
||||||
|
getThemeLinkConfig: ({ nextLinks }) => nextLinks,
|
||||||
|
getPageLinkConfig: ({ frontmatter }) => frontmatter.next
|
||||||
|
},
|
||||||
|
PREV: {
|
||||||
|
resolveLink: resolvePrev,
|
||||||
|
getThemeLinkConfig: ({ prevLinks }) => prevLinks,
|
||||||
|
getPageLinkConfig: ({ frontmatter }) => frontmatter.prev
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolvePageLink (
|
||||||
|
linkType,
|
||||||
|
{ $themeConfig, $page, $route, $site, sidebarItems }
|
||||||
|
) {
|
||||||
|
const { resolveLink, getThemeLinkConfig, getPageLinkConfig } = linkType
|
||||||
|
|
||||||
|
// Get link config from theme
|
||||||
|
const themeLinkConfig = getThemeLinkConfig($themeConfig)
|
||||||
|
|
||||||
|
// Get link config from current page
|
||||||
|
const pageLinkConfig = getPageLinkConfig($page)
|
||||||
|
|
||||||
|
// Page link config will overwrite global theme link config if defined
|
||||||
|
const link = isNil(pageLinkConfig) ? themeLinkConfig : pageLinkConfig
|
||||||
|
|
||||||
|
if (link === false) {
|
||||||
|
return
|
||||||
|
} else if (isString(link)) {
|
||||||
|
return resolvePage($site.pages, link, $route.path)
|
||||||
|
} else {
|
||||||
|
return resolveLink($page, sidebarItems)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function find (page, items, offset) {
|
||||||
|
const res = []
|
||||||
|
flatten(items, res)
|
||||||
|
for (let i = 0; i < res.length; i++) {
|
||||||
|
const cur = res[i]
|
||||||
|
if (cur.type === 'page' && cur.path === decodeURIComponent(page.path)) {
|
||||||
|
return res[i + offset]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function flatten (items, res) {
|
||||||
|
for (let i = 0, l = items.length; i < l; i++) {
|
||||||
|
if (items[i].type === 'group') {
|
||||||
|
flatten(items[i].children || [], res)
|
||||||
|
} else {
|
||||||
|
res.push(items[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="stylus">
|
||||||
|
@require '../styles/wrapper.styl'
|
||||||
|
|
||||||
|
.page-nav
|
||||||
|
@extend $wrapper
|
||||||
|
padding-top 1rem
|
||||||
|
padding-bottom 0
|
||||||
|
.inner
|
||||||
|
min-height 2rem
|
||||||
|
margin-top 0
|
||||||
|
border-top 1px solid $borderColor
|
||||||
|
padding-top 1rem
|
||||||
|
overflow auto // clear float
|
||||||
|
.next
|
||||||
|
float right
|
||||||
|
</style>
|
64
docs/src/.vuepress/theme/components/Sidebar.vue
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
<template>
|
||||||
|
<aside class="sidebar">
|
||||||
|
<NavLinks />
|
||||||
|
|
||||||
|
<slot name="top" />
|
||||||
|
|
||||||
|
<SidebarLinks
|
||||||
|
:depth="0"
|
||||||
|
:items="items"
|
||||||
|
/>
|
||||||
|
<slot name="bottom" />
|
||||||
|
</aside>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import SidebarLinks from '@theme/components/SidebarLinks.vue'
|
||||||
|
import NavLinks from '@theme/components/NavLinks.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Sidebar',
|
||||||
|
|
||||||
|
components: { SidebarLinks, NavLinks },
|
||||||
|
|
||||||
|
props: ['items']
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="stylus">
|
||||||
|
.sidebar
|
||||||
|
ul
|
||||||
|
padding 0
|
||||||
|
margin 0
|
||||||
|
list-style-type none
|
||||||
|
a
|
||||||
|
display inline-block
|
||||||
|
.nav-links
|
||||||
|
display none
|
||||||
|
border-bottom 1px solid $borderColor
|
||||||
|
padding 0.5rem 0 0.75rem 0
|
||||||
|
a
|
||||||
|
font-weight 600
|
||||||
|
.nav-item, .repo-link
|
||||||
|
display block
|
||||||
|
line-height 1.25rem
|
||||||
|
font-size 1.1em
|
||||||
|
padding 0.5rem 0 0.5rem 1.5rem
|
||||||
|
& > .sidebar-links
|
||||||
|
padding 1.5rem 0
|
||||||
|
& > li > a.sidebar-link
|
||||||
|
font-size 1.1em
|
||||||
|
line-height 1.7
|
||||||
|
font-weight bold
|
||||||
|
& > li:not(:first-child)
|
||||||
|
margin-top .75rem
|
||||||
|
|
||||||
|
@media (max-width: $MQMobile)
|
||||||
|
.sidebar
|
||||||
|
.nav-links
|
||||||
|
display block
|
||||||
|
.dropdown-wrapper .nav-dropdown .dropdown-item a.router-link-active::after
|
||||||
|
top calc(1rem - 2px)
|
||||||
|
& > .sidebar-links
|
||||||
|
padding 1rem 0
|
||||||
|
</style>
|
40
docs/src/.vuepress/theme/components/SidebarButton.vue
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="sidebar-button"
|
||||||
|
@click="$emit('toggle-sidebar')"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="icon"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
aria-hidden="true"
|
||||||
|
role="img"
|
||||||
|
viewBox="0 0 448 512"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M436 124H12c-6.627 0-12-5.373-12-12V80c0-6.627 5.373-12 12-12h424c6.627 0 12 5.373 12 12v32c0 6.627-5.373 12-12 12zm0 160H12c-6.627 0-12-5.373-12-12v-32c0-6.627 5.373-12 12-12h424c6.627 0 12 5.373 12 12v32c0 6.627-5.373 12-12 12zm0 160H12c-6.627 0-12-5.373-12-12v-32c0-6.627 5.373-12 12-12h424c6.627 0 12 5.373 12 12v32c0 6.627-5.373 12-12 12z"
|
||||||
|
class=""
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="stylus">
|
||||||
|
.sidebar-button
|
||||||
|
cursor pointer
|
||||||
|
display none
|
||||||
|
width 1.25rem
|
||||||
|
height 1.25rem
|
||||||
|
position absolute
|
||||||
|
padding 0.6rem
|
||||||
|
top 0.6rem
|
||||||
|
left 1rem
|
||||||
|
.icon
|
||||||
|
display block
|
||||||
|
width 1.25rem
|
||||||
|
height 1.25rem
|
||||||
|
|
||||||
|
@media (max-width: $MQMobile)
|
||||||
|
.sidebar-button
|
||||||
|
display block
|
||||||
|
</style>
|
141
docs/src/.vuepress/theme/components/SidebarGroup.vue
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
<template>
|
||||||
|
<section
|
||||||
|
class="sidebar-group"
|
||||||
|
:class="[
|
||||||
|
{
|
||||||
|
collapsable,
|
||||||
|
'is-sub-group': depth !== 0
|
||||||
|
},
|
||||||
|
`depth-${depth}`
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<RouterLink
|
||||||
|
v-if="item.path"
|
||||||
|
class="sidebar-heading clickable"
|
||||||
|
:class="{
|
||||||
|
open,
|
||||||
|
'active': isActive($route, item.path)
|
||||||
|
}"
|
||||||
|
:to="item.path"
|
||||||
|
@click.native="$emit('toggle')"
|
||||||
|
>
|
||||||
|
<span>{{ item.title }}</span>
|
||||||
|
<span
|
||||||
|
v-if="collapsable"
|
||||||
|
class="arrow"
|
||||||
|
:class="open ? 'down' : 'right'"
|
||||||
|
/>
|
||||||
|
</RouterLink>
|
||||||
|
|
||||||
|
<p
|
||||||
|
v-else
|
||||||
|
class="sidebar-heading"
|
||||||
|
:class="{ open }"
|
||||||
|
@click="$emit('toggle')"
|
||||||
|
>
|
||||||
|
<span>{{ item.title }}</span>
|
||||||
|
<span
|
||||||
|
v-if="collapsable"
|
||||||
|
class="arrow"
|
||||||
|
:class="open ? 'down' : 'right'"
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<DropdownTransition>
|
||||||
|
<SidebarLinks
|
||||||
|
v-if="open || !collapsable"
|
||||||
|
class="sidebar-group-items"
|
||||||
|
:items="item.children"
|
||||||
|
:sidebar-depth="item.sidebarDepth"
|
||||||
|
:initial-open-group-index="item.initialOpenGroupIndex"
|
||||||
|
:depth="depth + 1"
|
||||||
|
/>
|
||||||
|
</DropdownTransition>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { isActive } from '../util'
|
||||||
|
import DropdownTransition from '@theme/components/DropdownTransition.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'SidebarGroup',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
DropdownTransition
|
||||||
|
},
|
||||||
|
|
||||||
|
props: [
|
||||||
|
'item',
|
||||||
|
'open',
|
||||||
|
'collapsable',
|
||||||
|
'depth'
|
||||||
|
],
|
||||||
|
|
||||||
|
// ref: https://vuejs.org/v2/guide/components-edge-cases.html#Circular-References-Between-Components
|
||||||
|
beforeCreate () {
|
||||||
|
this.$options.components.SidebarLinks = require('@theme/components/SidebarLinks.vue').default
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: { isActive }
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="stylus">
|
||||||
|
.sidebar-group
|
||||||
|
.sidebar-group
|
||||||
|
padding-left 0.5em
|
||||||
|
&:not(.collapsable)
|
||||||
|
.sidebar-heading:not(.clickable)
|
||||||
|
cursor auto
|
||||||
|
color inherit
|
||||||
|
// refine styles of nested sidebar groups
|
||||||
|
&.is-sub-group
|
||||||
|
padding-left 0
|
||||||
|
& > .sidebar-heading
|
||||||
|
font-size 0.95em
|
||||||
|
line-height 1.4
|
||||||
|
font-weight normal
|
||||||
|
padding-left 2rem
|
||||||
|
&:not(.clickable)
|
||||||
|
opacity 0.5
|
||||||
|
& > .sidebar-group-items
|
||||||
|
padding-left 1rem
|
||||||
|
& > li > .sidebar-link
|
||||||
|
font-size: 0.95em;
|
||||||
|
border-left none
|
||||||
|
&.depth-2
|
||||||
|
& > .sidebar-heading
|
||||||
|
border-left none
|
||||||
|
|
||||||
|
.sidebar-heading
|
||||||
|
color $textColor
|
||||||
|
transition color .15s ease
|
||||||
|
cursor pointer
|
||||||
|
font-size 1.1em
|
||||||
|
font-weight bold
|
||||||
|
// text-transform uppercase
|
||||||
|
padding 0.35rem 1.5rem 0.35rem 1.25rem
|
||||||
|
width 100%
|
||||||
|
box-sizing border-box
|
||||||
|
margin 0
|
||||||
|
border-left 0.25rem solid transparent
|
||||||
|
&.open, &:hover
|
||||||
|
color inherit
|
||||||
|
.arrow
|
||||||
|
position relative
|
||||||
|
top -0.12em
|
||||||
|
left 0.5em
|
||||||
|
&.clickable
|
||||||
|
&.active
|
||||||
|
font-weight 600
|
||||||
|
color $accentColor
|
||||||
|
border-left-color $accentColor
|
||||||
|
&:hover
|
||||||
|
color $accentColor
|
||||||
|
|
||||||
|
.sidebar-group-items
|
||||||
|
transition height .1s ease-out
|
||||||
|
font-size 0.95em
|
||||||
|
overflow hidden
|
||||||
|
</style>
|
133
docs/src/.vuepress/theme/components/SidebarLink.vue
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
<script>
|
||||||
|
import { isActive, hashRE, groupHeaders } from '../util'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
functional: true,
|
||||||
|
|
||||||
|
props: ['item', 'sidebarDepth'],
|
||||||
|
|
||||||
|
render (h,
|
||||||
|
{
|
||||||
|
parent: {
|
||||||
|
$page,
|
||||||
|
$site,
|
||||||
|
$route,
|
||||||
|
$themeConfig,
|
||||||
|
$themeLocaleConfig
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
item,
|
||||||
|
sidebarDepth
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
// use custom active class matching logic
|
||||||
|
// due to edge case of paths ending with / + hash
|
||||||
|
const selfActive = isActive($route, item.path)
|
||||||
|
// for sidebar: auto pages, a hash link should be active if one of its child
|
||||||
|
// matches
|
||||||
|
const active = item.type === 'auto'
|
||||||
|
? selfActive || item.children.some(c => isActive($route, item.basePath + '#' + c.slug))
|
||||||
|
: selfActive
|
||||||
|
const link = item.type === 'external'
|
||||||
|
? renderExternal(h, item.path, item.title || item.path)
|
||||||
|
: renderLink(h, item.path, item.title || item.path, active)
|
||||||
|
|
||||||
|
const maxDepth = [
|
||||||
|
$page.frontmatter.sidebarDepth,
|
||||||
|
sidebarDepth,
|
||||||
|
$themeLocaleConfig.sidebarDepth,
|
||||||
|
$themeConfig.sidebarDepth,
|
||||||
|
1
|
||||||
|
].find(depth => depth !== undefined)
|
||||||
|
|
||||||
|
const displayAllHeaders = $themeLocaleConfig.displayAllHeaders
|
||||||
|
|| $themeConfig.displayAllHeaders
|
||||||
|
|
||||||
|
if (item.type === 'auto') {
|
||||||
|
return [link, renderChildren(h, item.children, item.basePath, $route, maxDepth)]
|
||||||
|
} else if ((active || displayAllHeaders) && item.headers && !hashRE.test(item.path)) {
|
||||||
|
const children = groupHeaders(item.headers)
|
||||||
|
return [link, renderChildren(h, children, item.path, $route, maxDepth)]
|
||||||
|
} else {
|
||||||
|
return link
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderLink (h, to, text, active, level) {
|
||||||
|
const component = {
|
||||||
|
props: {
|
||||||
|
to,
|
||||||
|
activeClass: '',
|
||||||
|
exactActiveClass: ''
|
||||||
|
},
|
||||||
|
class: {
|
||||||
|
active,
|
||||||
|
'sidebar-link': true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (level > 2) {
|
||||||
|
component.style = {
|
||||||
|
'padding-left': level + 'rem'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return h('RouterLink', component, text)
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderChildren (h, children, path, route, maxDepth, depth = 1) {
|
||||||
|
if (!children || depth > maxDepth) return null
|
||||||
|
return h('ul', { class: 'sidebar-sub-headers' }, children.map(c => {
|
||||||
|
const active = isActive(route, path + '#' + c.slug)
|
||||||
|
return h('li', { class: 'sidebar-sub-header' }, [
|
||||||
|
renderLink(h, path + '#' + c.slug, c.title, active, c.level - 1),
|
||||||
|
renderChildren(h, c.children, path, route, maxDepth, depth + 1)
|
||||||
|
])
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderExternal (h, to, text) {
|
||||||
|
return h('a', {
|
||||||
|
attrs: {
|
||||||
|
href: to,
|
||||||
|
target: '_blank',
|
||||||
|
rel: 'noopener noreferrer'
|
||||||
|
},
|
||||||
|
class: {
|
||||||
|
'sidebar-link': true
|
||||||
|
}
|
||||||
|
}, [text, h('OutboundLink')])
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="stylus">
|
||||||
|
.sidebar .sidebar-sub-headers
|
||||||
|
padding-left 1rem
|
||||||
|
font-size 0.95em
|
||||||
|
|
||||||
|
a.sidebar-link
|
||||||
|
font-size 1em
|
||||||
|
font-weight 400
|
||||||
|
display inline-block
|
||||||
|
color $textColor
|
||||||
|
border-left 0.25rem solid transparent
|
||||||
|
padding 0.35rem 1rem 0.35rem 1.25rem
|
||||||
|
line-height 1.4
|
||||||
|
width: 100%
|
||||||
|
box-sizing: border-box
|
||||||
|
&:hover
|
||||||
|
color $accentColor
|
||||||
|
&.active
|
||||||
|
font-weight 600
|
||||||
|
color $accentColor
|
||||||
|
border-left-color $accentColor
|
||||||
|
.sidebar-group &
|
||||||
|
padding-left 2rem
|
||||||
|
.sidebar-sub-headers &
|
||||||
|
padding-top 0.25rem
|
||||||
|
padding-bottom 0.25rem
|
||||||
|
border-left none
|
||||||
|
&.active
|
||||||
|
font-weight 500
|
||||||
|
</style>
|
103
docs/src/.vuepress/theme/components/SidebarLinks.vue
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
<template>
|
||||||
|
<ul
|
||||||
|
v-if="items.length"
|
||||||
|
class="sidebar-links"
|
||||||
|
>
|
||||||
|
<li
|
||||||
|
v-for="(item, i) in items"
|
||||||
|
:key="i"
|
||||||
|
>
|
||||||
|
<SidebarGroup
|
||||||
|
v-if="item.type === 'group'"
|
||||||
|
:item="item"
|
||||||
|
:open="i === openGroupIndex"
|
||||||
|
:collapsable="item.collapsable || item.collapsible"
|
||||||
|
:depth="depth"
|
||||||
|
@toggle="toggleGroup(i)"
|
||||||
|
/>
|
||||||
|
<SidebarLink
|
||||||
|
v-else
|
||||||
|
:sidebar-depth="sidebarDepth"
|
||||||
|
:item="item"
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import SidebarGroup from '@theme/components/SidebarGroup.vue'
|
||||||
|
import SidebarLink from '@theme/components/SidebarLink.vue'
|
||||||
|
import { isActive } from '../util'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'SidebarLinks',
|
||||||
|
|
||||||
|
components: { SidebarGroup, SidebarLink },
|
||||||
|
|
||||||
|
props: [
|
||||||
|
'items',
|
||||||
|
'depth', // depth of current sidebar links
|
||||||
|
'sidebarDepth', // depth of headers to be extracted
|
||||||
|
'initialOpenGroupIndex'
|
||||||
|
],
|
||||||
|
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
openGroupIndex: this.initialOpenGroupIndex || 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
'$route' () {
|
||||||
|
this.refreshIndex()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
created () {
|
||||||
|
this.refreshIndex()
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
refreshIndex () {
|
||||||
|
const index = resolveOpenGroupIndex(
|
||||||
|
this.$route,
|
||||||
|
this.items
|
||||||
|
)
|
||||||
|
if (index > -1) {
|
||||||
|
this.openGroupIndex = index
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleGroup (index) {
|
||||||
|
this.openGroupIndex = index === this.openGroupIndex ? -1 : index
|
||||||
|
},
|
||||||
|
|
||||||
|
isActive (page) {
|
||||||
|
return isActive(this.$route, page.regularPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveOpenGroupIndex (route, items) {
|
||||||
|
for (let i = 0; i < items.length; i++) {
|
||||||
|
const item = items[i]
|
||||||
|
if (descendantIsActive(route, item)) {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
function descendantIsActive (route, item) {
|
||||||
|
if (item.type === 'group') {
|
||||||
|
return item.children.some(child => {
|
||||||
|
if (child.type === 'group') {
|
||||||
|
return descendantIsActive(route, child)
|
||||||
|
} else {
|
||||||
|
return child.type === 'page' && isActive(route, child.path)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
</script>
|
44
docs/src/.vuepress/theme/global-components/Badge.vue
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
functional: true,
|
||||||
|
props: {
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
default: 'tip'
|
||||||
|
},
|
||||||
|
text: String,
|
||||||
|
vertical: {
|
||||||
|
type: String,
|
||||||
|
default: 'top'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
render (h, { props, slots }) {
|
||||||
|
return h('span', {
|
||||||
|
class: ['badge', props.type],
|
||||||
|
style: {
|
||||||
|
verticalAlign: props.vertical
|
||||||
|
}
|
||||||
|
}, props.text || slots().default)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="stylus" scoped>
|
||||||
|
.badge
|
||||||
|
display inline-block
|
||||||
|
font-size 14px
|
||||||
|
height 18px
|
||||||
|
line-height 18px
|
||||||
|
border-radius 3px
|
||||||
|
padding 0 6px
|
||||||
|
color white
|
||||||
|
background-color #42b983
|
||||||
|
&.tip, &.green
|
||||||
|
background-color $badgeTipColor
|
||||||
|
&.error
|
||||||
|
background-color $badgeErrorColor
|
||||||
|
&.warning, &.warn, &.yellow
|
||||||
|
background-color $badgeWarningColor
|
||||||
|
& + &
|
||||||
|
margin-left 5px
|
||||||
|
</style>
|
36
docs/src/.vuepress/theme/global-components/CodeBlock.vue
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="theme-code-block"
|
||||||
|
:class="{ 'theme-code-block__active': active }"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'CodeBlock',
|
||||||
|
props: {
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
active: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.theme-code-block {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.theme-code-block__active {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.theme-code-block > pre {
|
||||||
|
background-color: orange;
|
||||||
|
}
|
||||||
|
</style>
|
105
docs/src/.vuepress/theme/global-components/CodeGroup.vue
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
<template>
|
||||||
|
<div class="theme-code-group">
|
||||||
|
<div class="theme-code-group__nav">
|
||||||
|
<ul class="theme-code-group__ul">
|
||||||
|
<li
|
||||||
|
v-for="(tab, i) in codeTabs"
|
||||||
|
:key="tab.title"
|
||||||
|
class="theme-code-group__li"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="theme-code-group__nav-tab"
|
||||||
|
:class="{
|
||||||
|
'theme-code-group__nav-tab-active': i === activeCodeTabIndex,
|
||||||
|
}"
|
||||||
|
@click="changeCodeTab(i)"
|
||||||
|
>
|
||||||
|
{{ tab.title }}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<slot />
|
||||||
|
<pre
|
||||||
|
v-if="codeTabs.length < 1"
|
||||||
|
class="pre-blank"
|
||||||
|
>// Make sure to add code blocks to your code group</pre>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'CodeGroup',
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
codeTabs: [],
|
||||||
|
activeCodeTabIndex: -1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
activeCodeTabIndex (index) {
|
||||||
|
this.codeTabs.forEach(tab => {
|
||||||
|
tab.elm.classList.remove('theme-code-block__active')
|
||||||
|
})
|
||||||
|
this.codeTabs[index].elm.classList.add('theme-code-block__active')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
this.codeTabs = (this.$slots.default || []).filter(slot => Boolean(slot.componentOptions)).map((slot, index) => {
|
||||||
|
if (slot.componentOptions.propsData.active === '') {
|
||||||
|
this.activeCodeTabIndex = index
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: slot.componentOptions.propsData.title,
|
||||||
|
elm: slot.elm
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (this.activeCodeTabIndex === -1 && this.codeTabs.length > 0) {
|
||||||
|
this.activeCodeTabIndex = 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
changeCodeTab (index) {
|
||||||
|
this.activeCodeTabIndex = index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="stylus" scoped>
|
||||||
|
.theme-code-group {}
|
||||||
|
.theme-code-group__nav {
|
||||||
|
margin-bottom: -35px;
|
||||||
|
background-color: $codeBgColor;
|
||||||
|
padding-bottom: 22px;
|
||||||
|
border-top-left-radius: 6px;
|
||||||
|
border-top-right-radius: 6px;
|
||||||
|
padding-left: 10px;
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
.theme-code-group__ul {
|
||||||
|
margin: auto 0;
|
||||||
|
padding-left: 0;
|
||||||
|
display: inline-flex;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
.theme-code-group__li {}
|
||||||
|
.theme-code-group__nav-tab {
|
||||||
|
border: 0;
|
||||||
|
padding: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: transparent;
|
||||||
|
font-size: 0.85em;
|
||||||
|
line-height: 1.4;
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.theme-code-group__nav-tab-active {
|
||||||
|
border-bottom: #42b983 1px solid;
|
||||||
|
}
|
||||||
|
.pre-blank {
|
||||||
|
color: #42b983;
|
||||||
|
}
|
||||||
|
</style>
|
59
docs/src/.vuepress/theme/index.js
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
const path = require('path')
|
||||||
|
|
||||||
|
// Theme API.
|
||||||
|
module.exports = (options, ctx) => {
|
||||||
|
const { themeConfig, siteConfig } = ctx
|
||||||
|
|
||||||
|
// resolve algolia
|
||||||
|
const isAlgoliaSearch = (
|
||||||
|
themeConfig.algolia
|
||||||
|
|| Object
|
||||||
|
.keys(siteConfig.locales && themeConfig.locales || {})
|
||||||
|
.some(base => themeConfig.locales[base].algolia)
|
||||||
|
)
|
||||||
|
|
||||||
|
const enableSmoothScroll = themeConfig.smoothScroll === true
|
||||||
|
|
||||||
|
return {
|
||||||
|
alias () {
|
||||||
|
return {
|
||||||
|
'@AlgoliaSearchBox': isAlgoliaSearch
|
||||||
|
? path.resolve(__dirname, 'components/AlgoliaSearchBox.vue')
|
||||||
|
: path.resolve(__dirname, 'noopModule.js')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
plugins: [
|
||||||
|
['@vuepress/active-header-links', options.activeHeaderLinks],
|
||||||
|
'@vuepress/search',
|
||||||
|
'@vuepress/plugin-nprogress',
|
||||||
|
['container', {
|
||||||
|
type: 'tip',
|
||||||
|
defaultTitle: {
|
||||||
|
'/': 'TIP',
|
||||||
|
'/zh/': '提示'
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
['container', {
|
||||||
|
type: 'warning',
|
||||||
|
defaultTitle: {
|
||||||
|
'/': 'WARNING',
|
||||||
|
'/zh/': '注意'
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
['container', {
|
||||||
|
type: 'danger',
|
||||||
|
defaultTitle: {
|
||||||
|
'/': 'WARNING',
|
||||||
|
'/zh/': '警告'
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
['container', {
|
||||||
|
type: 'details',
|
||||||
|
before: info => `<details class="custom-block details">${info ? `<summary>${info}</summary>` : ''}\n`,
|
||||||
|
after: () => '</details>\n'
|
||||||
|
}],
|
||||||
|
['smooth-scroll', enableSmoothScroll]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
30
docs/src/.vuepress/theme/layouts/404.vue
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<template>
|
||||||
|
<div class="theme-container">
|
||||||
|
<div class="theme-default-content">
|
||||||
|
<h1>404</h1>
|
||||||
|
|
||||||
|
<blockquote>{{ getMsg() }}</blockquote>
|
||||||
|
|
||||||
|
<RouterLink to="/">
|
||||||
|
Take me home.
|
||||||
|
</RouterLink>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const msgs = [
|
||||||
|
`There's nothing here.`,
|
||||||
|
`How did we get here?`,
|
||||||
|
`That's a Four-Oh-Four.`,
|
||||||
|
`Looks like we've got some broken links.`
|
||||||
|
]
|
||||||
|
|
||||||
|
export default {
|
||||||
|
methods: {
|
||||||
|
getMsg () {
|
||||||
|
return msgs[Math.floor(Math.random() * msgs.length)]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
137
docs/src/.vuepress/theme/layouts/Layout.vue
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="theme-container"
|
||||||
|
:class="pageClasses"
|
||||||
|
@touchstart="onTouchStart"
|
||||||
|
@touchend="onTouchEnd"
|
||||||
|
>
|
||||||
|
<Navbar v-if="shouldShowNavbar" @toggle-sidebar="toggleSidebar" />
|
||||||
|
|
||||||
|
<div class="sidebar-mask" @click="toggleSidebar(false)" />
|
||||||
|
|
||||||
|
<Sidebar :items="sidebarItems" @toggle-sidebar="toggleSidebar">
|
||||||
|
<template #top>
|
||||||
|
<slot name="sidebar-top" />
|
||||||
|
</template>
|
||||||
|
<template #bottom>
|
||||||
|
<slot name="sidebar-bottom" />
|
||||||
|
</template>
|
||||||
|
</Sidebar>
|
||||||
|
|
||||||
|
<Home v-if="$page.frontmatter.home" />
|
||||||
|
|
||||||
|
<Page v-else :sidebar-items="sidebarItems">
|
||||||
|
<template #top>
|
||||||
|
<slot name="page-top" />
|
||||||
|
</template>
|
||||||
|
<template #bottom>
|
||||||
|
<slot name="page-bottom" />
|
||||||
|
</template>
|
||||||
|
</Page>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Home from "@theme/components/Home.vue";
|
||||||
|
import Navbar from "@theme/components/Navbar.vue";
|
||||||
|
import Page from "@theme/components/Page.vue";
|
||||||
|
import Sidebar from "@theme/components/Sidebar.vue";
|
||||||
|
import { resolveSidebarItems } from "../util";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "Layout",
|
||||||
|
|
||||||
|
components: {
|
||||||
|
Home,
|
||||||
|
Page,
|
||||||
|
Sidebar,
|
||||||
|
Navbar,
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
isSidebarOpen: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
shouldShowNavbar() {
|
||||||
|
const { themeConfig } = this.$site;
|
||||||
|
const { frontmatter } = this.$page;
|
||||||
|
if (frontmatter.navbar === false || themeConfig.navbar === false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
this.$title ||
|
||||||
|
themeConfig.logo ||
|
||||||
|
themeConfig.repo ||
|
||||||
|
themeConfig.nav ||
|
||||||
|
this.$themeLocaleConfig.nav
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
shouldShowSidebar() {
|
||||||
|
const { frontmatter } = this.$page;
|
||||||
|
return (
|
||||||
|
!frontmatter.home &&
|
||||||
|
frontmatter.sidebar !== false &&
|
||||||
|
this.sidebarItems.length
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
sidebarItems() {
|
||||||
|
return resolveSidebarItems(
|
||||||
|
this.$page,
|
||||||
|
this.$page.regularPath,
|
||||||
|
this.$site,
|
||||||
|
this.$localePath
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
pageClasses() {
|
||||||
|
const userPageClass = this.$page.frontmatter.pageClass;
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"no-navbar": !this.shouldShowNavbar,
|
||||||
|
"sidebar-open": this.isSidebarOpen,
|
||||||
|
"no-sidebar": !this.shouldShowSidebar,
|
||||||
|
},
|
||||||
|
userPageClass,
|
||||||
|
];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.$router.afterEach(() => {
|
||||||
|
this.isSidebarOpen = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
toggleSidebar(to) {
|
||||||
|
this.isSidebarOpen = typeof to === "boolean" ? to : !this.isSidebarOpen;
|
||||||
|
this.$emit("toggle-sidebar", this.isSidebarOpen);
|
||||||
|
},
|
||||||
|
|
||||||
|
// side swipe
|
||||||
|
onTouchStart(e) {
|
||||||
|
this.touchStart = {
|
||||||
|
x: e.changedTouches[0].clientX,
|
||||||
|
y: e.changedTouches[0].clientY,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
onTouchEnd(e) {
|
||||||
|
const dx = e.changedTouches[0].clientX - this.touchStart.x;
|
||||||
|
const dy = e.changedTouches[0].clientY - this.touchStart.y;
|
||||||
|
if (Math.abs(dx) > Math.abs(dy) && Math.abs(dx) > 40) {
|
||||||
|
if (dx > 0 && this.touchStart.x <= 80) {
|
||||||
|
this.toggleSidebar(true);
|
||||||
|
} else {
|
||||||
|
this.toggleSidebar(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
1
docs/src/.vuepress/theme/noopModule.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
export default {}
|
22
docs/src/.vuepress/theme/styles/arrow.styl
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
@require './config'
|
||||||
|
|
||||||
|
.arrow
|
||||||
|
display inline-block
|
||||||
|
width 0
|
||||||
|
height 0
|
||||||
|
&.up
|
||||||
|
border-left 4px solid transparent
|
||||||
|
border-right 4px solid transparent
|
||||||
|
border-bottom 6px solid $arrowBgColor
|
||||||
|
&.down
|
||||||
|
border-left 4px solid transparent
|
||||||
|
border-right 4px solid transparent
|
||||||
|
border-top 6px solid $arrowBgColor
|
||||||
|
&.right
|
||||||
|
border-top 4px solid transparent
|
||||||
|
border-bottom 4px solid transparent
|
||||||
|
border-left 6px solid $arrowBgColor
|
||||||
|
&.left
|
||||||
|
border-top 4px solid transparent
|
||||||
|
border-bottom 4px solid transparent
|
||||||
|
border-right 6px solid $arrowBgColor
|
137
docs/src/.vuepress/theme/styles/code.styl
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
{$contentClass}
|
||||||
|
code
|
||||||
|
color lighten($textColor, 20%)
|
||||||
|
padding 0.25rem 0.5rem
|
||||||
|
margin 0
|
||||||
|
font-size 0.85em
|
||||||
|
background-color rgba(27,31,35,0.05)
|
||||||
|
border-radius 3px
|
||||||
|
.token
|
||||||
|
&.deleted
|
||||||
|
color #EC5975
|
||||||
|
&.inserted
|
||||||
|
color $accentColor
|
||||||
|
|
||||||
|
{$contentClass}
|
||||||
|
pre, pre[class*="language-"]
|
||||||
|
line-height 1.4
|
||||||
|
padding 1.25rem 1.5rem
|
||||||
|
margin 0.85rem 0
|
||||||
|
background-color $codeBgColor
|
||||||
|
border-radius 6px
|
||||||
|
overflow auto
|
||||||
|
code
|
||||||
|
color #fff
|
||||||
|
padding 0
|
||||||
|
background-color transparent
|
||||||
|
border-radius 0
|
||||||
|
|
||||||
|
div[class*="language-"]
|
||||||
|
position relative
|
||||||
|
background-color $codeBgColor
|
||||||
|
border-radius 6px
|
||||||
|
.highlight-lines
|
||||||
|
user-select none
|
||||||
|
padding-top 1.3rem
|
||||||
|
position absolute
|
||||||
|
top 0
|
||||||
|
left 0
|
||||||
|
width 100%
|
||||||
|
line-height 1.4
|
||||||
|
.highlighted
|
||||||
|
background-color rgba(0, 0, 0, 66%)
|
||||||
|
pre, pre[class*="language-"]
|
||||||
|
background transparent
|
||||||
|
position relative
|
||||||
|
z-index 1
|
||||||
|
&::before
|
||||||
|
position absolute
|
||||||
|
z-index 3
|
||||||
|
top 0.8em
|
||||||
|
right 1em
|
||||||
|
font-size 0.75rem
|
||||||
|
color rgba(255, 255, 255, 0.4)
|
||||||
|
&:not(.line-numbers-mode)
|
||||||
|
.line-numbers-wrapper
|
||||||
|
display none
|
||||||
|
&.line-numbers-mode
|
||||||
|
.highlight-lines .highlighted
|
||||||
|
position relative
|
||||||
|
&:before
|
||||||
|
content ' '
|
||||||
|
position absolute
|
||||||
|
z-index 3
|
||||||
|
left 0
|
||||||
|
top 0
|
||||||
|
display block
|
||||||
|
width $lineNumbersWrapperWidth
|
||||||
|
height 100%
|
||||||
|
background-color rgba(0, 0, 0, 66%)
|
||||||
|
pre
|
||||||
|
padding-left $lineNumbersWrapperWidth + 1 rem
|
||||||
|
vertical-align middle
|
||||||
|
.line-numbers-wrapper
|
||||||
|
position absolute
|
||||||
|
top 0
|
||||||
|
width $lineNumbersWrapperWidth
|
||||||
|
text-align center
|
||||||
|
color rgba(255, 255, 255, 0.3)
|
||||||
|
padding 1.25rem 0
|
||||||
|
line-height 1.4
|
||||||
|
br
|
||||||
|
user-select none
|
||||||
|
.line-number
|
||||||
|
position relative
|
||||||
|
z-index 4
|
||||||
|
user-select none
|
||||||
|
font-size 0.85em
|
||||||
|
&::after
|
||||||
|
content ''
|
||||||
|
position absolute
|
||||||
|
z-index 2
|
||||||
|
top 0
|
||||||
|
left 0
|
||||||
|
width $lineNumbersWrapperWidth
|
||||||
|
height 100%
|
||||||
|
border-radius 6px 0 0 6px
|
||||||
|
border-right 1px solid rgba(0, 0, 0, 66%)
|
||||||
|
background-color $codeBgColor
|
||||||
|
|
||||||
|
|
||||||
|
for lang in $codeLang
|
||||||
|
div{'[class~="language-' + lang + '"]'}
|
||||||
|
&:before
|
||||||
|
content ('' + lang)
|
||||||
|
|
||||||
|
div[class~="language-javascript"]
|
||||||
|
&:before
|
||||||
|
content "js"
|
||||||
|
|
||||||
|
div[class~="language-typescript"]
|
||||||
|
&:before
|
||||||
|
content "ts"
|
||||||
|
|
||||||
|
div[class~="language-markup"]
|
||||||
|
&:before
|
||||||
|
content "html"
|
||||||
|
|
||||||
|
div[class~="language-markdown"]
|
||||||
|
&:before
|
||||||
|
content "md"
|
||||||
|
|
||||||
|
div[class~="language-json"]:before
|
||||||
|
content "json"
|
||||||
|
|
||||||
|
div[class~="language-ruby"]:before
|
||||||
|
content "rb"
|
||||||
|
|
||||||
|
div[class~="language-python"]:before
|
||||||
|
content "py"
|
||||||
|
|
||||||
|
div[class~="language-bash"]:before
|
||||||
|
content "sh"
|
||||||
|
|
||||||
|
div[class~="language-php"]:before
|
||||||
|
content "php"
|
||||||
|
|
||||||
|
@import '~prismjs/themes/prism-tomorrow.css'
|
1
docs/src/.vuepress/theme/styles/config.styl
Normal file
@ -0,0 +1 @@
|
|||||||
|
$contentClass = '.theme-default-content'
|
44
docs/src/.vuepress/theme/styles/custom-blocks.styl
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
.custom-block
|
||||||
|
.custom-block-title
|
||||||
|
font-weight 600
|
||||||
|
margin-bottom -0.4rem
|
||||||
|
&.tip, &.warning, &.danger
|
||||||
|
padding .1rem 1.5rem
|
||||||
|
border-left-width .5rem
|
||||||
|
border-left-style solid
|
||||||
|
margin 1rem 0
|
||||||
|
&.tip
|
||||||
|
background-color #f3f5f7
|
||||||
|
border-color #42b983
|
||||||
|
&.warning
|
||||||
|
background-color rgba(255,229,100,.3)
|
||||||
|
border-color darken(#ffe564, 35%)
|
||||||
|
color darken(#ffe564, 70%)
|
||||||
|
.custom-block-title
|
||||||
|
color darken(#ffe564, 50%)
|
||||||
|
a
|
||||||
|
color $textColor
|
||||||
|
&.danger
|
||||||
|
background-color #ffe6e6
|
||||||
|
border-color darken(red, 20%)
|
||||||
|
color darken(red, 70%)
|
||||||
|
.custom-block-title
|
||||||
|
color darken(red, 40%)
|
||||||
|
a
|
||||||
|
color $textColor
|
||||||
|
&.details
|
||||||
|
display block
|
||||||
|
position relative
|
||||||
|
border-radius 2px
|
||||||
|
margin 1.6em 0
|
||||||
|
padding 1.6em
|
||||||
|
background-color #eee
|
||||||
|
h4
|
||||||
|
margin-top 0
|
||||||
|
figure, p
|
||||||
|
&:last-child
|
||||||
|
margin-bottom 0
|
||||||
|
padding-bottom 0
|
||||||
|
summary
|
||||||
|
outline none
|
||||||
|
cursor pointer
|
200
docs/src/.vuepress/theme/styles/index.styl
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
@require './config'
|
||||||
|
@require './code'
|
||||||
|
@require './custom-blocks'
|
||||||
|
@require './arrow'
|
||||||
|
@require './wrapper'
|
||||||
|
@require './toc'
|
||||||
|
|
||||||
|
html, body
|
||||||
|
padding 0
|
||||||
|
margin 0
|
||||||
|
background-color #fff
|
||||||
|
|
||||||
|
body
|
||||||
|
font-family -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif
|
||||||
|
-webkit-font-smoothing antialiased
|
||||||
|
-moz-osx-font-smoothing grayscale
|
||||||
|
font-size 16px
|
||||||
|
color $textColor
|
||||||
|
|
||||||
|
.page
|
||||||
|
padding-left $sidebarWidth
|
||||||
|
|
||||||
|
.navbar
|
||||||
|
position fixed
|
||||||
|
z-index 20
|
||||||
|
top 0
|
||||||
|
left 0
|
||||||
|
right 0
|
||||||
|
height $navbarHeight
|
||||||
|
background-color #fff
|
||||||
|
box-sizing border-box
|
||||||
|
border-bottom 1px solid $borderColor
|
||||||
|
|
||||||
|
.sidebar-mask
|
||||||
|
position fixed
|
||||||
|
z-index 9
|
||||||
|
top 0
|
||||||
|
left 0
|
||||||
|
width 100vw
|
||||||
|
height 100vh
|
||||||
|
display none
|
||||||
|
|
||||||
|
.sidebar
|
||||||
|
font-size 16px
|
||||||
|
background-color #fff
|
||||||
|
width $sidebarWidth
|
||||||
|
position fixed
|
||||||
|
z-index 10
|
||||||
|
margin 0
|
||||||
|
top $navbarHeight
|
||||||
|
left 0
|
||||||
|
bottom 0
|
||||||
|
box-sizing border-box
|
||||||
|
border-right 1px solid $borderColor
|
||||||
|
overflow-y auto
|
||||||
|
|
||||||
|
{$contentClass}:not(.custom)
|
||||||
|
@extend $wrapper
|
||||||
|
> *:first-child
|
||||||
|
margin-top $navbarHeight
|
||||||
|
|
||||||
|
a:hover
|
||||||
|
text-decoration underline
|
||||||
|
|
||||||
|
p.demo
|
||||||
|
padding 1rem 1.5rem
|
||||||
|
border 1px solid #ddd
|
||||||
|
border-radius 4px
|
||||||
|
|
||||||
|
img
|
||||||
|
max-width 100%
|
||||||
|
|
||||||
|
{$contentClass}.custom
|
||||||
|
padding 0
|
||||||
|
margin 0
|
||||||
|
|
||||||
|
img
|
||||||
|
max-width 100%
|
||||||
|
|
||||||
|
a
|
||||||
|
font-weight 500
|
||||||
|
color $accentColor
|
||||||
|
text-decoration none
|
||||||
|
|
||||||
|
p a code
|
||||||
|
font-weight 400
|
||||||
|
color $accentColor
|
||||||
|
|
||||||
|
kbd
|
||||||
|
background #eee
|
||||||
|
border solid 0.15rem #ddd
|
||||||
|
border-bottom solid 0.25rem #ddd
|
||||||
|
border-radius 0.15rem
|
||||||
|
padding 0 0.15em
|
||||||
|
|
||||||
|
blockquote
|
||||||
|
font-size 1rem
|
||||||
|
color #999;
|
||||||
|
border-left .2rem solid #dfe2e5
|
||||||
|
margin 1rem 0
|
||||||
|
padding .25rem 0 .25rem 1rem
|
||||||
|
|
||||||
|
& > p
|
||||||
|
margin 0
|
||||||
|
|
||||||
|
ul, ol
|
||||||
|
padding-left 1.2em
|
||||||
|
|
||||||
|
strong
|
||||||
|
font-weight 600
|
||||||
|
|
||||||
|
h1, h2, h3, h4, h5, h6
|
||||||
|
font-weight 600
|
||||||
|
line-height 1.25
|
||||||
|
|
||||||
|
{$contentClass}:not(.custom) > &
|
||||||
|
margin-top (0.5rem - $navbarHeight)
|
||||||
|
padding-top ($navbarHeight + 1rem)
|
||||||
|
margin-bottom 0
|
||||||
|
|
||||||
|
&:first-child
|
||||||
|
margin-top -1.5rem
|
||||||
|
margin-bottom 1rem
|
||||||
|
|
||||||
|
+ p, + pre, + .custom-block
|
||||||
|
margin-top 2rem
|
||||||
|
|
||||||
|
&:hover .header-anchor
|
||||||
|
opacity: 1
|
||||||
|
|
||||||
|
h1
|
||||||
|
font-size 2.2rem
|
||||||
|
|
||||||
|
h2
|
||||||
|
font-size 1.65rem
|
||||||
|
padding-bottom .3rem
|
||||||
|
border-bottom 1px solid $borderColor
|
||||||
|
|
||||||
|
h3
|
||||||
|
font-size 1.35rem
|
||||||
|
|
||||||
|
a.header-anchor
|
||||||
|
font-size 0.85em
|
||||||
|
float left
|
||||||
|
margin-left -0.87em
|
||||||
|
padding-right 0.23em
|
||||||
|
margin-top 0.125em
|
||||||
|
opacity 0
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
text-decoration none
|
||||||
|
|
||||||
|
code, kbd, .line-number
|
||||||
|
font-family source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace
|
||||||
|
|
||||||
|
p, ul, ol
|
||||||
|
line-height 1.7
|
||||||
|
|
||||||
|
hr
|
||||||
|
border 0
|
||||||
|
border-top 1px solid $borderColor
|
||||||
|
|
||||||
|
table
|
||||||
|
border-collapse collapse
|
||||||
|
margin 1rem 0
|
||||||
|
display: block
|
||||||
|
overflow-x: auto
|
||||||
|
|
||||||
|
tr
|
||||||
|
border-top 1px solid #dfe2e5
|
||||||
|
|
||||||
|
&:nth-child(2n)
|
||||||
|
background-color #f6f8fa
|
||||||
|
|
||||||
|
th, td
|
||||||
|
border 1px solid #dfe2e5
|
||||||
|
padding .6em 1em
|
||||||
|
|
||||||
|
.theme-container
|
||||||
|
&.sidebar-open
|
||||||
|
.sidebar-mask
|
||||||
|
display: block
|
||||||
|
|
||||||
|
&.no-navbar
|
||||||
|
{$contentClass}:not(.custom) > h1, h2, h3, h4, h5, h6
|
||||||
|
margin-top 1.5rem
|
||||||
|
padding-top 0
|
||||||
|
|
||||||
|
.sidebar
|
||||||
|
top 0
|
||||||
|
|
||||||
|
@media (min-width: ($MQMobile + 1px))
|
||||||
|
.theme-container.no-sidebar
|
||||||
|
.sidebar
|
||||||
|
display none
|
||||||
|
|
||||||
|
.page
|
||||||
|
padding-left 0
|
||||||
|
|
||||||
|
@require 'mobile.styl'
|
37
docs/src/.vuepress/theme/styles/mobile.styl
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
@require './config'
|
||||||
|
|
||||||
|
$mobileSidebarWidth = $sidebarWidth * 0.82
|
||||||
|
|
||||||
|
// narrow desktop / iPad
|
||||||
|
@media (max-width: $MQNarrow)
|
||||||
|
.sidebar
|
||||||
|
font-size 15px
|
||||||
|
width $mobileSidebarWidth
|
||||||
|
.page
|
||||||
|
padding-left $mobileSidebarWidth
|
||||||
|
|
||||||
|
// wide mobile
|
||||||
|
@media (max-width: $MQMobile)
|
||||||
|
.sidebar
|
||||||
|
top 0
|
||||||
|
padding-top $navbarHeight
|
||||||
|
transform translateX(-100%)
|
||||||
|
transition transform .2s ease
|
||||||
|
.page
|
||||||
|
padding-left 0
|
||||||
|
.theme-container
|
||||||
|
&.sidebar-open
|
||||||
|
.sidebar
|
||||||
|
transform translateX(0)
|
||||||
|
&.no-navbar
|
||||||
|
.sidebar
|
||||||
|
padding-top: 0
|
||||||
|
|
||||||
|
// narrow mobile
|
||||||
|
@media (max-width: $MQMobileNarrow)
|
||||||
|
h1
|
||||||
|
font-size 1.9rem
|
||||||
|
{$contentClass}
|
||||||
|
div[class*="language-"]
|
||||||
|
margin 0.85rem -1.5rem
|
||||||
|
border-radius 0
|
3
docs/src/.vuepress/theme/styles/toc.styl
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
.table-of-contents
|
||||||
|
.badge
|
||||||
|
vertical-align middle
|
9
docs/src/.vuepress/theme/styles/wrapper.styl
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
$wrapper
|
||||||
|
max-width $contentWidth
|
||||||
|
margin 0 auto
|
||||||
|
padding 2rem 2.5rem
|
||||||
|
@media (max-width: $MQNarrow)
|
||||||
|
padding 2rem
|
||||||
|
@media (max-width: $MQMobileNarrow)
|
||||||
|
padding 1.5rem
|
||||||
|
|
244
docs/src/.vuepress/theme/util/index.js
Normal file
@ -0,0 +1,244 @@
|
|||||||
|
export const hashRE = /#.*$/
|
||||||
|
export const extRE = /\.(md|html)$/
|
||||||
|
export const endingSlashRE = /\/$/
|
||||||
|
export const outboundRE = /^[a-z]+:/i
|
||||||
|
|
||||||
|
export function normalize (path) {
|
||||||
|
return decodeURI(path)
|
||||||
|
.replace(hashRE, '')
|
||||||
|
.replace(extRE, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getHash (path) {
|
||||||
|
const match = path.match(hashRE)
|
||||||
|
if (match) {
|
||||||
|
return match[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isExternal (path) {
|
||||||
|
return outboundRE.test(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isMailto (path) {
|
||||||
|
return /^mailto:/.test(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isTel (path) {
|
||||||
|
return /^tel:/.test(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ensureExt (path) {
|
||||||
|
if (isExternal(path)) {
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
const hashMatch = path.match(hashRE)
|
||||||
|
const hash = hashMatch ? hashMatch[0] : ''
|
||||||
|
const normalized = normalize(path)
|
||||||
|
|
||||||
|
if (endingSlashRE.test(normalized)) {
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
return normalized + '.html' + hash
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isActive (route, path) {
|
||||||
|
const routeHash = decodeURIComponent(route.hash)
|
||||||
|
const linkHash = getHash(path)
|
||||||
|
if (linkHash && routeHash !== linkHash) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const routePath = normalize(route.path)
|
||||||
|
const pagePath = normalize(path)
|
||||||
|
return routePath === pagePath
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolvePage (pages, rawPath, base) {
|
||||||
|
if (isExternal(rawPath)) {
|
||||||
|
return {
|
||||||
|
type: 'external',
|
||||||
|
path: rawPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (base) {
|
||||||
|
rawPath = resolvePath(rawPath, base)
|
||||||
|
}
|
||||||
|
const path = normalize(rawPath)
|
||||||
|
for (let i = 0; i < pages.length; i++) {
|
||||||
|
if (normalize(pages[i].regularPath) === path) {
|
||||||
|
return Object.assign({}, pages[i], {
|
||||||
|
type: 'page',
|
||||||
|
path: ensureExt(pages[i].path)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.error(`[vuepress] No matching page found for sidebar item "${rawPath}"`)
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolvePath (relative, base, append) {
|
||||||
|
const firstChar = relative.charAt(0)
|
||||||
|
if (firstChar === '/') {
|
||||||
|
return relative
|
||||||
|
}
|
||||||
|
|
||||||
|
if (firstChar === '?' || firstChar === '#') {
|
||||||
|
return base + relative
|
||||||
|
}
|
||||||
|
|
||||||
|
const stack = base.split('/')
|
||||||
|
|
||||||
|
// remove trailing segment if:
|
||||||
|
// - not appending
|
||||||
|
// - appending to trailing slash (last segment is empty)
|
||||||
|
if (!append || !stack[stack.length - 1]) {
|
||||||
|
stack.pop()
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolve relative path
|
||||||
|
const segments = relative.replace(/^\//, '').split('/')
|
||||||
|
for (let i = 0; i < segments.length; i++) {
|
||||||
|
const segment = segments[i]
|
||||||
|
if (segment === '..') {
|
||||||
|
stack.pop()
|
||||||
|
} else if (segment !== '.') {
|
||||||
|
stack.push(segment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensure leading slash
|
||||||
|
if (stack[0] !== '') {
|
||||||
|
stack.unshift('')
|
||||||
|
}
|
||||||
|
|
||||||
|
return stack.join('/')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param { Page } page
|
||||||
|
* @param { string } regularPath
|
||||||
|
* @param { SiteData } site
|
||||||
|
* @param { string } localePath
|
||||||
|
* @returns { SidebarGroup }
|
||||||
|
*/
|
||||||
|
export function resolveSidebarItems (page, regularPath, site, localePath) {
|
||||||
|
const { pages, themeConfig } = site
|
||||||
|
|
||||||
|
const localeConfig = localePath && themeConfig.locales
|
||||||
|
? themeConfig.locales[localePath] || themeConfig
|
||||||
|
: themeConfig
|
||||||
|
|
||||||
|
const pageSidebarConfig = page.frontmatter.sidebar || localeConfig.sidebar || themeConfig.sidebar
|
||||||
|
if (pageSidebarConfig === 'auto') {
|
||||||
|
return resolveHeaders(page)
|
||||||
|
}
|
||||||
|
|
||||||
|
const sidebarConfig = localeConfig.sidebar || themeConfig.sidebar
|
||||||
|
if (!sidebarConfig) {
|
||||||
|
return []
|
||||||
|
} else {
|
||||||
|
const { base, config } = resolveMatchingConfig(regularPath, sidebarConfig)
|
||||||
|
if (config === 'auto') {
|
||||||
|
return resolveHeaders(page)
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
? config.map(item => resolveItem(item, pages, base))
|
||||||
|
: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param { Page } page
|
||||||
|
* @returns { SidebarGroup }
|
||||||
|
*/
|
||||||
|
function resolveHeaders (page) {
|
||||||
|
const headers = groupHeaders(page.headers || [])
|
||||||
|
return [{
|
||||||
|
type: 'group',
|
||||||
|
collapsable: false,
|
||||||
|
title: page.title,
|
||||||
|
path: null,
|
||||||
|
children: headers.map(h => ({
|
||||||
|
type: 'auto',
|
||||||
|
title: h.title,
|
||||||
|
basePath: page.path,
|
||||||
|
path: page.path + '#' + h.slug,
|
||||||
|
children: h.children || []
|
||||||
|
}))
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function groupHeaders (headers) {
|
||||||
|
// group h3s under h2
|
||||||
|
headers = headers.map(h => Object.assign({}, h))
|
||||||
|
let lastH2
|
||||||
|
headers.forEach(h => {
|
||||||
|
if (h.level === 2) {
|
||||||
|
lastH2 = h
|
||||||
|
} else if (lastH2) {
|
||||||
|
(lastH2.children || (lastH2.children = [])).push(h)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return headers.filter(h => h.level === 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveNavLinkItem (linkItem) {
|
||||||
|
return Object.assign(linkItem, {
|
||||||
|
type: linkItem.items && linkItem.items.length ? 'links' : 'link'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param { Route } route
|
||||||
|
* @param { Array<string|string[]> | Array<SidebarGroup> | [link: string]: SidebarConfig } config
|
||||||
|
* @returns { base: string, config: SidebarConfig }
|
||||||
|
*/
|
||||||
|
export function resolveMatchingConfig (regularPath, config) {
|
||||||
|
if (Array.isArray(config)) {
|
||||||
|
return {
|
||||||
|
base: '/',
|
||||||
|
config: config
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const base in config) {
|
||||||
|
if (ensureEndingSlash(regularPath).indexOf(encodeURI(base)) === 0) {
|
||||||
|
return {
|
||||||
|
base,
|
||||||
|
config: config[base]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureEndingSlash (path) {
|
||||||
|
return /(\.html|\/)$/.test(path)
|
||||||
|
? path
|
||||||
|
: path + '/'
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveItem (item, pages, base, groupDepth = 1) {
|
||||||
|
if (typeof item === 'string') {
|
||||||
|
return resolvePage(pages, item, base)
|
||||||
|
} else if (Array.isArray(item)) {
|
||||||
|
return Object.assign(resolvePage(pages, item[0], base), {
|
||||||
|
title: item[1]
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
const children = item.children || []
|
||||||
|
if (children.length === 0 && item.path) {
|
||||||
|
return Object.assign(resolvePage(pages, item.path, base), {
|
||||||
|
title: item.title
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
type: 'group',
|
||||||
|
path: item.path,
|
||||||
|
title: item.title,
|
||||||
|
sidebarDepth: item.sidebarDepth,
|
||||||
|
initialOpenGroupIndex: item.initialOpenGroupIndex,
|
||||||
|
children: children.map(child => resolveItem(child, pages, base, groupDepth + 1)),
|
||||||
|
collapsable: item.collapsable !== false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
45
docs/src/appendix/index.md
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
---
|
||||||
|
sidebarDepth: 1
|
||||||
|
sidebar: auto
|
||||||
|
---
|
||||||
|
|
||||||
|
# Appendix
|
||||||
|
|
||||||
|
## GraphQL API
|
||||||
|
|
||||||
|
Hetty exposes a GraphQL API over HTTP for managing all its features. This API is
|
||||||
|
used by the web admin interface; a Next.js app using Apollo Client.
|
||||||
|
|
||||||
|
### Playground
|
||||||
|
|
||||||
|
You can also introspect and manually experiment with the API via the included GraphQL Playground. To access it, start Hetty and visit: [http://localhost:8080/api/playground](http://localhost:8080/api/playground).
|
||||||
|
|
||||||
|
### Schema
|
||||||
|
|
||||||
|
<<< @/../pkg/api/schema.graphql
|
||||||
|
|
||||||
|
Source: [pkg/api/schema.graphql](https://github.com/dstotijn/hetty/blob/master/pkg/api/schema.graphql)
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2020 David Stotijn
|
||||||
|
|
||||||
|
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.
|
BIN
docs/src/guide/add_scope_rule.png
Normal file
After Width: | Height: | Size: 46 KiB |
BIN
docs/src/guide/create_project.png
Normal file
After Width: | Height: | Size: 21 KiB |
BIN
docs/src/guide/filter_in_scope.png
Normal file
After Width: | Height: | Size: 27 KiB |
85
docs/src/guide/getting-started.md
Executable file
@ -0,0 +1,85 @@
|
|||||||
|
# Getting Started
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Hetty compiles to a static binary, with an embedded SQLite database and web
|
||||||
|
admin interface.
|
||||||
|
|
||||||
|
### Install pre-built release (recommended)
|
||||||
|
|
||||||
|
👉 Downloads for Linux, macOS and Windows are available on the [releases page](https://github.com/dstotijn/hetty/releases).
|
||||||
|
|
||||||
|
### Build from source
|
||||||
|
|
||||||
|
#### Prerequisites
|
||||||
|
|
||||||
|
- [Go](https://golang.org/)
|
||||||
|
- [Yarn](https://yarnpkg.com/)
|
||||||
|
- [go.rice](https://github.com/GeertJohan/go.rice)
|
||||||
|
|
||||||
|
Hetty depends on SQLite (via [mattn/go-sqlite3](https://github.com/mattn/go-sqlite3))
|
||||||
|
and needs `cgo` to compile. Additionally, the static resources for the web admin interface
|
||||||
|
(Next.js) need to be generated via [Yarn](https://yarnpkg.com/) and embedded in
|
||||||
|
a `.go` file with [go.rice](https://github.com/GeertJohan/go.rice) beforehand.
|
||||||
|
|
||||||
|
Clone the repository and use the `build` make target to create a binary:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ git clone git@github.com:dstotijn/hetty.git
|
||||||
|
$ cd hetty
|
||||||
|
$ make build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker
|
||||||
|
|
||||||
|
A Docker image is available on Docker Hub: [`dstotijn/hetty`](https://hub.docker.com/r/dstotijn/hetty).
|
||||||
|
For persistent storage of CA certificate and project databases, mount a volume:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ mkdir -p $HOME/.hetty
|
||||||
|
$ docker run -v $HOME/.hetty:/root/.hetty -p 8080:8080 dstotijn/hetty
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
When Hetty is started, by default it listens on `:8080` and is accessible via
|
||||||
|
[http://localhost:8080](http://localhost:8080). Depending on incoming HTTP
|
||||||
|
requests, it either acts as a MITM proxy, or it serves the API and web interface.
|
||||||
|
|
||||||
|
By default, project database files and CA certificates are stored in a `.hetty`
|
||||||
|
directory under the user's home directory (`$HOME` on Linux/macOS, `%USERPROFILE%`
|
||||||
|
on Windows).
|
||||||
|
|
||||||
|
To start, ensure `hetty` (downloaded from a release, or manually built) is in your
|
||||||
|
`$PATH` and run:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ hetty
|
||||||
|
```
|
||||||
|
|
||||||
|
You should see:
|
||||||
|
|
||||||
|
```
|
||||||
|
2020/11/01 14:47:10 [INFO] Running server on :8080 ...
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, visit [http://localhost:8080](http://localhost:8080) to get started.
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
An overview of available configuration flags:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ hetty -h
|
||||||
|
Usage of ./hetty:
|
||||||
|
-addr string
|
||||||
|
TCP address to listen on, in the form "host:port" (default ":8080")
|
||||||
|
-adminPath string
|
||||||
|
File path to admin build
|
||||||
|
-cert string
|
||||||
|
CA certificate filepath. Creates a new CA certificate is file doesn't exist (default "~/.hetty/hetty_cert.pem")
|
||||||
|
-key string
|
||||||
|
CA private key filepath. Creates a new CA private key if file doesn't exist (default "~/.hetty/hetty_key.pem")
|
||||||
|
-projects string
|
||||||
|
Projects directory path (default "~/.hetty/projects")
|
||||||
|
```
|
BIN
docs/src/guide/hetty_v0.2.0_header.png
Normal file
After Width: | Height: | Size: 144 KiB |
29
docs/src/guide/index.md
Executable file
@ -0,0 +1,29 @@
|
|||||||
|
---
|
||||||
|
sidebarDepth: 0
|
||||||
|
---
|
||||||
|
|
||||||
|
# Introduction
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
**Hetty** is an HTTP toolkit for security research. It aims to become an open
|
||||||
|
source alternative to commercial software like Burp Suite Pro, with powerful
|
||||||
|
features tailored to the needs of the infosec and bug bounty community.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Machine-in-the-middle (MITM) HTTP/1.1 proxy with logs
|
||||||
|
- Project based database storage (SQLite)
|
||||||
|
- Scope support
|
||||||
|
- Headless management API using GraphQL
|
||||||
|
- Embedded web admin interface (Next.js)
|
||||||
|
|
||||||
|
::: tip INFO
|
||||||
|
Hetty is in early development. Additional features are planned
|
||||||
|
for a `v1.0` release. Please see the <a href="https://github.com/dstotijn/hetty/projects/1">backlog</a>
|
||||||
|
for details.
|
||||||
|
:::
|
BIN
docs/src/guide/manage_projects.png
Normal file
After Width: | Height: | Size: 49 KiB |
238
docs/src/guide/modules.md
Normal file
@ -0,0 +1,238 @@
|
|||||||
|
---
|
||||||
|
sidebarDepth: 3
|
||||||
|
---
|
||||||
|
|
||||||
|
# Modules
|
||||||
|
|
||||||
|
Hetty consists of various _modules_ that together form an HTTP toolkit. They
|
||||||
|
typically are managed via the web admin interface. Some modules expose settings
|
||||||
|
and behavior that is leveraged by other modules.
|
||||||
|
|
||||||
|
The available modules:
|
||||||
|
|
||||||
|
[[toc]]
|
||||||
|
|
||||||
|
## Projects
|
||||||
|
|
||||||
|
Projects are self-contained (SQLite) database files that contain module data.
|
||||||
|
They allow you organize your work, for example to split your work between research
|
||||||
|
targets.
|
||||||
|
|
||||||
|
You can create multiple projects, but only one can be open at a time. Most other
|
||||||
|
modules are useful only if you have a project opened, so creating a project is
|
||||||
|
typically the first thing you do when you start using Hetty.
|
||||||
|
|
||||||
|
### Creating a new project
|
||||||
|
|
||||||
|
When you open the Hetty admin interface after starting the program, you’ll be prompted
|
||||||
|
on the homepage to create a new project. Give it a name (alphanumeric and space character)
|
||||||
|
and click the create button:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
The project name will become the base for the database file on disk. For example,
|
||||||
|
if you name your project `My first project`, the file on disk will be
|
||||||
|
`My first project.db`.
|
||||||
|
|
||||||
|
::: tip INFO
|
||||||
|
Project database files by default are stored in `$HOME/.hetty/projects` on Linux
|
||||||
|
and macOS, and `%USERPROFILE%/.hetty` on Windows. You can override this path with
|
||||||
|
the `-projects` flag. See: [Usage](/guide/getting-started.md#usage).
|
||||||
|
:::
|
||||||
|
|
||||||
|
### Managing projects
|
||||||
|
|
||||||
|
You can open and delete existing projects on the “Projects” page, available via
|
||||||
|
the folder icon in the menu bar.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
An opened (_active_) project is listed in green. You can close it using the “X”
|
||||||
|
button. To delete a project, use the trash bin icon.
|
||||||
|
|
||||||
|
::: danger
|
||||||
|
Deleting a project is irreversible.
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Proxy
|
||||||
|
|
||||||
|
Hetty features a HTTP/1.1 proxy server with machine-in-the-middle (MITM) behavior.
|
||||||
|
For now, its only configuration is done via command line flags.
|
||||||
|
|
||||||
|
::: tip INFO
|
||||||
|
Support for HTTP/2 and WebSockets are currently not supported, but this will
|
||||||
|
likely be addressed in the (near) future.
|
||||||
|
:::
|
||||||
|
|
||||||
|
### Network address
|
||||||
|
|
||||||
|
To configure the network address that the proxy listens on, use the `-addr` flag
|
||||||
|
when starting Hetty. The address needs to be in the format `[host]:port`. E.g.
|
||||||
|
`localhost:3000` or `:3000`. If the host in the address is empty or a literal
|
||||||
|
unspecified IP address, Hetty listens on all available unicast and anycast IP
|
||||||
|
addresses of the local system.
|
||||||
|
|
||||||
|
::: tip INFO
|
||||||
|
When not specified with `-addr`, Hetty by default listens on `:8080`.
|
||||||
|
:::
|
||||||
|
|
||||||
|
Example of starting Hetty, binding to port `3000` on all IPs of the local system:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ hetty -addr :3000
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using the proxy
|
||||||
|
|
||||||
|
To use Hetty as an HTTP proxy server, you’ll need to configure your HTTP client (e.g.
|
||||||
|
your browser or mobile OS). Refer to your client documentation or use a search
|
||||||
|
engine to find instructions for setting a HTTP proxy.
|
||||||
|
|
||||||
|
### Certificate Authority (CA)
|
||||||
|
|
||||||
|
In order for Hetty to proxy requests going to HTTPS endpoints, a root CA certificate for
|
||||||
|
Hetty will need to be set up. Furthermore, the CA certificate needs to be
|
||||||
|
installed to the host for them to be trusted by your browser. The following steps
|
||||||
|
will cover how you can generate a certificate, provide it to Hetty, and how
|
||||||
|
you can install it in your local CA store.
|
||||||
|
|
||||||
|
::: tip INFO
|
||||||
|
Certificate management features (e.g. automated installing of a root CA to your local
|
||||||
|
OS or browser trust store) are planned for a future release. In the meantime, please
|
||||||
|
use the instructions below.
|
||||||
|
:::
|
||||||
|
|
||||||
|
#### Generating a CA certificate
|
||||||
|
|
||||||
|
You can generate a CA keypair two different ways. The first is bundled directly
|
||||||
|
with Hetty, and simplifies the process immensely. The alternative is using OpenSSL
|
||||||
|
to generate them, which provides more control over expiration time and cryptography
|
||||||
|
used, but requires you install the OpenSSL tooling. The first is suggested for any
|
||||||
|
beginners trying to get started.
|
||||||
|
|
||||||
|
#### Generating CA certificates with hetty
|
||||||
|
|
||||||
|
Hetty will generate the default key and certificate on its own if none are supplied
|
||||||
|
or found in `~/.hetty/` when first running the CLI. To generate a default key and
|
||||||
|
certificate with hetty, simply run the command with no arguments.
|
||||||
|
|
||||||
|
You should now have a key and certificate located at `~/.hetty/hetty_key.pem` and
|
||||||
|
`~/.hetty/hetty_cert.pem` respectively.
|
||||||
|
|
||||||
|
#### Generating CA certificates with OpenSSL
|
||||||
|
|
||||||
|
::: tip INFO
|
||||||
|
This following instructions are for Linux but should provide guidance for Windows
|
||||||
|
and macOS as well.
|
||||||
|
:::
|
||||||
|
|
||||||
|
You can start off by generating a new key and CA certificate which will both expire
|
||||||
|
after a month.
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
$ mkdir ~/.hetty
|
||||||
|
$ openssl req -newkey rsa:2048 -new -nodes -x509 -days 31 -keyout ~/.hetty/hetty_key.pem -out ~/.hetty/hetty_cert.pem
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
The default location which `hetty` will check for the key and CA certificate is under
|
||||||
|
`~/.hetty/`, at `hetty_key.pem` and `hetty_cert.pem` respectively. You can move them
|
||||||
|
here and `hetty` will detect them automatically. Otherwise, you can specify the
|
||||||
|
location of these as arguments to `hetty`.
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
\$ hetty -key /some/directory/key.pem -cert /some/directory/cert.pem
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Trusting the CA certificate
|
||||||
|
|
||||||
|
In order for your browser to allow traffic to the local Hetty proxy, you may need
|
||||||
|
to install these certificates to your local CA store.
|
||||||
|
|
||||||
|
On Ubuntu, you can update your local CA store with the certificate by running the
|
||||||
|
following commands:
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
$ sudo cp ~/.hetty/hetty_cert.pem /usr/local/share/ca-certificates/hetty.crt
|
||||||
|
$ sudo update-ca-certificates
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
On Windows, you would add your certificate by using the Certificate Manager,
|
||||||
|
which you can run via:
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
certmgr.msc
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
On macOS, you can add your certificate by using the Keychain Access program. This
|
||||||
|
can be found under `Application/Utilities/Keychain Access.app`. After opening this,
|
||||||
|
drag the certificate into the app. Next, open the certificate in the app, enter the
|
||||||
|
_Trust_ section, and under _When using this certificate_ select _Always Trust_.
|
||||||
|
|
||||||
|
::: tip INFO
|
||||||
|
Various Linux distributions may require other steps or commands for updating
|
||||||
|
their certificate authority. See the documentation relevant to your distribution for
|
||||||
|
more information on how to update the system to trust your self-signed certificate.
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
The scope module lets you define _rules_ that other modules can use to control
|
||||||
|
their behavior. For example, the [proxy logs module](#proxy-logs) can be configured to only
|
||||||
|
show logs for in-scope requests; meaning only requests are shown that match one
|
||||||
|
or more scope rules.
|
||||||
|
|
||||||
|
### Managing scope rules
|
||||||
|
|
||||||
|
You can manage scope rules via the “Scope” page, available via the crosshair icon
|
||||||
|
in the menu bar.
|
||||||
|
|
||||||
|
A rule consists of a _type_ and a regular expression ([RE2 syntax](https://github.com/google/re2/wiki/Syntax)).
|
||||||
|
The only supported type at the moment is “URL”.
|
||||||
|
|
||||||
|
::: tip INFO
|
||||||
|
Just like all module configuration, scope rules are defined and stored per-project.
|
||||||
|
:::
|
||||||
|
|
||||||
|
#### Adding a rule
|
||||||
|
|
||||||
|
On the ”Scope” page, enter a regular expression and click “Add rule”:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
_Example: Rule that matches URLs with `example.com` (or any subdomain) on any path._
|
||||||
|
|
||||||
|
#### Deleting rules
|
||||||
|
|
||||||
|
Use the trash icon next to an existing scope rule to delete it.
|
||||||
|
|
||||||
|
## Proxy logs
|
||||||
|
|
||||||
|
You can few logs captured by the Proxy module on the Proxy logs page, available
|
||||||
|
via the proxy icon in the menu bar.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Showing a log entry
|
||||||
|
|
||||||
|
Click a row in the overview table to view log details in the bottom request and
|
||||||
|
response panes. When a request and/or response has a body, it's shown below the
|
||||||
|
HTTP headers. Header keys and values can be copied to clipboard by clicking them.
|
||||||
|
|
||||||
|
### Filtering logs
|
||||||
|
|
||||||
|
To only show log entries that match any of the [scope rules](#scope), click the
|
||||||
|
filter icon in the search bar and select “Only show in-scope requests”:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
::: tip INFO
|
||||||
|
At the moment of writing (`v0.2.0`), text based search is not implemented yet.
|
||||||
|
:::
|
BIN
docs/src/guide/proxy_logs.png
Normal file
After Width: | Height: | Size: 410 KiB |
6
docs/src/index.md
Executable file
@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
home: true
|
||||||
|
heroImage: https://hetty.xyz/assets/logo.png
|
||||||
|
actionText: Read the docs →
|
||||||
|
actionLink: /guide/
|
||||||
|
---
|
7869
docs/yarn.lock
Normal file
16
go.mod
@ -3,13 +3,15 @@ module github.com/dstotijn/hetty
|
|||||||
go 1.15
|
go 1.15
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/99designs/gqlgen v0.11.3
|
github.com/99designs/gqlgen v0.13.0
|
||||||
github.com/GeertJohan/go.rice v1.0.0
|
github.com/GeertJohan/go.rice v1.0.0
|
||||||
github.com/cayleygraph/cayley v0.7.7
|
github.com/Masterminds/squirrel v1.4.0
|
||||||
github.com/cayleygraph/quad v1.1.0
|
|
||||||
github.com/google/uuid v1.1.2
|
|
||||||
github.com/gorilla/mux v1.7.4
|
github.com/gorilla/mux v1.7.4
|
||||||
github.com/hidal-go/hidalgo v0.0.0-20190814174001-42e03f3b5eaa
|
github.com/hashicorp/golang-lru v0.5.1 // indirect
|
||||||
github.com/vektah/gqlparser/v2 v2.0.1
|
github.com/jmoiron/sqlx v1.2.0
|
||||||
golang.org/x/tools v0.0.0-20200925191224-5d1fdd8fa346 // indirect
|
github.com/mattn/go-sqlite3 v1.14.4
|
||||||
|
github.com/mitchellh/go-homedir v1.1.0
|
||||||
|
github.com/mitchellh/mapstructure v1.1.2 // indirect
|
||||||
|
github.com/vektah/gqlparser/v2 v2.1.0
|
||||||
|
google.golang.org/appengine v1.6.6 // indirect
|
||||||
)
|
)
|
||||||
|
379
go.sum
@ -1,317 +1,98 @@
|
|||||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
github.com/99designs/gqlgen v0.13.0 h1:haLTcUp3Vwp80xMVEg5KRNwzfUrgFdRmtBY8fuB8scA=
|
||||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
github.com/99designs/gqlgen v0.13.0/go.mod h1:NV130r6f4tpRWuAI+zsrSdooO/eWUv+Gyyoi3rEfXIk=
|
||||||
cloud.google.com/go v0.37.4/go.mod h1:NHPJ89PdicEuT9hdPXMROBD91xc5uRDxsMtSB16k7hw=
|
|
||||||
github.com/99designs/gqlgen v0.11.3 h1:oFSxl1DFS9X///uHV3y6CEfpcXWrDUxVblR4Xib2bs4=
|
|
||||||
github.com/99designs/gqlgen v0.11.3/go.mod h1:RgX5GRRdDWNkh4pBrdzNpNPFVsdoUFY2+adM6nb1N+4=
|
|
||||||
github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8=
|
|
||||||
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
|
|
||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
github.com/GeertJohan/go.incremental v1.0.0 h1:7AH+pY1XUgQE4Y1HcXYaMqAI0m9yrFqo/jt0CW30vsg=
|
github.com/GeertJohan/go.incremental v1.0.0 h1:7AH+pY1XUgQE4Y1HcXYaMqAI0m9yrFqo/jt0CW30vsg=
|
||||||
github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0=
|
github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0=
|
||||||
github.com/GeertJohan/go.rice v1.0.0 h1:KkI6O9uMaQU3VEKaj01ulavtF7o1fWT7+pk/4voiMLQ=
|
github.com/GeertJohan/go.rice v1.0.0 h1:KkI6O9uMaQU3VEKaj01ulavtF7o1fWT7+pk/4voiMLQ=
|
||||||
github.com/GeertJohan/go.rice v1.0.0/go.mod h1:eH6gbSOAUv07dQuZVnBmoDP8mgsM1rtixis4Tib9if0=
|
github.com/GeertJohan/go.rice v1.0.0/go.mod h1:eH6gbSOAUv07dQuZVnBmoDP8mgsM1rtixis4Tib9if0=
|
||||||
github.com/Microsoft/go-winio v0.4.12/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
|
github.com/Masterminds/squirrel v1.4.0 h1:he5i/EXixZxrBUWcxzDYMiju9WZ3ld/l7QBNuo/eN3w=
|
||||||
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk=
|
github.com/Masterminds/squirrel v1.4.0/go.mod h1:yaPeOnPG5ZRwL9oKdTsO/prlkPbXWZlRVMQ/gGlzIuA=
|
||||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
|
||||||
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
|
|
||||||
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
|
|
||||||
github.com/agnivade/levenshtein v1.0.1 h1:3oJU7J3FGFmyhn8KHjmVaZCN5hxTr7GxgRue+sxIXdQ=
|
github.com/agnivade/levenshtein v1.0.1 h1:3oJU7J3FGFmyhn8KHjmVaZCN5hxTr7GxgRue+sxIXdQ=
|
||||||
github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM=
|
github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM=
|
||||||
github.com/agnivade/levenshtein v1.0.3 h1:M5ZnqLOoZR8ygVq0FfkXsNOKzMCk0xRiow0R5+5VkQ0=
|
github.com/agnivade/levenshtein v1.0.3 h1:M5ZnqLOoZR8ygVq0FfkXsNOKzMCk0xRiow0R5+5VkQ0=
|
||||||
github.com/agnivade/levenshtein v1.0.3/go.mod h1:4SFRZbbXWLF4MU1T9Qg0pGgH3Pjs+t6ie5efyrwRJXs=
|
github.com/agnivade/levenshtein v1.0.3/go.mod h1:4SFRZbbXWLF4MU1T9Qg0pGgH3Pjs+t6ie5efyrwRJXs=
|
||||||
github.com/akavel/rsrc v0.8.0 h1:zjWn7ukO9Kc5Q62DOJCcxGpXC18RawVtYAGdz2aLlfw=
|
github.com/akavel/rsrc v0.8.0 h1:zjWn7ukO9Kc5Q62DOJCcxGpXC18RawVtYAGdz2aLlfw=
|
||||||
github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
|
github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
|
||||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
|
||||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
|
||||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
|
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
|
||||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
|
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
|
||||||
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
|
|
||||||
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q=
|
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q=
|
||||||
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
|
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
|
||||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
|
||||||
github.com/badgerodon/peg v0.0.0-20130729175151-9e5f7f4d07ca/go.mod h1:TWe0N2hv5qvpLHT+K16gYcGBllld4h65dQ/5CNuirmk=
|
|
||||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
|
||||||
github.com/beorn7/perks v1.0.0 h1:HWo1m869IqiPhD389kmkxeTalrjNbbJTC8LXupb+sl0=
|
|
||||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
|
||||||
github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4=
|
|
||||||
github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=
|
|
||||||
github.com/cayleygraph/cayley v0.7.7 h1:z+7xkAbg6bKiXJOtOkEG3zCm2K084sr/aGwFV7xcQNs=
|
|
||||||
github.com/cayleygraph/cayley v0.7.7/go.mod h1:VUd+PInYf94/VY41ePeFtFyP99BAs953kFT4N+6F7Ko=
|
|
||||||
github.com/cayleygraph/quad v1.1.0 h1:w1nXAmn+nz07+qlw89dke9LwWkYpeX+OcvfTvGQRBpM=
|
|
||||||
github.com/cayleygraph/quad v1.1.0/go.mod h1:maWODEekEhrO0mdc9h5n/oP7cH1h/OTgqQ2qWbuI9M4=
|
|
||||||
github.com/cenkalti/backoff v2.1.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
|
|
||||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
|
||||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
|
||||||
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
|
|
||||||
github.com/containerd/continuity v0.0.0-20181203112020-004b46473808/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
|
|
||||||
github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
|
|
||||||
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
|
|
||||||
github.com/coreos/bbolt v1.3.3/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
|
|
||||||
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
|
||||||
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
|
|
||||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
|
||||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
|
||||||
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
|
||||||
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
|
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||||
github.com/cznic/mathutil v0.0.0-20170313102836-1447ad269d64 h1:oad14P7M0/ZAPSMH1nl1vC8zdKVkA3kfHLO59z1l8Eg=
|
|
||||||
github.com/cznic/mathutil v0.0.0-20170313102836-1447ad269d64/go.mod h1:e6NPNENfs9mPDVNRekM7lKScauxd5kXTr1Mfyig6TDM=
|
|
||||||
github.com/d4l3k/messagediff v1.2.1 h1:ZcAIMYsUg0EAp9X+tt8/enBE/Q8Yd5kzPynLyKptt9U=
|
|
||||||
github.com/d4l3k/messagediff v1.2.1/go.mod h1:Oozbb1TVXFac9FtSIxHBMnBCq2qeH/2KkEQxENCrlLo=
|
|
||||||
github.com/daaku/go.zipexe v1.0.0 h1:VSOgZtH418pH9L16hC/JrgSNJbbAL26pj7lmD1+CGdY=
|
github.com/daaku/go.zipexe v1.0.0 h1:VSOgZtH418pH9L16hC/JrgSNJbbAL26pj7lmD1+CGdY=
|
||||||
github.com/daaku/go.zipexe v1.0.0/go.mod h1:z8IiR6TsVLEYKwXAoE/I+8ys/sDkgTzSL0CLnGVd57E=
|
github.com/daaku/go.zipexe v1.0.0/go.mod h1:z8IiR6TsVLEYKwXAoE/I+8ys/sDkgTzSL0CLnGVd57E=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dennwc/base v1.0.0 h1:xlBzvBNRvkQ1LFI/jom7rr0vZsvYDKtvMM6lIpjFb3M=
|
|
||||||
github.com/dennwc/base v1.0.0/go.mod h1:zaTDIiAcg2oKW9XhjIaRc1kJVteCFXSSW6jwmCedUaI=
|
|
||||||
github.com/dennwc/graphql v0.0.0-20180603144102-12cfed44bc5d/go.mod h1:lg9KQn0BgRCSCGNpcGvJp/0Ljf1Yxk8TZq9HSYc43fk=
|
|
||||||
github.com/dgraph-io/badger v1.5.4/go.mod h1:VZxzAIRPHRVNRKRo6AXrX9BJegn6il06VMTZVJYCIjQ=
|
|
||||||
github.com/dgraph-io/badger v1.5.5/go.mod h1:QgCntgIUPsjnp7cMLhUybJHb7iIoQWAHT6tF8ngCjWk=
|
|
||||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
|
||||||
github.com/dgryski/go-farm v0.0.0-20190416075124-e1214b5e05dc/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
|
|
||||||
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
|
|
||||||
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
|
||||||
github.com/dgryski/trifles v0.0.0-20190318185328-a8d75aae118c h1:TUuUh0Xgj97tLMNtWtNvI9mIV6isjEb9lBMNv+77IGM=
|
github.com/dgryski/trifles v0.0.0-20190318185328-a8d75aae118c h1:TUuUh0Xgj97tLMNtWtNvI9mIV6isjEb9lBMNv+77IGM=
|
||||||
github.com/dgryski/trifles v0.0.0-20190318185328-a8d75aae118c/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
|
github.com/dgryski/trifles v0.0.0-20190318185328-a8d75aae118c/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
|
||||||
github.com/dlclark/regexp2 v1.1.4 h1:1udHhhGkIMplSrLeMJpPN7BHz1Iq2wVBUcb+3fxzhQM=
|
|
||||||
github.com/dlclark/regexp2 v1.1.4/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
|
||||||
github.com/docker/docker v0.7.3-0.20180412203414-a422774e593b/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
|
||||||
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
|
|
||||||
github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
|
||||||
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
|
||||||
github.com/dop251/goja v0.0.0-20190105122144-6d5bf35058fa h1:cA2OMt2CQ2yq2WhQw16mHv6ej9YY07H4pzfR/z/y+1Q=
|
|
||||||
github.com/dop251/goja v0.0.0-20190105122144-6d5bf35058fa/go.mod h1:Mw6PkjjMXWbTj+nnj4s3QPXq1jaT0s5pC0iFD4+BOAA=
|
|
||||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
|
||||||
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
|
|
||||||
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
|
|
||||||
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
|
|
||||||
github.com/flimzy/diff v0.1.5/go.mod h1:lFJtC7SPsK0EroDmGTSrdtWKAxOk3rO+q+e04LL05Hs=
|
|
||||||
github.com/flimzy/diff v0.1.6/go.mod h1:lFJtC7SPsK0EroDmGTSrdtWKAxOk3rO+q+e04LL05Hs=
|
|
||||||
github.com/flimzy/kivik v1.8.1/go.mod h1:S2aPycbG0eDFll4wgXt9uacSNkXISPufutnc9sv+mdA=
|
|
||||||
github.com/flimzy/testy v0.1.16/go.mod h1:3szguN8NXqgq9bt9Gu8TQVj698PJWmyx/VY1frwwKrM=
|
|
||||||
github.com/fortytw2/leaktest v1.2.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
|
|
||||||
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
|
|
||||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
|
||||||
github.com/fsouza/go-dockerclient v1.2.2/go.mod h1:KpcjM623fQYE9MZiTGzKhjfxXAV9wbyX2C1cyRHfhl0=
|
|
||||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
|
||||||
github.com/go-chi/chi v3.3.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
|
github.com/go-chi/chi v3.3.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
|
||||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
github.com/go-sql-driver/mysql v1.4.0 h1:7LxgVwFb2hIQtMm87NdgAVfXjnt4OePseqT1tKx+opk=
|
||||||
github.com/go-kivik/couchdb v1.8.1/go.mod h1:5XJRkAMpBlEVA4q0ktIZjUPYBjoBmRoiWvwUBzP3BOQ=
|
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||||
github.com/go-kivik/kivik v1.8.1/go.mod h1:nIuJ8z4ikBrVUSk3Ua8NoDqYKULPNjuddjqRvlSUyyQ=
|
|
||||||
github.com/go-kivik/kiviktest v1.1.2/go.mod h1:JdhVyzixoYhoIDUt6hRf1yAfYyaDa5/u9SDOindDkfQ=
|
|
||||||
github.com/go-kivik/pouchdb v1.3.5/go.mod h1:U+siUrqLCVxeMU3QjQTYIC3/F/e6EUKm+o5buJb7vpw=
|
|
||||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
|
||||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
|
||||||
github.com/go-sourcemap/sourcemap v2.1.2+incompatible h1:0b/xya7BKGhXuqFESKM4oIiRo9WOt2ebz7KxfreD6ug=
|
|
||||||
github.com/go-sourcemap/sourcemap v2.1.2+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
|
|
||||||
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
|
||||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
|
||||||
github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI=
|
|
||||||
github.com/gobuffalo/envy v1.7.1 h1:OQl5ys5MBea7OGCdvPbBJWRgnhC/fGona6QKfvFeau8=
|
|
||||||
github.com/gobuffalo/envy v1.7.1/go.mod h1:FurDp9+EDPE4aIUS3ZLyD+7/9fpx7YRt/ukY6jIHf0w=
|
|
||||||
github.com/gobuffalo/logger v1.0.1 h1:ZEgyRGgAm4ZAhAO45YXMs5Fp+bzGLESFewzAVBMKuTg=
|
|
||||||
github.com/gobuffalo/logger v1.0.1/go.mod h1:2zbswyIUa45I+c+FLXuWl9zSWEiVuthsk8ze5s8JvPs=
|
|
||||||
github.com/gobuffalo/packd v0.3.0 h1:eMwymTkA1uXsqxS0Tpoop3Lc0u3kTfiMBE6nKtQU4g4=
|
|
||||||
github.com/gobuffalo/packd v0.3.0/go.mod h1:zC7QkmNkYVGKPw4tHpBQ+ml7W/3tIebgeo1b36chA3Q=
|
|
||||||
github.com/gobuffalo/packr/v2 v2.7.1 h1:n3CIW5T17T8v4GGK5sWXLVWJhCz7b5aNLSxW6gYim4o=
|
|
||||||
github.com/gobuffalo/packr/v2 v2.7.1/go.mod h1:qYEvAazPaVxy7Y7KR0W8qYEE+RymX74kETFqjFoFlOc=
|
|
||||||
github.com/gogo/protobuf v1.0.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
github.com/gogo/protobuf v1.0.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
|
||||||
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
|
||||||
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
|
||||||
github.com/gogo/protobuf v1.3.0 h1:G8O7TerXerS4F6sx9OV7/nRfJdnXgHZu/S/7F2SN+UE=
|
|
||||||
github.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
|
|
||||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
|
||||||
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
|
||||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
|
||||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
|
||||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
|
||||||
github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
|
|
||||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
|
||||||
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
|
||||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
|
||||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
|
||||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
|
||||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
|
||||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
|
||||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
|
||||||
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
|
||||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
|
||||||
github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
|
|
||||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
|
||||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
|
||||||
github.com/gopherjs/gopherjs v0.0.0-20190411002643-bd77b112433e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
|
||||||
github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
|
||||||
github.com/gopherjs/jsbuiltin v0.0.0-20180426082241-50091555e127/go.mod h1:7X1acUyFRf+oVFTU6SWw9mnb57Vxn+Nbh8iPbKg95hs=
|
|
||||||
github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||||
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
|
||||||
github.com/gorilla/mux v1.6.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
github.com/gorilla/mux v1.6.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||||
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
|
||||||
github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc=
|
github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc=
|
||||||
github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||||
github.com/gorilla/websocket v1.2.0 h1:VJtLvh6VQym50czpZzx07z/kw9EgAxI3x1ZB8taTMQQ=
|
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
|
||||||
github.com/gorilla/websocket v1.2.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
|
|
||||||
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
|
||||||
github.com/gotestyourself/gotestyourself v2.2.0+incompatible/go.mod h1:zZKM6oeNM8k+FRljX1mnzVYeS8wiGgQyvST1/GafPbY=
|
|
||||||
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
|
||||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
|
|
||||||
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
|
||||||
github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo=
|
github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo=
|
||||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||||
github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU=
|
github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU=
|
||||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
|
||||||
github.com/hidal-go/hidalgo v0.0.0-20190814174001-42e03f3b5eaa h1:hBE4LGxApbZiV/3YoEPv7uYlUMWOogG1hwtkpiU87zQ=
|
|
||||||
github.com/hidal-go/hidalgo v0.0.0-20190814174001-42e03f3b5eaa/go.mod h1:bPkrxDlroXxigw8BMWTEPTv4W5/rQwNgg2BECXsgyX0=
|
|
||||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
|
||||||
github.com/imdario/mergo v0.3.7/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
|
||||||
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
|
|
||||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
|
||||||
github.com/jackc/fake v0.0.0-20150926172116-812a484cc733/go.mod h1:WrMFNQdiFJ80sQsxDoMokWK1W5TQtxBFNpzWTD84ibQ=
|
|
||||||
github.com/jackc/pgx v3.3.0+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I=
|
|
||||||
github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA=
|
github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA=
|
||||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||||
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
|
github.com/jmoiron/sqlx v1.2.0 h1:41Ip0zITnmWNR/vHV+S4m+VoUivnWY5E4OJfLZjCJMA=
|
||||||
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
|
github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks=
|
||||||
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
|
||||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
|
||||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
|
||||||
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
|
||||||
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
|
|
||||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
|
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
|
||||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
|
||||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw=
|
||||||
github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o=
|
||||||
github.com/linkeddata/gojsonld v0.0.0-20170418210642-4f5db6791326 h1:YP3lfXXYiQV5MKeUqVnxRP5uuMQTLPx+PGYm1UBoU98=
|
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk=
|
||||||
github.com/linkeddata/gojsonld v0.0.0-20170418210642-4f5db6791326/go.mod h1:nfqkuSNlsk1bvti/oa7TThx4KmRMBmSxf3okHI9wp3E=
|
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw=
|
||||||
|
github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A=
|
||||||
|
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||||
github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
|
github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
|
||||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
|
||||||
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
|
||||||
github.com/mailru/easyjson v0.0.0-20180730094502-03f2033d19d5/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
|
||||||
github.com/mailru/easyjson v0.0.0-20190403194419-1ea4449da983/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
|
||||||
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
|
||||||
github.com/matryer/moq v0.0.0-20200106131100-75d0ddfc0007 h1:reVOUXwnhsYv/8UqjvhrMOu5CNT9UapHFLbQ2JcXsmg=
|
github.com/matryer/moq v0.0.0-20200106131100-75d0ddfc0007 h1:reVOUXwnhsYv/8UqjvhrMOu5CNT9UapHFLbQ2JcXsmg=
|
||||||
github.com/matryer/moq v0.0.0-20200106131100-75d0ddfc0007/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ=
|
github.com/matryer/moq v0.0.0-20200106131100-75d0ddfc0007/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ=
|
||||||
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||||
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
|
github.com/mattn/go-sqlite3 v1.14.4 h1:4rQjbDxdu9fSgI/r3KN72G3c2goxknAqHHgPWWs8UlI=
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
github.com/mattn/go-sqlite3 v1.14.4/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
|
||||||
|
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||||
github.com/mitchellh/mapstructure v0.0.0-20180203102830-a4e142e9c047 h1:zCoDWFD5nrJJVjbXiDZcVhOBSzKn3o9LgRLLMRNuru8=
|
github.com/mitchellh/mapstructure v0.0.0-20180203102830-a4e142e9c047 h1:zCoDWFD5nrJJVjbXiDZcVhOBSzKn3o9LgRLLMRNuru8=
|
||||||
github.com/mitchellh/mapstructure v0.0.0-20180203102830-a4e142e9c047/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
github.com/mitchellh/mapstructure v0.0.0-20180203102830-a4e142e9c047/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||||
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
|
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
|
||||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
|
||||||
github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229 h1:E2B8qYyeSgv5MXpmzZXRNp8IAQ4vjxIjhpAf5hv/tAg=
|
github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229 h1:E2B8qYyeSgv5MXpmzZXRNp8IAQ4vjxIjhpAf5hv/tAg=
|
||||||
github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229/go.mod h1:0aYXnNPJ8l7uZxf45rWW1a/uME32OF0rhiYGNQ2oF2E=
|
github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229/go.mod h1:0aYXnNPJ8l7uZxf45rWW1a/uME32OF0rhiYGNQ2oF2E=
|
||||||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
|
||||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
|
||||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
|
||||||
github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
|
||||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
|
||||||
github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
|
||||||
github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
|
|
||||||
github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
|
|
||||||
github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
|
|
||||||
github.com/opencontainers/selinux v1.0.0/go.mod h1:+BLncwf63G4dgOzykXAxcmnFlUaOlkDdmw/CqsW6pjs=
|
|
||||||
github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74=
|
github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74=
|
||||||
github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
|
github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
|
||||||
github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
|
|
||||||
github.com/ory/dockertest v3.3.4+incompatible/go.mod h1:1vX4m9wsvi00u5bseYwXaSnhNrne+V0E6LAcBILJdPs=
|
|
||||||
github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
|
|
||||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
|
||||||
github.com/pelletier/go-toml v1.4.0/go.mod h1:PN7xzY2wHTK0K9p34ErDQMlFxa51Fk0OUruD3k1mMwo=
|
|
||||||
github.com/peterh/liner v0.0.0-20170317030525-88609521dc4b/go.mod h1:xIteQHvHuaLYG9IFj6mSxM0fCKrs34IrEQUhOYuGPHc=
|
|
||||||
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
|
||||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
|
||||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
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/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
|
||||||
github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
|
|
||||||
github.com/prometheus/client_golang v0.9.3 h1:9iH4JKXLzFbOAdtqv/a+j8aewx2Y8lAjAydhbaScPF8=
|
|
||||||
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
|
|
||||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
|
||||||
github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
|
||||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE=
|
|
||||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
|
||||||
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
|
||||||
github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
|
||||||
github.com/prometheus/common v0.4.0 h1:7etb9YClo3a6HjLzfl6rIQaU+FDfi0VSX39io3aQ+DM=
|
|
||||||
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
|
||||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
|
||||||
github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
|
||||||
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084 h1:sofwID9zm4tzrgykg80hfFph1mryUeLRsUfoocVVmRY=
|
|
||||||
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
|
||||||
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
|
||||||
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
|
||||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
|
||||||
github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
|
||||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
|
||||||
github.com/rogpeppe/go-internal v1.3.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
|
||||||
github.com/rogpeppe/go-internal v1.4.0/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
|
||||||
github.com/rogpeppe/go-internal v1.5.0 h1:Usqs0/lDK/NqTkvrmKSwA/3XkZAs7ZAW/eLeQ2MVBTw=
|
|
||||||
github.com/rogpeppe/go-internal v1.5.0/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
|
||||||
github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
|
github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
|
||||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
|
||||||
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
|
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
|
||||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
|
||||||
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
|
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
|
||||||
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||||
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
|
|
||||||
github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
|
github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
|
||||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
|
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
|
||||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||||
github.com/shurcooL/vfsgen v0.0.0-20180121065927-ffb13db8def0/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw=
|
github.com/shurcooL/vfsgen v0.0.0-20180121065927-ffb13db8def0/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw=
|
||||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
|
||||||
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
|
||||||
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
|
|
||||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
|
||||||
github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h1:SnhjPscd9TpLiy1LpzGSKh3bXCfxxXuqd9xmQJy3slM=
|
|
||||||
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
|
|
||||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
|
||||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
|
||||||
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
|
|
||||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
|
||||||
github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s=
|
|
||||||
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
|
|
||||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
|
||||||
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
|
|
||||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
|
||||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
|
||||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
|
||||||
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
|
||||||
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
|
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
|
||||||
github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
|
||||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
|
|
||||||
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
|
|
||||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
|
||||||
github.com/tylertreat/BoomFilters v0.0.0-20181028192813-611b3dbe80e8 h1:7X4KYG3guI2mPQGxm/ZNNsiu4BjKnef0KG0TblMC+Z8=
|
|
||||||
github.com/tylertreat/BoomFilters v0.0.0-20181028192813-611b3dbe80e8/go.mod h1:OYRfF6eb5wY9VRFkXJH8FFBi3plw2v+giaIu7P054pM=
|
|
||||||
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
|
|
||||||
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
|
||||||
github.com/urfave/cli/v2 v2.1.1 h1:Qt8FeAtxE/vfdrLmR3rxR6JRE0RoVmbXu8+6kZtYU4k=
|
github.com/urfave/cli/v2 v2.1.1 h1:Qt8FeAtxE/vfdrLmR3rxR6JRE0RoVmbXu8+6kZtYU4k=
|
||||||
github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
|
github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
|
||||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||||
@ -320,154 +101,42 @@ github.com/valyala/fasttemplate v1.0.1 h1:tY9CJiPnMXf1ERmG2EyK7gNUd+c6RKGD0IfU8W
|
|||||||
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
|
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
|
||||||
github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e h1:+w0Zm/9gaWpEAyDlU1eKOuk5twTjAjuevXqcJJw8hrg=
|
github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e h1:+w0Zm/9gaWpEAyDlU1eKOuk5twTjAjuevXqcJJw8hrg=
|
||||||
github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e/go.mod h1:/HUdMve7rvxZma+2ZELQeNh88+003LL7Pf/CZ089j8U=
|
github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e/go.mod h1:/HUdMve7rvxZma+2ZELQeNh88+003LL7Pf/CZ089j8U=
|
||||||
github.com/vektah/gqlparser/v2 v2.0.1 h1:xgl5abVnsd4hkN9rk65OJID9bfcLSMuTaTcZj777q1o=
|
github.com/vektah/gqlparser/v2 v2.1.0 h1:uiKJ+T5HMGGQM2kRKQ8Pxw8+Zq9qhhZhz/lieYvCMns=
|
||||||
github.com/vektah/gqlparser/v2 v2.0.1/go.mod h1:SyUiHgLATUR8BiYURfTirrTcGpcE+4XkV2se04Px1Ms=
|
github.com/vektah/gqlparser/v2 v2.1.0/go.mod h1:SyUiHgLATUR8BiYURfTirrTcGpcE+4XkV2se04Px1Ms=
|
||||||
github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I=
|
|
||||||
github.com/xdg/stringprep v1.0.0/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y=
|
|
||||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
|
||||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
|
||||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
|
||||||
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
|
||||||
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
|
||||||
go.mongodb.org/mongo-driver v1.0.4/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM=
|
|
||||||
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
|
|
||||||
go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
|
|
||||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
|
||||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
|
||||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
|
||||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
|
||||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
|
||||||
golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
|
||||||
golang.org/x/crypto v0.0.0-20191002192127-34f69633bfdc/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
|
||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8=
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8=
|
||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
|
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
|
||||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
|
||||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
|
||||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
|
||||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
|
||||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
|
||||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||||
golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
|
|
||||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
|
||||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
|
||||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20190628185345-da137c7871d7 h1:rTIdg5QFRR7XCaK4LCjBiPbx8j4DQRpdYMnGn/bJUEU=
|
|
||||||
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
|
||||||
golang.org/x/net v0.0.0-20200822124328-c89045814202 h1:VvcQYSHwXgi7W+TpUR6A9g6Up98WAHf3f/ulnJ62IyA=
|
|
||||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
|
||||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
|
||||||
golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/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-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY=
|
|
||||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 h1:qwRHBd0NqMbJxfbotnDhm2ByMI1Shq4Y6oRJo21SGJA=
|
|
||||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/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-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20190515120540-06a5c4944438/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20190614160838-b47fdc937951/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20191009170203-06d7bd2c5f4f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg=
|
golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg=
|
||||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
|
|
||||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
|
||||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
|
||||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
|
||||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
|
||||||
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
|
||||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
|
||||||
golang.org/x/tools v0.0.0-20190125232054-d66bd3c5d5a6/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20190125232054-d66bd3c5d5a6/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
|
||||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
|
||||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
|
||||||
golang.org/x/tools v0.0.0-20190515012406-7d7faa4812bd h1:oMEQDWVXVNpceQoVd1JN3CQ7LYJJzs5qWqZIUcxXHHw=
|
golang.org/x/tools v0.0.0-20190515012406-7d7faa4812bd h1:oMEQDWVXVNpceQoVd1JN3CQ7LYJJzs5qWqZIUcxXHHw=
|
||||||
golang.org/x/tools v0.0.0-20190515012406-7d7faa4812bd/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
golang.org/x/tools v0.0.0-20190515012406-7d7faa4812bd/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||||
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
|
||||||
golang.org/x/tools v0.0.0-20191004055002-72853e10c5a3/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
|
||||||
golang.org/x/tools v0.0.0-20191010075000-0337d82405ff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
|
||||||
golang.org/x/tools v0.0.0-20200114235610-7ae403b6b589 h1:rjUrONFu4kLchcZTfp3/96bR8bW8dIa8uz3cR5n0cgM=
|
golang.org/x/tools v0.0.0-20200114235610-7ae403b6b589 h1:rjUrONFu4kLchcZTfp3/96bR8bW8dIa8uz3cR5n0cgM=
|
||||||
golang.org/x/tools v0.0.0-20200114235610-7ae403b6b589/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
golang.org/x/tools v0.0.0-20200114235610-7ae403b6b589/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
golang.org/x/tools v0.0.0-20200925191224-5d1fdd8fa346 h1:hzJjkvxUIF3bSt+v8N5tBQNx/605vszZJ+3XsIamzZo=
|
|
||||||
golang.org/x/tools v0.0.0-20200925191224-5d1fdd8fa346/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
|
|
||||||
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-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc=
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||||
google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
|
|
||||||
google.golang.org/api v0.3.2/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
|
|
||||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
|
||||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
|
||||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
|
||||||
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
|
||||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
|
||||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
|
||||||
google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
|
||||||
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
|
||||||
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
|
|
||||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
|
||||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
|
||||||
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
|
||||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
|
||||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
|
||||||
gopkg.in/olivere/elastic.v5 v5.0.80/go.mod h1:uhHoB4o3bvX5sorxBU29rPcmBQdV2Qfg0FBrx5D6pV0=
|
|
||||||
gopkg.in/olivere/elastic.v5 v5.0.81/go.mod h1:uhHoB4o3bvX5sorxBU29rPcmBQdV2Qfg0FBrx5D6pV0=
|
|
||||||
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
|
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
|
||||||
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
|
||||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
||||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
|
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
|
||||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
|
||||||
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
|
||||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
|
||||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
|
||||||
sourcegraph.com/sourcegraph/appdash v0.0.0-20180110180208-2cc67fd64755/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU=
|
sourcegraph.com/sourcegraph/appdash v0.0.0-20180110180208-2cc67fd64755/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU=
|
||||||
sourcegraph.com/sourcegraph/appdash-data v0.0.0-20151005221446-73f23eafcf67/go.mod h1:L5q+DGLGOQFpo1snNEkLOJT2d1YTW66rWNzatr3He1k=
|
sourcegraph.com/sourcegraph/appdash-data v0.0.0-20151005221446-73f23eafcf67/go.mod h1:L5q+DGLGOQFpo1snNEkLOJT2d1YTW66rWNzatr3He1k=
|
||||||
|
11
gqlgen.yml
@ -42,13 +42,10 @@ omit_slice_element_pointers: true
|
|||||||
# The first line in each type will be used as defaults for resolver arguments and
|
# The first line in each type will be used as defaults for resolver arguments and
|
||||||
# modelgen, the others will be allowed when binding to fields. Configure them to
|
# modelgen, the others will be allowed when binding to fields. Configure them to
|
||||||
# your liking
|
# your liking
|
||||||
# models:
|
models:
|
||||||
# ID:
|
ID:
|
||||||
# model:
|
model:
|
||||||
# - github.com/99designs/gqlgen/graphql.ID
|
- github.com/99designs/gqlgen/graphql.Int64
|
||||||
# - github.com/99designs/gqlgen/graphql.Int
|
|
||||||
# - github.com/99designs/gqlgen/graphql.Int64
|
|
||||||
# - github.com/99designs/gqlgen/graphql.Int32
|
|
||||||
# Int:
|
# Int:
|
||||||
# model:
|
# model:
|
||||||
# - github.com/99designs/gqlgen/graphql.Int
|
# - github.com/99designs/gqlgen/graphql.Int
|
||||||
|
12
modd.conf
@ -1,12 +0,0 @@
|
|||||||
@cert = $HOME/.ssh/hetty_cert.pem
|
|
||||||
@key = $HOME/.ssh/hetty_key.pem
|
|
||||||
@db = hetty.bolt
|
|
||||||
@addr = :8080
|
|
||||||
|
|
||||||
**/*.go {
|
|
||||||
daemon +sigterm: go run ./cmd/hetty \
|
|
||||||
-cert=@cert \
|
|
||||||
-key=@key \
|
|
||||||
-db=@db \
|
|
||||||
-addr=@addr
|
|
||||||
}
|
|
2383
pkg/api/generated.go
@ -9,13 +9,25 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type ClearHTTPRequestLogResult struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CloseProjectResult struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeleteProjectResult struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
}
|
||||||
|
|
||||||
type HTTPHeader struct {
|
type HTTPHeader struct {
|
||||||
Key string `json:"key"`
|
Key string `json:"key"`
|
||||||
Value string `json:"value"`
|
Value string `json:"value"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type HTTPRequestLog struct {
|
type HTTPRequestLog struct {
|
||||||
ID string `json:"id"`
|
ID int64 `json:"id"`
|
||||||
URL string `json:"url"`
|
URL string `json:"url"`
|
||||||
Method HTTPMethod `json:"method"`
|
Method HTTPMethod `json:"method"`
|
||||||
Proto string `json:"proto"`
|
Proto string `json:"proto"`
|
||||||
@ -25,13 +37,50 @@ type HTTPRequestLog struct {
|
|||||||
Response *HTTPResponseLog `json:"response"`
|
Response *HTTPResponseLog `json:"response"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type HTTPRequestLogFilter struct {
|
||||||
|
OnlyInScope bool `json:"onlyInScope"`
|
||||||
|
SearchExpression *string `json:"searchExpression"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type HTTPRequestLogFilterInput struct {
|
||||||
|
OnlyInScope *bool `json:"onlyInScope"`
|
||||||
|
SearchExpression *string `json:"searchExpression"`
|
||||||
|
}
|
||||||
|
|
||||||
type HTTPResponseLog struct {
|
type HTTPResponseLog struct {
|
||||||
RequestID string `json:"requestId"`
|
RequestID int64 `json:"requestId"`
|
||||||
Proto string `json:"proto"`
|
Proto string `json:"proto"`
|
||||||
Status string `json:"status"`
|
StatusCode int `json:"statusCode"`
|
||||||
StatusCode int `json:"statusCode"`
|
StatusReason string `json:"statusReason"`
|
||||||
Body *string `json:"body"`
|
Body *string `json:"body"`
|
||||||
Headers []HTTPHeader `json:"headers"`
|
Headers []HTTPHeader `json:"headers"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Project struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
IsActive bool `json:"isActive"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ScopeHeader struct {
|
||||||
|
Key *string `json:"key"`
|
||||||
|
Value *string `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ScopeHeaderInput struct {
|
||||||
|
Key *string `json:"key"`
|
||||||
|
Value *string `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ScopeRule struct {
|
||||||
|
URL *string `json:"url"`
|
||||||
|
Header *ScopeHeader `json:"header"`
|
||||||
|
Body *string `json:"body"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ScopeRuleInput struct {
|
||||||
|
URL *string `json:"url"`
|
||||||
|
Header *ScopeHeaderInput `json:"header"`
|
||||||
|
Body *string `json:"body"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type HTTPMethod string
|
type HTTPMethod string
|
||||||
|
@ -5,22 +5,40 @@ package api
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/99designs/gqlgen/graphql"
|
||||||
|
"github.com/dstotijn/hetty/pkg/proj"
|
||||||
"github.com/dstotijn/hetty/pkg/reqlog"
|
"github.com/dstotijn/hetty/pkg/reqlog"
|
||||||
|
"github.com/dstotijn/hetty/pkg/scope"
|
||||||
|
"github.com/dstotijn/hetty/pkg/search"
|
||||||
|
"github.com/vektah/gqlparser/v2/gqlerror"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Resolver struct {
|
type Resolver struct {
|
||||||
RequestLogService *reqlog.Service
|
RequestLogService *reqlog.Service
|
||||||
|
ProjectService *proj.Service
|
||||||
|
ScopeService *scope.Scope
|
||||||
}
|
}
|
||||||
|
|
||||||
type queryResolver struct{ *Resolver }
|
type queryResolver struct{ *Resolver }
|
||||||
|
type mutationResolver struct{ *Resolver }
|
||||||
|
|
||||||
func (r *Resolver) Query() QueryResolver { return &queryResolver{r} }
|
func (r *Resolver) Query() QueryResolver { return &queryResolver{r} }
|
||||||
|
func (r *Resolver) Mutation() MutationResolver { return &mutationResolver{r} }
|
||||||
|
|
||||||
func (r *queryResolver) HTTPRequestLogs(ctx context.Context) ([]HTTPRequestLog, error) {
|
func (r *queryResolver) HTTPRequestLogs(ctx context.Context) ([]HTTPRequestLog, error) {
|
||||||
reqs, err := r.RequestLogService.FindAllRequests(ctx)
|
reqs, err := r.RequestLogService.FindRequests(ctx)
|
||||||
|
if err == proj.ErrNoProject {
|
||||||
|
return nil, &gqlerror.Error{
|
||||||
|
Path: graphql.GetPath(ctx),
|
||||||
|
Message: "No active project.",
|
||||||
|
Extensions: map[string]interface{}{
|
||||||
|
"code": "no_active_project",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("could not query repository for requests: %v", err)
|
return nil, fmt.Errorf("could not query repository for requests: %v", err)
|
||||||
}
|
}
|
||||||
@ -37,12 +55,8 @@ func (r *queryResolver) HTTPRequestLogs(ctx context.Context) ([]HTTPRequestLog,
|
|||||||
return logs, nil
|
return logs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *queryResolver) HTTPRequestLog(ctx context.Context, id string) (*HTTPRequestLog, error) {
|
func (r *queryResolver) HTTPRequestLog(ctx context.Context, id int64) (*HTTPRequestLog, error) {
|
||||||
reqLogID, err := uuid.Parse(id)
|
log, err := r.RequestLogService.FindRequestLogByID(ctx, id)
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("invalid id: %v", err)
|
|
||||||
}
|
|
||||||
log, err := r.RequestLogService.FindRequestLogByID(ctx, reqLogID)
|
|
||||||
if err == reqlog.ErrRequestNotFound {
|
if err == reqlog.ErrRequestNotFound {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
@ -59,18 +73,21 @@ func (r *queryResolver) HTTPRequestLog(ctx context.Context, id string) (*HTTPReq
|
|||||||
|
|
||||||
func parseRequestLog(req reqlog.Request) (HTTPRequestLog, error) {
|
func parseRequestLog(req reqlog.Request) (HTTPRequestLog, error) {
|
||||||
method := HTTPMethod(req.Request.Method)
|
method := HTTPMethod(req.Request.Method)
|
||||||
if !method.IsValid() {
|
if method != "" && !method.IsValid() {
|
||||||
return HTTPRequestLog{}, fmt.Errorf("request has invalid method: %v", method)
|
return HTTPRequestLog{}, fmt.Errorf("request has invalid method: %v", method)
|
||||||
}
|
}
|
||||||
|
|
||||||
log := HTTPRequestLog{
|
log := HTTPRequestLog{
|
||||||
ID: req.ID.String(),
|
ID: req.ID,
|
||||||
URL: req.Request.URL.String(),
|
|
||||||
Proto: req.Request.Proto,
|
Proto: req.Request.Proto,
|
||||||
Method: method,
|
Method: method,
|
||||||
Timestamp: req.Timestamp,
|
Timestamp: req.Timestamp,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if req.Request.URL != nil {
|
||||||
|
log.URL = req.Request.URL.String()
|
||||||
|
}
|
||||||
|
|
||||||
if len(req.Body) > 0 {
|
if len(req.Body) > 0 {
|
||||||
reqBody := string(req.Body)
|
reqBody := string(req.Body)
|
||||||
log.Body = &reqBody
|
log.Body = &reqBody
|
||||||
@ -90,11 +107,14 @@ func parseRequestLog(req reqlog.Request) (HTTPRequestLog, error) {
|
|||||||
|
|
||||||
if req.Response != nil {
|
if req.Response != nil {
|
||||||
log.Response = &HTTPResponseLog{
|
log.Response = &HTTPResponseLog{
|
||||||
RequestID: req.ID.String(),
|
RequestID: req.Response.RequestID,
|
||||||
Proto: req.Response.Response.Proto,
|
Proto: req.Response.Response.Proto,
|
||||||
Status: req.Response.Response.Status,
|
|
||||||
StatusCode: req.Response.Response.StatusCode,
|
StatusCode: req.Response.Response.StatusCode,
|
||||||
}
|
}
|
||||||
|
statusReasonSubs := strings.SplitN(req.Response.Response.Status, " ", 2)
|
||||||
|
if len(statusReasonSubs) == 2 {
|
||||||
|
log.Response.StatusReason = statusReasonSubs[1]
|
||||||
|
}
|
||||||
if len(req.Response.Body) > 0 {
|
if len(req.Response.Body) > 0 {
|
||||||
resBody := string(req.Response.Body)
|
resBody := string(req.Response.Body)
|
||||||
log.Response.Body = &resBody
|
log.Response.Body = &resBody
|
||||||
@ -114,3 +134,200 @@ func parseRequestLog(req reqlog.Request) (HTTPRequestLog, error) {
|
|||||||
|
|
||||||
return log, nil
|
return log, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *mutationResolver) OpenProject(ctx context.Context, name string) (*Project, error) {
|
||||||
|
p, err := r.ProjectService.Open(ctx, name)
|
||||||
|
if err == proj.ErrInvalidName {
|
||||||
|
return nil, gqlerror.Errorf("Project name must only contain alphanumeric or space chars.")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not open project: %v", err)
|
||||||
|
}
|
||||||
|
return &Project{
|
||||||
|
Name: p.Name,
|
||||||
|
IsActive: p.IsActive,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *queryResolver) ActiveProject(ctx context.Context) (*Project, error) {
|
||||||
|
p, err := r.ProjectService.ActiveProject()
|
||||||
|
if err == proj.ErrNoProject {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not open project: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Project{
|
||||||
|
Name: p.Name,
|
||||||
|
IsActive: p.IsActive,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *queryResolver) Projects(ctx context.Context) ([]Project, error) {
|
||||||
|
p, err := r.ProjectService.Projects()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not get projects: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
projects := make([]Project, len(p))
|
||||||
|
for i, proj := range p {
|
||||||
|
projects[i] = Project{
|
||||||
|
Name: proj.Name,
|
||||||
|
IsActive: proj.IsActive,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return projects, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *queryResolver) Scope(ctx context.Context) ([]ScopeRule, error) {
|
||||||
|
rules := r.ScopeService.Rules()
|
||||||
|
return scopeToScopeRules(rules), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func regexpToStringPtr(r *regexp.Regexp) *string {
|
||||||
|
if r == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
s := r.String()
|
||||||
|
return &s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *mutationResolver) CloseProject(ctx context.Context) (*CloseProjectResult, error) {
|
||||||
|
if err := r.ProjectService.Close(); err != nil {
|
||||||
|
return nil, fmt.Errorf("could not close project: %v", err)
|
||||||
|
}
|
||||||
|
return &CloseProjectResult{true}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *mutationResolver) DeleteProject(ctx context.Context, name string) (*DeleteProjectResult, error) {
|
||||||
|
if err := r.ProjectService.Delete(name); err != nil {
|
||||||
|
return nil, fmt.Errorf("could not delete project: %v", err)
|
||||||
|
}
|
||||||
|
return &DeleteProjectResult{
|
||||||
|
Success: true,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *mutationResolver) ClearHTTPRequestLog(ctx context.Context) (*ClearHTTPRequestLogResult, error) {
|
||||||
|
if err := r.RequestLogService.ClearRequests(ctx); err != nil {
|
||||||
|
return nil, fmt.Errorf("could not clear request log: %v", err)
|
||||||
|
}
|
||||||
|
return &ClearHTTPRequestLogResult{true}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *mutationResolver) SetScope(ctx context.Context, input []ScopeRuleInput) ([]ScopeRule, error) {
|
||||||
|
rules := make([]scope.Rule, len(input))
|
||||||
|
for i, rule := range input {
|
||||||
|
u, err := stringPtrToRegexp(rule.URL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid URL in scope rule: %v", err)
|
||||||
|
}
|
||||||
|
var headerKey, headerValue *regexp.Regexp
|
||||||
|
if rule.Header != nil {
|
||||||
|
headerKey, err = stringPtrToRegexp(rule.Header.Key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid header key in scope rule: %v", err)
|
||||||
|
}
|
||||||
|
headerValue, err = stringPtrToRegexp(rule.Header.Key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid header value in scope rule: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
body, err := stringPtrToRegexp(rule.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid body in scope rule: %v", err)
|
||||||
|
}
|
||||||
|
rules[i] = scope.Rule{
|
||||||
|
URL: u,
|
||||||
|
Header: scope.Header{
|
||||||
|
Key: headerKey,
|
||||||
|
Value: headerValue,
|
||||||
|
},
|
||||||
|
Body: body,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.ScopeService.SetRules(ctx, rules); err != nil {
|
||||||
|
return nil, fmt.Errorf("could not set scope: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return scopeToScopeRules(rules), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *queryResolver) HTTPRequestLogFilter(ctx context.Context) (*HTTPRequestLogFilter, error) {
|
||||||
|
return findReqFilterToHTTPReqLogFilter(r.RequestLogService.FindReqsFilter), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *mutationResolver) SetHTTPRequestLogFilter(
|
||||||
|
ctx context.Context,
|
||||||
|
input *HTTPRequestLogFilterInput,
|
||||||
|
) (*HTTPRequestLogFilter, error) {
|
||||||
|
filter, err := findRequestsFilterFromInput(input)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not parse request log filter: %v", err)
|
||||||
|
}
|
||||||
|
if err := r.RequestLogService.SetRequestLogFilter(ctx, filter); err != nil {
|
||||||
|
return nil, fmt.Errorf("could not set request log filter: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return findReqFilterToHTTPReqLogFilter(filter), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func stringPtrToRegexp(s *string) (*regexp.Regexp, error) {
|
||||||
|
if s == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return regexp.Compile(*s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func scopeToScopeRules(rules []scope.Rule) []ScopeRule {
|
||||||
|
scopeRules := make([]ScopeRule, len(rules))
|
||||||
|
for i, rule := range rules {
|
||||||
|
scopeRules[i].URL = regexpToStringPtr(rule.URL)
|
||||||
|
if rule.Header.Key != nil || rule.Header.Value != nil {
|
||||||
|
scopeRules[i].Header = &ScopeHeader{
|
||||||
|
Key: regexpToStringPtr(rule.Header.Key),
|
||||||
|
Value: regexpToStringPtr(rule.Header.Value),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
scopeRules[i].Body = regexpToStringPtr(rule.Body)
|
||||||
|
}
|
||||||
|
return scopeRules
|
||||||
|
}
|
||||||
|
|
||||||
|
func findRequestsFilterFromInput(input *HTTPRequestLogFilterInput) (filter reqlog.FindRequestsFilter, err error) {
|
||||||
|
if input == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if input.OnlyInScope != nil {
|
||||||
|
filter.OnlyInScope = *input.OnlyInScope
|
||||||
|
}
|
||||||
|
if input.SearchExpression != nil && *input.SearchExpression != "" {
|
||||||
|
expr, err := search.ParseQuery(*input.SearchExpression)
|
||||||
|
if err != nil {
|
||||||
|
return reqlog.FindRequestsFilter{}, fmt.Errorf("could not parse search query: %v", err)
|
||||||
|
}
|
||||||
|
filter.RawSearchExpr = *input.SearchExpression
|
||||||
|
filter.SearchExpr = expr
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func findReqFilterToHTTPReqLogFilter(findReqFilter reqlog.FindRequestsFilter) *HTTPRequestLogFilter {
|
||||||
|
empty := reqlog.FindRequestsFilter{}
|
||||||
|
if findReqFilter == empty {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
httpReqLogFilter := &HTTPRequestLogFilter{
|
||||||
|
OnlyInScope: findReqFilter.OnlyInScope,
|
||||||
|
}
|
||||||
|
|
||||||
|
if findReqFilter.RawSearchExpr != "" {
|
||||||
|
httpReqLogFilter.SearchExpression = &findReqFilter.RawSearchExpr
|
||||||
|
}
|
||||||
|
|
||||||
|
return httpReqLogFilter
|
||||||
|
}
|
||||||
|
@ -12,8 +12,8 @@ type HttpRequestLog {
|
|||||||
type HttpResponseLog {
|
type HttpResponseLog {
|
||||||
requestId: ID!
|
requestId: ID!
|
||||||
proto: String!
|
proto: String!
|
||||||
status: String!
|
|
||||||
statusCode: Int!
|
statusCode: Int!
|
||||||
|
statusReason: String!
|
||||||
body: String
|
body: String
|
||||||
headers: [HttpHeader!]!
|
headers: [HttpHeader!]!
|
||||||
}
|
}
|
||||||
@ -23,9 +23,73 @@ type HttpHeader {
|
|||||||
value: String!
|
value: String!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Project {
|
||||||
|
name: String!
|
||||||
|
isActive: Boolean!
|
||||||
|
}
|
||||||
|
|
||||||
|
type ScopeRule {
|
||||||
|
url: Regexp
|
||||||
|
header: ScopeHeader
|
||||||
|
body: Regexp
|
||||||
|
}
|
||||||
|
|
||||||
|
input ScopeRuleInput {
|
||||||
|
url: Regexp
|
||||||
|
header: ScopeHeaderInput
|
||||||
|
body: Regexp
|
||||||
|
}
|
||||||
|
|
||||||
|
type ScopeHeader {
|
||||||
|
key: Regexp
|
||||||
|
value: Regexp
|
||||||
|
}
|
||||||
|
|
||||||
|
input ScopeHeaderInput {
|
||||||
|
key: Regexp
|
||||||
|
value: Regexp
|
||||||
|
}
|
||||||
|
|
||||||
|
type CloseProjectResult {
|
||||||
|
success: Boolean!
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeleteProjectResult {
|
||||||
|
success: Boolean!
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClearHTTPRequestLogResult {
|
||||||
|
success: Boolean!
|
||||||
|
}
|
||||||
|
|
||||||
|
input HttpRequestLogFilterInput {
|
||||||
|
onlyInScope: Boolean
|
||||||
|
searchExpression: String
|
||||||
|
}
|
||||||
|
|
||||||
|
type HttpRequestLogFilter {
|
||||||
|
onlyInScope: Boolean!
|
||||||
|
searchExpression: String
|
||||||
|
}
|
||||||
|
|
||||||
type Query {
|
type Query {
|
||||||
httpRequestLog(id: ID!): HttpRequestLog
|
httpRequestLog(id: ID!): HttpRequestLog
|
||||||
httpRequestLogs: [HttpRequestLog!]!
|
httpRequestLogs: [HttpRequestLog!]!
|
||||||
|
httpRequestLogFilter: HttpRequestLogFilter
|
||||||
|
activeProject: Project
|
||||||
|
projects: [Project!]!
|
||||||
|
scope: [ScopeRule!]!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Mutation {
|
||||||
|
openProject(name: String!): Project
|
||||||
|
closeProject: CloseProjectResult!
|
||||||
|
deleteProject(name: String!): DeleteProjectResult!
|
||||||
|
clearHTTPRequestLog: ClearHTTPRequestLogResult!
|
||||||
|
setScope(scope: [ScopeRuleInput!]!): [ScopeRule!]!
|
||||||
|
setHttpRequestLogFilter(
|
||||||
|
filter: HttpRequestLogFilterInput
|
||||||
|
): HttpRequestLogFilter
|
||||||
}
|
}
|
||||||
|
|
||||||
enum HttpMethod {
|
enum HttpMethod {
|
||||||
@ -41,3 +105,4 @@ enum HttpMethod {
|
|||||||
}
|
}
|
||||||
|
|
||||||
scalar Time
|
scalar Time
|
||||||
|
scalar Regexp
|
||||||
|
@ -1,64 +0,0 @@
|
|||||||
package cayley
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"github.com/cayleygraph/cayley/clog"
|
|
||||||
"github.com/cayleygraph/cayley/graph"
|
|
||||||
hkv "github.com/hidal-go/hidalgo/kv"
|
|
||||||
"github.com/hidal-go/hidalgo/kv/bolt"
|
|
||||||
)
|
|
||||||
|
|
||||||
const Type = bolt.Name
|
|
||||||
|
|
||||||
func boltFilePath(path, filename string) string {
|
|
||||||
return filepath.Join(path, filename)
|
|
||||||
}
|
|
||||||
|
|
||||||
func boltCreate(path string, opt graph.Options) (hkv.KV, error) {
|
|
||||||
filename, err := opt.StringKey("filename", "indexes.bolt")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = os.MkdirAll(path, 0700)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
db, err := bolt.Open(boltFilePath(path, filename), nil)
|
|
||||||
if err != nil {
|
|
||||||
clog.Errorf("Error: couldn't create Bolt database: %v", err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return db, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func boltOpen(path string, opt graph.Options) (hkv.KV, error) {
|
|
||||||
filename, err := opt.StringKey("filename", "indexes.bolt")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
db, err := bolt.Open(boltFilePath(path, filename), nil)
|
|
||||||
if err != nil {
|
|
||||||
clog.Errorf("Error, couldn't open! %v", err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
bdb := db.DB()
|
|
||||||
bdb.NoSync, err = opt.BoolKey("nosync", false)
|
|
||||||
if err != nil {
|
|
||||||
db.Close()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
bdb.NoGrowSync = bdb.NoSync
|
|
||||||
if bdb.NoSync {
|
|
||||||
clog.Infof("Running in nosync mode")
|
|
||||||
}
|
|
||||||
|
|
||||||
return db, nil
|
|
||||||
}
|
|
@ -1,307 +0,0 @@
|
|||||||
package cayley
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"path"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/cayleygraph/cayley"
|
|
||||||
"github.com/cayleygraph/cayley/graph"
|
|
||||||
"github.com/cayleygraph/cayley/graph/kv"
|
|
||||||
"github.com/cayleygraph/cayley/schema"
|
|
||||||
"github.com/cayleygraph/quad"
|
|
||||||
"github.com/cayleygraph/quad/voc"
|
|
||||||
"github.com/cayleygraph/quad/voc/rdf"
|
|
||||||
"github.com/google/uuid"
|
|
||||||
|
|
||||||
"github.com/dstotijn/hetty/pkg/reqlog"
|
|
||||||
)
|
|
||||||
|
|
||||||
type HTTPRequest struct {
|
|
||||||
rdfType struct{} `quad:"@type > hy:HTTPRequest"`
|
|
||||||
ID quad.IRI `quad:"@id"`
|
|
||||||
Proto string `quad:"hy:proto"`
|
|
||||||
URL string `quad:"hy:url"`
|
|
||||||
Method string `quad:"hy:method"`
|
|
||||||
Body string `quad:"hy:body,optional"`
|
|
||||||
Headers []HTTPHeader `quad:"hy:header"`
|
|
||||||
Timestamp time.Time `quad:"hy:timestamp"`
|
|
||||||
Response *HTTPResponse `quad:"hy:request < *,optional"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type HTTPResponse struct {
|
|
||||||
rdfType struct{} `quad:"@type > hy:HTTPResponse"`
|
|
||||||
RequestID quad.IRI `quad:"hy:request"`
|
|
||||||
Proto string `quad:"hy:proto"`
|
|
||||||
Status string `quad:"hy:status"`
|
|
||||||
StatusCode int `quad:"hy:status_code"`
|
|
||||||
Headers []HTTPHeader `quad:"hy:header"`
|
|
||||||
Body string `quad:"hy:body,optional"`
|
|
||||||
Timestamp time.Time `quad:"hy:timestamp"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type HTTPHeader struct {
|
|
||||||
rdfType struct{} `quad:"@type > hy:HTTPHeader"`
|
|
||||||
Key string `quad:"hy:key"`
|
|
||||||
Value string `quad:"hy:value,optional"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Database struct {
|
|
||||||
store *cayley.Handle
|
|
||||||
schema *schema.Config
|
|
||||||
mu sync.Mutex
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
voc.RegisterPrefix("hy:", "https://hetty.xyz/")
|
|
||||||
schema.RegisterType(quad.IRI("hy:HTTPRequest"), HTTPRequest{})
|
|
||||||
schema.RegisterType(quad.IRI("hy:HTTPResponse"), HTTPResponse{})
|
|
||||||
schema.RegisterType(quad.IRI("hy:HTTPHeader"), HTTPHeader{})
|
|
||||||
|
|
||||||
kv.Register(Type, kv.Registration{
|
|
||||||
NewFunc: boltOpen,
|
|
||||||
InitFunc: boltCreate,
|
|
||||||
IsPersistent: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewDatabase(filename string) (*Database, error) {
|
|
||||||
dir, file := path.Split(filename)
|
|
||||||
if dir == "" {
|
|
||||||
dir = "."
|
|
||||||
}
|
|
||||||
opts := graph.Options{
|
|
||||||
"filename": file,
|
|
||||||
}
|
|
||||||
|
|
||||||
schemaCfg := schema.NewConfig()
|
|
||||||
schemaCfg.GenerateID = func(_ interface{}) quad.Value {
|
|
||||||
return quad.BNode(uuid.New().String())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize the database.
|
|
||||||
err := graph.InitQuadStore("bolt", dir, opts)
|
|
||||||
if err != nil && err != graph.ErrDatabaseExists {
|
|
||||||
return nil, fmt.Errorf("cayley: could not initialize database: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Open the database.
|
|
||||||
store, err := cayley.NewGraph("bolt", dir, opts)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("cayley: could not open database: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &Database{
|
|
||||||
store: store,
|
|
||||||
schema: schemaCfg,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (db *Database) Close() error {
|
|
||||||
return db.store.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (db *Database) FindAllRequestLogs(ctx context.Context) ([]reqlog.Request, error) {
|
|
||||||
db.mu.Lock()
|
|
||||||
defer db.mu.Unlock()
|
|
||||||
|
|
||||||
var reqLogs []reqlog.Request
|
|
||||||
var reqs []HTTPRequest
|
|
||||||
|
|
||||||
path := cayley.StartPath(db.store, quad.IRI("hy:HTTPRequest")).In(quad.IRI(rdf.Type))
|
|
||||||
err := path.Iterate(ctx).EachValue(db.store, func(v quad.Value) {
|
|
||||||
var req HTTPRequest
|
|
||||||
if err := db.schema.LoadToDepth(ctx, db.store, &req, -1, v); err != nil {
|
|
||||||
log.Printf("[ERROR] Could not load sub-graph for http requests: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
reqs = append(reqs, req)
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("cayley: could not iterate over http requests: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, req := range reqs {
|
|
||||||
reqLog, err := parseRequestQuads(req, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("cayley: could not parse request quads (id: %v): %v", req.ID, err)
|
|
||||||
}
|
|
||||||
reqLogs = append(reqLogs, reqLog)
|
|
||||||
}
|
|
||||||
|
|
||||||
// By default, all retrieved requests are ordered chronologically, oldest first.
|
|
||||||
// Reverse the order, so newest logs are first.
|
|
||||||
for i := len(reqLogs)/2 - 1; i >= 0; i-- {
|
|
||||||
opp := len(reqLogs) - 1 - i
|
|
||||||
reqLogs[i], reqLogs[opp] = reqLogs[opp], reqLogs[i]
|
|
||||||
}
|
|
||||||
|
|
||||||
return reqLogs, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (db *Database) FindRequestLogByID(ctx context.Context, id uuid.UUID) (reqlog.Request, error) {
|
|
||||||
db.mu.Lock()
|
|
||||||
defer db.mu.Unlock()
|
|
||||||
|
|
||||||
var req HTTPRequest
|
|
||||||
err := db.schema.LoadTo(ctx, db.store, &req, iriFromUUID(id))
|
|
||||||
if schema.IsNotFound(err) {
|
|
||||||
return reqlog.Request{}, reqlog.ErrRequestNotFound
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return reqlog.Request{}, fmt.Errorf("cayley: could not load value: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
reqLog, err := parseRequestQuads(req, nil)
|
|
||||||
if err != nil {
|
|
||||||
return reqlog.Request{}, fmt.Errorf("cayley: could not parse request log (id: %v): %v", req.ID, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return reqLog, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (db *Database) AddRequestLog(ctx context.Context, reqLog reqlog.Request) error {
|
|
||||||
db.mu.Lock()
|
|
||||||
defer db.mu.Unlock()
|
|
||||||
|
|
||||||
httpReq := HTTPRequest{
|
|
||||||
ID: iriFromUUID(reqLog.ID),
|
|
||||||
Proto: reqLog.Request.Proto,
|
|
||||||
Method: reqLog.Request.Method,
|
|
||||||
URL: reqLog.Request.URL.String(),
|
|
||||||
Headers: httpHeadersSliceFromMap(reqLog.Request.Header),
|
|
||||||
Body: string(reqLog.Body),
|
|
||||||
Timestamp: reqLog.Timestamp,
|
|
||||||
}
|
|
||||||
|
|
||||||
tx := cayley.NewTransaction()
|
|
||||||
qw := graph.NewTxWriter(tx, graph.Add)
|
|
||||||
|
|
||||||
_, err := db.schema.WriteAsQuads(qw, httpReq)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cayley: could not write quads: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := db.store.ApplyTransaction(tx); err != nil {
|
|
||||||
return fmt.Errorf("cayley: could not apply transaction: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (db *Database) AddResponseLog(ctx context.Context, resLog reqlog.Response) error {
|
|
||||||
db.mu.Lock()
|
|
||||||
defer db.mu.Unlock()
|
|
||||||
|
|
||||||
httpRes := HTTPResponse{
|
|
||||||
RequestID: iriFromUUID(resLog.RequestID),
|
|
||||||
Proto: resLog.Response.Proto,
|
|
||||||
Status: resLog.Response.Status,
|
|
||||||
StatusCode: resLog.Response.StatusCode,
|
|
||||||
Headers: httpHeadersSliceFromMap(resLog.Response.Header),
|
|
||||||
Body: string(resLog.Body),
|
|
||||||
Timestamp: resLog.Timestamp,
|
|
||||||
}
|
|
||||||
|
|
||||||
tx := cayley.NewTransaction()
|
|
||||||
qw := graph.NewTxWriter(tx, graph.Add)
|
|
||||||
|
|
||||||
_, err := db.schema.WriteAsQuads(qw, httpRes)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cayley: could not write response quads: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := db.store.ApplyTransaction(tx); err != nil {
|
|
||||||
return fmt.Errorf("cayley: could not apply transaction: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func iriFromUUID(id uuid.UUID) quad.IRI {
|
|
||||||
return quad.IRI("hy:" + id.String()).Full().Short()
|
|
||||||
}
|
|
||||||
|
|
||||||
func uuidFromIRI(iri quad.IRI) (uuid.UUID, error) {
|
|
||||||
iriString := iri.Short().String()
|
|
||||||
stripped := strings.TrimRight(strings.TrimLeft(iriString, "<hy:"), ">")
|
|
||||||
id, err := uuid.Parse(stripped)
|
|
||||||
if err != nil {
|
|
||||||
return uuid.Nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return id, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func httpHeadersSliceFromMap(hm http.Header) []HTTPHeader {
|
|
||||||
if hm == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
var hs []HTTPHeader
|
|
||||||
for key, values := range hm {
|
|
||||||
for _, value := range values {
|
|
||||||
hs = append(hs, HTTPHeader{Key: key, Value: value})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return hs
|
|
||||||
}
|
|
||||||
|
|
||||||
func httpHeadersMapFromSlice(hs []HTTPHeader) http.Header {
|
|
||||||
if hs == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
hm := make(http.Header)
|
|
||||||
for _, header := range hs {
|
|
||||||
hm.Add(header.Key, header.Value)
|
|
||||||
}
|
|
||||||
return hm
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseRequestQuads(req HTTPRequest, _ *HTTPResponse) (reqlog.Request, error) {
|
|
||||||
reqID, err := uuidFromIRI(req.ID)
|
|
||||||
if err != nil {
|
|
||||||
return reqlog.Request{}, fmt.Errorf("cannot parse request id: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
u, err := url.Parse(req.URL)
|
|
||||||
if err != nil {
|
|
||||||
return reqlog.Request{}, fmt.Errorf("cannot parse request url: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
reqLog := reqlog.Request{
|
|
||||||
ID: reqID,
|
|
||||||
Request: http.Request{
|
|
||||||
Method: req.Method,
|
|
||||||
URL: u,
|
|
||||||
Proto: req.Proto,
|
|
||||||
Header: httpHeadersMapFromSlice(req.Headers),
|
|
||||||
},
|
|
||||||
Timestamp: req.Timestamp,
|
|
||||||
}
|
|
||||||
if req.Body != "" {
|
|
||||||
reqLog.Body = []byte(reqLog.Body)
|
|
||||||
}
|
|
||||||
|
|
||||||
if req.Response != nil {
|
|
||||||
reqLog.Response = &reqlog.Response{
|
|
||||||
RequestID: reqID,
|
|
||||||
Response: http.Response{
|
|
||||||
Proto: req.Response.Proto,
|
|
||||||
Status: req.Response.Status,
|
|
||||||
StatusCode: req.Response.StatusCode,
|
|
||||||
Header: httpHeadersMapFromSlice(req.Response.Headers),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
if req.Response.Body != "" {
|
|
||||||
reqLog.Response.Body = []byte(req.Response.Body)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return reqLog, nil
|
|
||||||
}
|
|