mirror of
https://github.com/dstotijn/hetty.git
synced 2025-07-01 18:47:29 -04:00
Compare commits
6 Commits
Author | SHA1 | Date | |
---|---|---|---|
f6789fa245 | |||
81fbfe4cb3 | |||
6931d63250 | |||
71e87d3cd3 | |||
0ffbb618fa | |||
c01f190fc8 |
@ -1,4 +1,3 @@
|
|||||||
**/rice-box.go
|
|
||||||
/admin/.env
|
/admin/.env
|
||||||
/admin/.next
|
/admin/.next
|
||||||
/admin/dist
|
/admin/dist
|
||||||
|
31
.github/workflows/build_run.yml
vendored
31
.github/workflows/build_run.yml
vendored
@ -1,31 +0,0 @@
|
|||||||
name: Build Frontend, Backend and Run
|
|
||||||
|
|
||||||
on: [pull_request]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
name: Build Images and Deploy Example WebApp
|
|
||||||
runs-on: ubuntu-20.04
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
with:
|
|
||||||
submodules: true
|
|
||||||
|
|
||||||
- name: Test Yarn
|
|
||||||
uses:
|
|
||||||
node_version: '14.x'
|
|
||||||
run: |-
|
|
||||||
cd admin && \
|
|
||||||
yarn install && \
|
|
||||||
yarn export
|
|
||||||
|
|
||||||
- name: Test make
|
|
||||||
run: |-
|
|
||||||
cd .. && \
|
|
||||||
make build
|
|
||||||
|
|
||||||
- name: Run
|
|
||||||
run: |-
|
|
||||||
./hetty
|
|
14
.github/workflows/dockerimage.yml
vendored
14
.github/workflows/dockerimage.yml
vendored
@ -1,14 +0,0 @@
|
|||||||
name: Docker Image CI
|
|
||||||
|
|
||||||
on: [pull_request]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
|
|
||||||
build:
|
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- name: Build the Docker image
|
|
||||||
run: docker build . --file Dockerfile --tag hetty:$(date +%s)
|
|
24
.github/workflows/go.yml
vendored
24
.github/workflows/go.yml
vendored
@ -1,24 +0,0 @@
|
|||||||
name: Go
|
|
||||||
on: [pull_request]
|
|
||||||
jobs:
|
|
||||||
|
|
||||||
build:
|
|
||||||
name: Build
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
|
|
||||||
- name: Set up Go 1.15
|
|
||||||
uses: actions/setup-go@v1
|
|
||||||
with:
|
|
||||||
go-version: 1.15
|
|
||||||
id: go
|
|
||||||
|
|
||||||
- name: Check out code into the Go module directory
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
|
|
||||||
- name: Get dependencies
|
|
||||||
run: |
|
|
||||||
go get -v -t -d ./...
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
run: go build -v ./cmd/hetty
|
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,10 +1,7 @@
|
|||||||
.release-env
|
.release-env
|
||||||
.vscode
|
.vscode
|
||||||
**/rice-box.go
|
**/rice-box.go
|
||||||
sqlite3_mod_regexp.dylib
|
|
||||||
dist
|
dist
|
||||||
hetty
|
hetty
|
||||||
hetty.bolt
|
|
||||||
hetty.db
|
|
||||||
*.pem
|
*.pem
|
||||||
*.test
|
*.test
|
||||||
|
@ -1,11 +1,7 @@
|
|||||||
env:
|
env:
|
||||||
- GO111MODULE=on
|
- GO111MODULE=on
|
||||||
- CGO_ENABLED=1
|
- CGO_ENABLED=1
|
||||||
- CGO_CFLAGS=-I/go/pkg/mod/github.com/mattn/go-sqlite3@v1.14.4
|
|
||||||
before:
|
|
||||||
hooks:
|
|
||||||
- make clean
|
|
||||||
- make embed
|
|
||||||
builds:
|
builds:
|
||||||
- id: hetty-darwin-amd64
|
- id: hetty-darwin-amd64
|
||||||
main: ./cmd/hetty
|
main: ./cmd/hetty
|
||||||
@ -16,48 +12,50 @@ builds:
|
|||||||
env:
|
env:
|
||||||
- CC=o64-clang
|
- CC=o64-clang
|
||||||
- CXX=o64-clang++
|
- CXX=o64-clang++
|
||||||
- CGO_LDFLAGS=-Wl,-undefined,dynamic_lookup
|
|
||||||
flags:
|
flags:
|
||||||
- -mod=readonly
|
- -mod=readonly
|
||||||
ldflags:
|
|
||||||
- id: hetty-linux-amd64
|
- id: hetty-linux-amd64
|
||||||
main: ./cmd/hetty
|
main: ./cmd/hetty
|
||||||
goarch:
|
goarch:
|
||||||
- amd64
|
- amd64
|
||||||
goos:
|
goos:
|
||||||
- linux
|
- linux
|
||||||
|
flags:
|
||||||
|
- -mod=readonly
|
||||||
|
|
||||||
|
- id: hetty-windows-amd64
|
||||||
|
main: ./cmd/hetty
|
||||||
|
goarch:
|
||||||
|
- amd64
|
||||||
|
goos:
|
||||||
|
- windows
|
||||||
env:
|
env:
|
||||||
- CGO_CFLAGS=-I/go/pkg/mod/github.com/mattn/go-sqlite3@v1.14.4
|
- CC=x86_64-w64-mingw32-gcc
|
||||||
- CGO_LDFLAGS=-Wl,--unresolved-symbols=ignore-in-object-files
|
- CXX=x86_64-w64-mingw32-g++
|
||||||
flags:
|
flags:
|
||||||
- -mod=readonly
|
- -mod=readonly
|
||||||
ldflags:
|
ldflags:
|
||||||
# - id: hetty-windows-amd64
|
- -buildmode=exe
|
||||||
# main: ./cmd/hetty
|
|
||||||
# goarch:
|
|
||||||
# - amd64
|
|
||||||
# goos:
|
|
||||||
# - windows
|
|
||||||
# env:
|
|
||||||
# - CC=x86_64-w64-mingw32-gcc
|
|
||||||
# - CXX=x86_64-w64-mingw32-g++
|
|
||||||
# - CGO_CFLAGS=-I/go/pkg/mod/github.com/mattn/go-sqlite3@v1.14.4
|
|
||||||
# - CGO_LDFLAGS=-Wl,--unresolved-symbols=ignore-in-object-files # Not working :(
|
|
||||||
# flags:
|
|
||||||
# - -mod=readonly
|
|
||||||
# ldflags:
|
|
||||||
# - -buildmode=exe
|
|
||||||
archives:
|
archives:
|
||||||
- replacements:
|
-
|
||||||
darwin: macOS
|
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:
|
||||||
|
12
Dockerfile
12
Dockerfile
@ -2,17 +2,15 @@ ARG GO_VERSION=1.15
|
|||||||
ARG CGO_ENABLED=1
|
ARG CGO_ENABLED=1
|
||||||
ARG NODE_VERSION=14.11
|
ARG NODE_VERSION=14.11
|
||||||
|
|
||||||
FROM golang:${GO_VERSION} AS go-builder
|
FROM golang:${GO_VERSION}-alpine AS go-builder
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
RUN apt-get update && \
|
RUN apk add --no-cache build-base
|
||||||
apt-get install -y build-essential
|
|
||||||
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
|
||||||
ENV CGO_CFLAGS=-I/go/pkg/mod/github.com/mattn/go-sqlite3@v1.14.4
|
RUN rm -f cmd/hetty/rice-box.go
|
||||||
ENV CGO_LDFLAGS=-Wl,--unresolved-symbols=ignore-in-object-files
|
RUN go build ./cmd/hetty
|
||||||
RUN go build -o hetty ./cmd/hetty
|
|
||||||
|
|
||||||
FROM node:${NODE_VERSION}-alpine AS node-builder
|
FROM node:${NODE_VERSION}-alpine AS node-builder
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
@ -22,7 +20,7 @@ COPY admin/ .
|
|||||||
ENV NEXT_TELEMETRY_DISABLED=1
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
RUN yarn run export
|
RUN yarn run export
|
||||||
|
|
||||||
FROM debian:buster-slim
|
FROM alpine:3.12
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY --from=go-builder /app/hetty .
|
COPY --from=go-builder /app/hetty .
|
||||||
COPY --from=node-builder /app/dist admin
|
COPY --from=node-builder /app/dist admin
|
||||||
|
32
Makefile
32
Makefile
@ -1,36 +1,26 @@
|
|||||||
PACKAGE_NAME := github.com/dstotijn/hetty
|
PACKAGE_NAME := github.com/dstotijn/hetty
|
||||||
GOLANG_CROSS_VERSION ?= v1.15.2
|
GOLANG_CROSS_VERSION ?= v1.15.2
|
||||||
|
|
||||||
setup:
|
|
||||||
go mod download
|
|
||||||
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
|
|
||||||
env CGO_ENABLED=1 CGO_CFLAGS="-DUSE_LIBSQLITE3" CGO_LDFLAGS="-Wl,-undefined,dynamic_lookup" \
|
|
||||||
go build -tags libsqlite3 ./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
|
|
||||||
|
|
||||||
release-dry-run:
|
|
||||||
@docker run \
|
@docker run \
|
||||||
--rm \
|
--rm \
|
||||||
-v `pwd`:/go/src/$(PACKAGE_NAME) \
|
-v `pwd`:/go/src/$(PACKAGE_NAME) \
|
||||||
-v `pwd`/admin/dist:/go/src/$(PACKAGE_NAME)/admin/dist \
|
|
||||||
-w /go/src/$(PACKAGE_NAME) \
|
-w /go/src/$(PACKAGE_NAME) \
|
||||||
troian/golang-cross:${GOLANG_CROSS_VERSION} \
|
troian/golang-cross:${GOLANG_CROSS_VERSION} \
|
||||||
--rm-dist --skip-validate --skip-publish
|
--rm-dist --skip-validate --skip-publish
|
||||||
.PHONY: release-dry-run
|
|
||||||
|
|
||||||
release:
|
.PHONY: release
|
||||||
|
release: embed
|
||||||
@if [ ! -f ".release-env" ]; then \
|
@if [ ! -f ".release-env" ]; then \
|
||||||
echo "\033[91mFile \`.release-env\` is missing.\033[0m";\
|
echo "\033[91mFile \`.release-env\` is missing.\033[0m";\
|
||||||
exit 1;\
|
exit 1;\
|
||||||
@ -38,9 +28,7 @@ release:
|
|||||||
@docker run \
|
@docker run \
|
||||||
--rm \
|
--rm \
|
||||||
-v `pwd`:/go/src/$(PACKAGE_NAME) \
|
-v `pwd`:/go/src/$(PACKAGE_NAME) \
|
||||||
-v `pwd`/admin/dist:/go/src/$(PACKAGE_NAME)/admin/dist \
|
|
||||||
-w /go/src/$(PACKAGE_NAME) \
|
-w /go/src/$(PACKAGE_NAME) \
|
||||||
--env-file .release-env \
|
--env-file .release-env \
|
||||||
troian/golang-cross:${GOLANG_CROSS_VERSION} \
|
troian/golang-cross:${GOLANG_CROSS_VERSION} \
|
||||||
release --rm-dist
|
release --rm-dist
|
||||||
.PHONY: release
|
|
161
README.md
161
README.md
@ -1,70 +1,89 @@
|
|||||||
<img src="https://i.imgur.com/AT71SBq.png" width="346" />
|
<h1>
|
||||||
|
<a href="https://github.com/dstotijn/hetty">
|
||||||
|
<img src="https://hetty.xyz/assets/logo.png" width="293">
|
||||||
|
</a>
|
||||||
|
</h1>
|
||||||
|
|
||||||
> Hetty is an HTTP toolkit for security research. It aims to become an open source
|
[](https://github.com/dstotijn/hetty/releases/latest)
|
||||||
> alternative to commercial software like Burp Suite Pro, with powerful features
|

|
||||||
> tailored to the needs of the infosec and bug bounty community.
|
[](https://github.com/dstotijn/hetty/blob/master/LICENSE)
|
||||||
|
|
||||||
<img src="https://i.imgur.com/ZZ6o83X.png">
|
**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/to do
|
<img src="https://hetty.xyz/assets/hetty_v0.2.0_header.png">
|
||||||
|
|
||||||
- [x] HTTP man-in-the-middle (MITM) proxy and GraphQL server.
|
## Features
|
||||||
- [x] Web interface (Next.js) with proxy log viewer.
|
|
||||||
- [x] Add scope support to the proxy.
|
- Man-in-the-middle (MITM) HTTP/1.1 proxy with logs
|
||||||
- [ ] Full text search (with regex) in proxy log viewer.
|
- Project based database storage (SQLite)
|
||||||
- [x] Project management.
|
- Scope support
|
||||||
- [ ] Sender module for sending manual HTTP requests, either from scratch or based
|
- Headless management API using GraphQL
|
||||||
off requests from the proxy log.
|
- Embedded web interface (Next.js)
|
||||||
- [ ] Attacker module for automated sending of HTTP requests. Leverage the concurrency
|
|
||||||
features of Go and its `net/http` package to make it blazingly fast.
|
ℹ️ 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.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
Hetty is packaged on GitHub as a single binary, with the web interface resources
|
Hetty compiles to a self-contained binary, with an embedded SQLite database
|
||||||
embedded.
|
and web based admin interface.
|
||||||
|
|
||||||
👉 You can find downloads for Linux, macOS and Windows on the [releases page](https://github.com/dstotijn/hetty/releases).
|
### Install pre-built release (recommended)
|
||||||
|
|
||||||
### Alternatives:
|
👉 Downloads for Linux, macOS and Windows are available on the [releases page](https://github.com/dstotijn/hetty/releases).
|
||||||
|
|
||||||
**Build from source**
|
### 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:
|
||||||
|
|
||||||
```
|
```
|
||||||
$ GO111MODULE=auto go get -u -v github.com/dstotijn/hetty/cmd/hetty
|
$ git clone git@github.com:dstotijn/hetty.git
|
||||||
|
$ cd hetty
|
||||||
|
$ make build
|
||||||
```
|
```
|
||||||
|
|
||||||
Then export the Next.js frontend app:
|
### 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:
|
||||||
|
|
||||||
```
|
```
|
||||||
$ cd admin
|
$ mkdir -p $HOME/.hetty
|
||||||
$ yarn install
|
$ docker run -v $HOME/.hetty:/root/.hetty -p 8080:8080 dstotijn/hetty
|
||||||
$ yarn export
|
|
||||||
```
|
|
||||||
|
|
||||||
This will ensure a folder `./admin/dist` exists.
|
|
||||||
Then, you can bundle the frontend app using `rice`.
|
|
||||||
The easiest way to do this is via a supplied `Makefile` command in the root of
|
|
||||||
the project:
|
|
||||||
|
|
||||||
```
|
|
||||||
make build
|
|
||||||
```
|
|
||||||
|
|
||||||
**Docker**
|
|
||||||
|
|
||||||
Alternatively, you can run Hetty via Docker. See: [`dstotijn/hetty`](https://hub.docker.com/r/dstotijn/hetty)
|
|
||||||
on Docker Hub.
|
|
||||||
|
|
||||||
```
|
|
||||||
$ docker run -v $HOME/.hetty:/root/.hetty -p 127.0.0.1:8080:8080 dstotijn/hetty
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
Hetty is packaged as a single binary, with the web interface resources embedded.
|
When Hetty is run, by default it listens on `:8080` and is accessible via
|
||||||
When the program is run, it listens by default on `:8080` and is accessible via
|
|
||||||
http://localhost:8080. Depending on incoming HTTP requests, it either acts as a
|
http://localhost:8080. Depending on incoming HTTP requests, it either acts as a
|
||||||
MITM proxy, or it serves the GraphQL API and web interface (Next.js).
|
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
|
$ hetty -h
|
||||||
@ -81,6 +100,16 @@ Usage of ./hetty:
|
|||||||
Projects directory path (default "~/.hetty/projects")
|
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
|
## Certificate Setup and Installation
|
||||||
|
|
||||||
In order for Hetty to proxy requests going to HTTPS endpoints, a root CA certificate for
|
In order for Hetty to proxy requests going to HTTPS endpoints, a root CA certificate for
|
||||||
@ -163,38 +192,40 @@ _more information on how to update the system to trust your self-signed certific
|
|||||||
|
|
||||||
## Vision and roadmap
|
## Vision and roadmap
|
||||||
|
|
||||||
The project has just gotten underway, and as such I haven’t had time yet to do a
|
|
||||||
write-up on its mission and roadmap. A short summary/braindump:
|
|
||||||
|
|
||||||
- Fast core/engine, built with Go, with a minimal memory footprint.
|
- Fast core/engine, built with Go, with a minimal memory footprint.
|
||||||
- GraphQL server to interact with the backend.
|
- Easy to use admin interface, built with Next.js and Material UI.
|
||||||
- Easy to use web 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
|
- Extensibility is top of mind. All modules are written as Go packages, to
|
||||||
be used by the main `hetty` program, but also usable as libraries for other software.
|
be used by Hetty, but also as libraries by other software.
|
||||||
Aside from the GraphQL server, it should (eventually) be possible to also use
|
- Pluggable architecture for MITM proxy, projects, scope. It should be possible.
|
||||||
it as a CLI tool.
|
to build a plugin system in the (near) future.
|
||||||
- Pluggable architecture for the MITM proxy and future modules, making it
|
- Based on feedback and real-world usage of pentesters and bug bounty hunters.
|
||||||
possible for hook into the core engine.
|
- Aim for a relatively small core feature set that the majority of security researchers need.
|
||||||
- Talk to the community, and focus on the features that the majority.
|
|
||||||
Less features means less code to maintain.
|
|
||||||
|
|
||||||
## Status
|
## Support
|
||||||
|
|
||||||
The project is currently under active development. Please star/follow and check
|
Use [issues](https://github.com/dstotijn/hetty/issues) for bug reports and
|
||||||
back soon. 🤗
|
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
|
## Contributing
|
||||||
|
|
||||||
Please see the [Contribution Guidelines](CONTRIBUTING.md) for details.
|
Want to contribute? Great! Please check the [Contribution Guidelines](CONTRIBUTING.md)
|
||||||
|
for details.
|
||||||
|
|
||||||
## Acknowledgements
|
## Acknowledgements
|
||||||
|
|
||||||
Thanks to the [Hacker101 community on Discord](https://www.hacker101.com/discord)
|
- Thanks to the [Hacker101 community on Discord](https://www.hacker101.com/discord)
|
||||||
for all the encouragement to actually start building this thing!
|
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
|
## License
|
||||||
|
|
||||||
[MIT](LICENSE)
|
[MIT License](LICENSE)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -1,4 +0,0 @@
|
|||||||
@env = CGO_CFLAGS=-DUSE_LIBSQLITE3 CGO_LDFLAGS=-Wl,-undefined,dynamic_lookup
|
|
||||||
**/*.go {
|
|
||||||
daemon +sigterm: @env go run -tags libsqlite3 ./cmd/hetty
|
|
||||||
}
|
|
@ -1,759 +0,0 @@
|
|||||||
/*
|
|
||||||
** 2012-11-13
|
|
||||||
**
|
|
||||||
** The author disclaims copyright to this source code. In place of
|
|
||||||
** a legal notice, here is a blessing:
|
|
||||||
**
|
|
||||||
** May you do good and not evil.
|
|
||||||
** May you find forgiveness for yourself and forgive others.
|
|
||||||
** May you share freely, never taking more than you give.
|
|
||||||
**
|
|
||||||
******************************************************************************
|
|
||||||
**
|
|
||||||
** The code in this file implements a compact but reasonably
|
|
||||||
** efficient regular-expression matcher for posix extended regular
|
|
||||||
** expressions against UTF8 text.
|
|
||||||
**
|
|
||||||
** This file is an SQLite extension. It registers a single function
|
|
||||||
** named "regexp(A,B)" where A is the regular expression and B is the
|
|
||||||
** string to be matched. By registering this function, SQLite will also
|
|
||||||
** then implement the "B regexp A" operator. Note that with the function
|
|
||||||
** the regular expression comes first, but with the operator it comes
|
|
||||||
** second.
|
|
||||||
**
|
|
||||||
** The following regular expression syntax is supported:
|
|
||||||
**
|
|
||||||
** X* zero or more occurrences of X
|
|
||||||
** X+ one or more occurrences of X
|
|
||||||
** X? zero or one occurrences of X
|
|
||||||
** X{p,q} between p and q occurrences of X
|
|
||||||
** (X) match X
|
|
||||||
** X|Y X or Y
|
|
||||||
** ^X X occurring at the beginning of the string
|
|
||||||
** X$ X occurring at the end of the string
|
|
||||||
** . Match any single character
|
|
||||||
** \c Character c where c is one of \{}()[]|*+?.
|
|
||||||
** \c C-language escapes for c in afnrtv. ex: \t or \n
|
|
||||||
** \uXXXX Where XXXX is exactly 4 hex digits, unicode value XXXX
|
|
||||||
** \xXX Where XX is exactly 2 hex digits, unicode value XX
|
|
||||||
** [abc] Any single character from the set abc
|
|
||||||
** [^abc] Any single character not in the set abc
|
|
||||||
** [a-z] Any single character in the range a-z
|
|
||||||
** [^a-z] Any single character not in the range a-z
|
|
||||||
** \b Word boundary
|
|
||||||
** \w Word character. [A-Za-z0-9_]
|
|
||||||
** \W Non-word character
|
|
||||||
** \d Digit
|
|
||||||
** \D Non-digit
|
|
||||||
** \s Whitespace character
|
|
||||||
** \S Non-whitespace character
|
|
||||||
**
|
|
||||||
** A nondeterministic finite automaton (NFA) is used for matching, so the
|
|
||||||
** performance is bounded by O(N*M) where N is the size of the regular
|
|
||||||
** expression and M is the size of the input string. The matcher never
|
|
||||||
** exhibits exponential behavior. Note that the X{p,q} operator expands
|
|
||||||
** to p copies of X following by q-p copies of X? and that the size of the
|
|
||||||
** regular expression in the O(N*M) performance bound is computed after
|
|
||||||
** this expansion.
|
|
||||||
*/
|
|
||||||
#include <string.h>
|
|
||||||
#include <stdlib.h>
|
|
||||||
#include "sqlite3ext.h"
|
|
||||||
SQLITE_EXTENSION_INIT1
|
|
||||||
|
|
||||||
/*
|
|
||||||
** The following #defines change the names of some functions implemented in
|
|
||||||
** this file to prevent name collisions with C-library functions of the
|
|
||||||
** same name.
|
|
||||||
*/
|
|
||||||
#define re_match sqlite3re_match
|
|
||||||
#define re_compile sqlite3re_compile
|
|
||||||
#define re_free sqlite3re_free
|
|
||||||
|
|
||||||
/* The end-of-input character */
|
|
||||||
#define RE_EOF 0 /* End of input */
|
|
||||||
|
|
||||||
/* The NFA is implemented as sequence of opcodes taken from the following
|
|
||||||
** set. Each opcode has a single integer argument.
|
|
||||||
*/
|
|
||||||
#define RE_OP_MATCH 1 /* Match the one character in the argument */
|
|
||||||
#define RE_OP_ANY 2 /* Match any one character. (Implements ".") */
|
|
||||||
#define RE_OP_ANYSTAR 3 /* Special optimized version of .* */
|
|
||||||
#define RE_OP_FORK 4 /* Continue to both next and opcode at iArg */
|
|
||||||
#define RE_OP_GOTO 5 /* Jump to opcode at iArg */
|
|
||||||
#define RE_OP_ACCEPT 6 /* Halt and indicate a successful match */
|
|
||||||
#define RE_OP_CC_INC 7 /* Beginning of a [...] character class */
|
|
||||||
#define RE_OP_CC_EXC 8 /* Beginning of a [^...] character class */
|
|
||||||
#define RE_OP_CC_VALUE 9 /* Single value in a character class */
|
|
||||||
#define RE_OP_CC_RANGE 10 /* Range of values in a character class */
|
|
||||||
#define RE_OP_WORD 11 /* Perl word character [A-Za-z0-9_] */
|
|
||||||
#define RE_OP_NOTWORD 12 /* Not a perl word character */
|
|
||||||
#define RE_OP_DIGIT 13 /* digit: [0-9] */
|
|
||||||
#define RE_OP_NOTDIGIT 14 /* Not a digit */
|
|
||||||
#define RE_OP_SPACE 15 /* space: [ \t\n\r\v\f] */
|
|
||||||
#define RE_OP_NOTSPACE 16 /* Not a digit */
|
|
||||||
#define RE_OP_BOUNDARY 17 /* Boundary between word and non-word */
|
|
||||||
|
|
||||||
/* Each opcode is a "state" in the NFA */
|
|
||||||
typedef unsigned short ReStateNumber;
|
|
||||||
|
|
||||||
/* Because this is an NFA and not a DFA, multiple states can be active at
|
|
||||||
** once. An instance of the following object records all active states in
|
|
||||||
** the NFA. The implementation is optimized for the common case where the
|
|
||||||
** number of actives states is small.
|
|
||||||
*/
|
|
||||||
typedef struct ReStateSet {
|
|
||||||
unsigned nState; /* Number of current states */
|
|
||||||
ReStateNumber *aState; /* Current states */
|
|
||||||
} ReStateSet;
|
|
||||||
|
|
||||||
/* An input string read one character at a time.
|
|
||||||
*/
|
|
||||||
typedef struct ReInput ReInput;
|
|
||||||
struct ReInput {
|
|
||||||
const unsigned char *z; /* All text */
|
|
||||||
int i; /* Next byte to read */
|
|
||||||
int mx; /* EOF when i>=mx */
|
|
||||||
};
|
|
||||||
|
|
||||||
/* A compiled NFA (or an NFA that is in the process of being compiled) is
|
|
||||||
** an instance of the following object.
|
|
||||||
*/
|
|
||||||
typedef struct ReCompiled ReCompiled;
|
|
||||||
struct ReCompiled {
|
|
||||||
ReInput sIn; /* Regular expression text */
|
|
||||||
const char *zErr; /* Error message to return */
|
|
||||||
char *aOp; /* Operators for the virtual machine */
|
|
||||||
int *aArg; /* Arguments to each operator */
|
|
||||||
unsigned (*xNextChar)(ReInput*); /* Next character function */
|
|
||||||
unsigned char zInit[12]; /* Initial text to match */
|
|
||||||
int nInit; /* Number of characters in zInit */
|
|
||||||
unsigned nState; /* Number of entries in aOp[] and aArg[] */
|
|
||||||
unsigned nAlloc; /* Slots allocated for aOp[] and aArg[] */
|
|
||||||
};
|
|
||||||
|
|
||||||
/* Add a state to the given state set if it is not already there */
|
|
||||||
static void re_add_state(ReStateSet *pSet, int newState){
|
|
||||||
unsigned i;
|
|
||||||
for(i=0; i<pSet->nState; i++) if( pSet->aState[i]==newState ) return;
|
|
||||||
pSet->aState[pSet->nState++] = (ReStateNumber)newState;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Extract the next unicode character from *pzIn and return it. Advance
|
|
||||||
** *pzIn to the first byte past the end of the character returned. To
|
|
||||||
** be clear: this routine converts utf8 to unicode. This routine is
|
|
||||||
** optimized for the common case where the next character is a single byte.
|
|
||||||
*/
|
|
||||||
static unsigned re_next_char(ReInput *p){
|
|
||||||
unsigned c;
|
|
||||||
if( p->i>=p->mx ) return 0;
|
|
||||||
c = p->z[p->i++];
|
|
||||||
if( c>=0x80 ){
|
|
||||||
if( (c&0xe0)==0xc0 && p->i<p->mx && (p->z[p->i]&0xc0)==0x80 ){
|
|
||||||
c = (c&0x1f)<<6 | (p->z[p->i++]&0x3f);
|
|
||||||
if( c<0x80 ) c = 0xfffd;
|
|
||||||
}else if( (c&0xf0)==0xe0 && p->i+1<p->mx && (p->z[p->i]&0xc0)==0x80
|
|
||||||
&& (p->z[p->i+1]&0xc0)==0x80 ){
|
|
||||||
c = (c&0x0f)<<12 | ((p->z[p->i]&0x3f)<<6) | (p->z[p->i+1]&0x3f);
|
|
||||||
p->i += 2;
|
|
||||||
if( c<=0x7ff || (c>=0xd800 && c<=0xdfff) ) c = 0xfffd;
|
|
||||||
}else if( (c&0xf8)==0xf0 && p->i+3<p->mx && (p->z[p->i]&0xc0)==0x80
|
|
||||||
&& (p->z[p->i+1]&0xc0)==0x80 && (p->z[p->i+2]&0xc0)==0x80 ){
|
|
||||||
c = (c&0x07)<<18 | ((p->z[p->i]&0x3f)<<12) | ((p->z[p->i+1]&0x3f)<<6)
|
|
||||||
| (p->z[p->i+2]&0x3f);
|
|
||||||
p->i += 3;
|
|
||||||
if( c<=0xffff || c>0x10ffff ) c = 0xfffd;
|
|
||||||
}else{
|
|
||||||
c = 0xfffd;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return c;
|
|
||||||
}
|
|
||||||
static unsigned re_next_char_nocase(ReInput *p){
|
|
||||||
unsigned c = re_next_char(p);
|
|
||||||
if( c>='A' && c<='Z' ) c += 'a' - 'A';
|
|
||||||
return c;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Return true if c is a perl "word" character: [A-Za-z0-9_] */
|
|
||||||
static int re_word_char(int c){
|
|
||||||
return (c>='0' && c<='9') || (c>='a' && c<='z')
|
|
||||||
|| (c>='A' && c<='Z') || c=='_';
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Return true if c is a "digit" character: [0-9] */
|
|
||||||
static int re_digit_char(int c){
|
|
||||||
return (c>='0' && c<='9');
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Return true if c is a perl "space" character: [ \t\r\n\v\f] */
|
|
||||||
static int re_space_char(int c){
|
|
||||||
return c==' ' || c=='\t' || c=='\n' || c=='\r' || c=='\v' || c=='\f';
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Run a compiled regular expression on the zero-terminated input
|
|
||||||
** string zIn[]. Return true on a match and false if there is no match.
|
|
||||||
*/
|
|
||||||
static int re_match(ReCompiled *pRe, const unsigned char *zIn, int nIn){
|
|
||||||
ReStateSet aStateSet[2], *pThis, *pNext;
|
|
||||||
ReStateNumber aSpace[100];
|
|
||||||
ReStateNumber *pToFree;
|
|
||||||
unsigned int i = 0;
|
|
||||||
unsigned int iSwap = 0;
|
|
||||||
int c = RE_EOF+1;
|
|
||||||
int cPrev = 0;
|
|
||||||
int rc = 0;
|
|
||||||
ReInput in;
|
|
||||||
|
|
||||||
in.z = zIn;
|
|
||||||
in.i = 0;
|
|
||||||
in.mx = nIn>=0 ? nIn : (int)strlen((char const*)zIn);
|
|
||||||
|
|
||||||
/* Look for the initial prefix match, if there is one. */
|
|
||||||
if( pRe->nInit ){
|
|
||||||
unsigned char x = pRe->zInit[0];
|
|
||||||
while( in.i+pRe->nInit<=in.mx
|
|
||||||
&& (zIn[in.i]!=x ||
|
|
||||||
strncmp((const char*)zIn+in.i, (const char*)pRe->zInit, pRe->nInit)!=0)
|
|
||||||
){
|
|
||||||
in.i++;
|
|
||||||
}
|
|
||||||
if( in.i+pRe->nInit>in.mx ) return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if( pRe->nState<=(sizeof(aSpace)/(sizeof(aSpace[0])*2)) ){
|
|
||||||
pToFree = 0;
|
|
||||||
aStateSet[0].aState = aSpace;
|
|
||||||
}else{
|
|
||||||
pToFree = sqlite3_malloc64( sizeof(ReStateNumber)*2*pRe->nState );
|
|
||||||
if( pToFree==0 ) return -1;
|
|
||||||
aStateSet[0].aState = pToFree;
|
|
||||||
}
|
|
||||||
aStateSet[1].aState = &aStateSet[0].aState[pRe->nState];
|
|
||||||
pNext = &aStateSet[1];
|
|
||||||
pNext->nState = 0;
|
|
||||||
re_add_state(pNext, 0);
|
|
||||||
while( c!=RE_EOF && pNext->nState>0 ){
|
|
||||||
cPrev = c;
|
|
||||||
c = pRe->xNextChar(&in);
|
|
||||||
pThis = pNext;
|
|
||||||
pNext = &aStateSet[iSwap];
|
|
||||||
iSwap = 1 - iSwap;
|
|
||||||
pNext->nState = 0;
|
|
||||||
for(i=0; i<pThis->nState; i++){
|
|
||||||
int x = pThis->aState[i];
|
|
||||||
switch( pRe->aOp[x] ){
|
|
||||||
case RE_OP_MATCH: {
|
|
||||||
if( pRe->aArg[x]==c ) re_add_state(pNext, x+1);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case RE_OP_ANY: {
|
|
||||||
re_add_state(pNext, x+1);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case RE_OP_WORD: {
|
|
||||||
if( re_word_char(c) ) re_add_state(pNext, x+1);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case RE_OP_NOTWORD: {
|
|
||||||
if( !re_word_char(c) ) re_add_state(pNext, x+1);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case RE_OP_DIGIT: {
|
|
||||||
if( re_digit_char(c) ) re_add_state(pNext, x+1);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case RE_OP_NOTDIGIT: {
|
|
||||||
if( !re_digit_char(c) ) re_add_state(pNext, x+1);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case RE_OP_SPACE: {
|
|
||||||
if( re_space_char(c) ) re_add_state(pNext, x+1);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case RE_OP_NOTSPACE: {
|
|
||||||
if( !re_space_char(c) ) re_add_state(pNext, x+1);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case RE_OP_BOUNDARY: {
|
|
||||||
if( re_word_char(c)!=re_word_char(cPrev) ) re_add_state(pThis, x+1);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case RE_OP_ANYSTAR: {
|
|
||||||
re_add_state(pNext, x);
|
|
||||||
re_add_state(pThis, x+1);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case RE_OP_FORK: {
|
|
||||||
re_add_state(pThis, x+pRe->aArg[x]);
|
|
||||||
re_add_state(pThis, x+1);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case RE_OP_GOTO: {
|
|
||||||
re_add_state(pThis, x+pRe->aArg[x]);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case RE_OP_ACCEPT: {
|
|
||||||
rc = 1;
|
|
||||||
goto re_match_end;
|
|
||||||
}
|
|
||||||
case RE_OP_CC_INC:
|
|
||||||
case RE_OP_CC_EXC: {
|
|
||||||
int j = 1;
|
|
||||||
int n = pRe->aArg[x];
|
|
||||||
int hit = 0;
|
|
||||||
for(j=1; j>0 && j<n; j++){
|
|
||||||
if( pRe->aOp[x+j]==RE_OP_CC_VALUE ){
|
|
||||||
if( pRe->aArg[x+j]==c ){
|
|
||||||
hit = 1;
|
|
||||||
j = -1;
|
|
||||||
}
|
|
||||||
}else{
|
|
||||||
if( pRe->aArg[x+j]<=c && pRe->aArg[x+j+1]>=c ){
|
|
||||||
hit = 1;
|
|
||||||
j = -1;
|
|
||||||
}else{
|
|
||||||
j++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if( pRe->aOp[x]==RE_OP_CC_EXC ) hit = !hit;
|
|
||||||
if( hit ) re_add_state(pNext, x+n);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for(i=0; i<pNext->nState; i++){
|
|
||||||
if( pRe->aOp[pNext->aState[i]]==RE_OP_ACCEPT ){ rc = 1; break; }
|
|
||||||
}
|
|
||||||
re_match_end:
|
|
||||||
sqlite3_free(pToFree);
|
|
||||||
return rc;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Resize the opcode and argument arrays for an RE under construction.
|
|
||||||
*/
|
|
||||||
static int re_resize(ReCompiled *p, int N){
|
|
||||||
char *aOp;
|
|
||||||
int *aArg;
|
|
||||||
aOp = sqlite3_realloc64(p->aOp, N*sizeof(p->aOp[0]));
|
|
||||||
if( aOp==0 ) return 1;
|
|
||||||
p->aOp = aOp;
|
|
||||||
aArg = sqlite3_realloc64(p->aArg, N*sizeof(p->aArg[0]));
|
|
||||||
if( aArg==0 ) return 1;
|
|
||||||
p->aArg = aArg;
|
|
||||||
p->nAlloc = N;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Insert a new opcode and argument into an RE under construction. The
|
|
||||||
** insertion point is just prior to existing opcode iBefore.
|
|
||||||
*/
|
|
||||||
static int re_insert(ReCompiled *p, int iBefore, int op, int arg){
|
|
||||||
int i;
|
|
||||||
if( p->nAlloc<=p->nState && re_resize(p, p->nAlloc*2) ) return 0;
|
|
||||||
for(i=p->nState; i>iBefore; i--){
|
|
||||||
p->aOp[i] = p->aOp[i-1];
|
|
||||||
p->aArg[i] = p->aArg[i-1];
|
|
||||||
}
|
|
||||||
p->nState++;
|
|
||||||
p->aOp[iBefore] = (char)op;
|
|
||||||
p->aArg[iBefore] = arg;
|
|
||||||
return iBefore;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Append a new opcode and argument to the end of the RE under construction.
|
|
||||||
*/
|
|
||||||
static int re_append(ReCompiled *p, int op, int arg){
|
|
||||||
return re_insert(p, p->nState, op, arg);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Make a copy of N opcodes starting at iStart onto the end of the RE
|
|
||||||
** under construction.
|
|
||||||
*/
|
|
||||||
static void re_copy(ReCompiled *p, int iStart, int N){
|
|
||||||
if( p->nState+N>=p->nAlloc && re_resize(p, p->nAlloc*2+N) ) return;
|
|
||||||
memcpy(&p->aOp[p->nState], &p->aOp[iStart], N*sizeof(p->aOp[0]));
|
|
||||||
memcpy(&p->aArg[p->nState], &p->aArg[iStart], N*sizeof(p->aArg[0]));
|
|
||||||
p->nState += N;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Return true if c is a hexadecimal digit character: [0-9a-fA-F]
|
|
||||||
** If c is a hex digit, also set *pV = (*pV)*16 + valueof(c). If
|
|
||||||
** c is not a hex digit *pV is unchanged.
|
|
||||||
*/
|
|
||||||
static int re_hex(int c, int *pV){
|
|
||||||
if( c>='0' && c<='9' ){
|
|
||||||
c -= '0';
|
|
||||||
}else if( c>='a' && c<='f' ){
|
|
||||||
c -= 'a' - 10;
|
|
||||||
}else if( c>='A' && c<='F' ){
|
|
||||||
c -= 'A' - 10;
|
|
||||||
}else{
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
*pV = (*pV)*16 + (c & 0xff);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* A backslash character has been seen, read the next character and
|
|
||||||
** return its interpretation.
|
|
||||||
*/
|
|
||||||
static unsigned re_esc_char(ReCompiled *p){
|
|
||||||
static const char zEsc[] = "afnrtv\\()*.+?[$^{|}]";
|
|
||||||
static const char zTrans[] = "\a\f\n\r\t\v";
|
|
||||||
int i, v = 0;
|
|
||||||
char c;
|
|
||||||
if( p->sIn.i>=p->sIn.mx ) return 0;
|
|
||||||
c = p->sIn.z[p->sIn.i];
|
|
||||||
if( c=='u' && p->sIn.i+4<p->sIn.mx ){
|
|
||||||
const unsigned char *zIn = p->sIn.z + p->sIn.i;
|
|
||||||
if( re_hex(zIn[1],&v)
|
|
||||||
&& re_hex(zIn[2],&v)
|
|
||||||
&& re_hex(zIn[3],&v)
|
|
||||||
&& re_hex(zIn[4],&v)
|
|
||||||
){
|
|
||||||
p->sIn.i += 5;
|
|
||||||
return v;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if( c=='x' && p->sIn.i+2<p->sIn.mx ){
|
|
||||||
const unsigned char *zIn = p->sIn.z + p->sIn.i;
|
|
||||||
if( re_hex(zIn[1],&v)
|
|
||||||
&& re_hex(zIn[2],&v)
|
|
||||||
){
|
|
||||||
p->sIn.i += 3;
|
|
||||||
return v;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for(i=0; zEsc[i] && zEsc[i]!=c; i++){}
|
|
||||||
if( zEsc[i] ){
|
|
||||||
if( i<6 ) c = zTrans[i];
|
|
||||||
p->sIn.i++;
|
|
||||||
}else{
|
|
||||||
p->zErr = "unknown \\ escape";
|
|
||||||
}
|
|
||||||
return c;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Forward declaration */
|
|
||||||
static const char *re_subcompile_string(ReCompiled*);
|
|
||||||
|
|
||||||
/* Peek at the next byte of input */
|
|
||||||
static unsigned char rePeek(ReCompiled *p){
|
|
||||||
return p->sIn.i<p->sIn.mx ? p->sIn.z[p->sIn.i] : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Compile RE text into a sequence of opcodes. Continue up to the
|
|
||||||
** first unmatched ")" character, then return. If an error is found,
|
|
||||||
** return a pointer to the error message string.
|
|
||||||
*/
|
|
||||||
static const char *re_subcompile_re(ReCompiled *p){
|
|
||||||
const char *zErr;
|
|
||||||
int iStart, iEnd, iGoto;
|
|
||||||
iStart = p->nState;
|
|
||||||
zErr = re_subcompile_string(p);
|
|
||||||
if( zErr ) return zErr;
|
|
||||||
while( rePeek(p)=='|' ){
|
|
||||||
iEnd = p->nState;
|
|
||||||
re_insert(p, iStart, RE_OP_FORK, iEnd + 2 - iStart);
|
|
||||||
iGoto = re_append(p, RE_OP_GOTO, 0);
|
|
||||||
p->sIn.i++;
|
|
||||||
zErr = re_subcompile_string(p);
|
|
||||||
if( zErr ) return zErr;
|
|
||||||
p->aArg[iGoto] = p->nState - iGoto;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Compile an element of regular expression text (anything that can be
|
|
||||||
** an operand to the "|" operator). Return NULL on success or a pointer
|
|
||||||
** to the error message if there is a problem.
|
|
||||||
*/
|
|
||||||
static const char *re_subcompile_string(ReCompiled *p){
|
|
||||||
int iPrev = -1;
|
|
||||||
int iStart;
|
|
||||||
unsigned c;
|
|
||||||
const char *zErr;
|
|
||||||
while( (c = p->xNextChar(&p->sIn))!=0 ){
|
|
||||||
iStart = p->nState;
|
|
||||||
switch( c ){
|
|
||||||
case '|':
|
|
||||||
case '$':
|
|
||||||
case ')': {
|
|
||||||
p->sIn.i--;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
case '(': {
|
|
||||||
zErr = re_subcompile_re(p);
|
|
||||||
if( zErr ) return zErr;
|
|
||||||
if( rePeek(p)!=')' ) return "unmatched '('";
|
|
||||||
p->sIn.i++;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case '.': {
|
|
||||||
if( rePeek(p)=='*' ){
|
|
||||||
re_append(p, RE_OP_ANYSTAR, 0);
|
|
||||||
p->sIn.i++;
|
|
||||||
}else{
|
|
||||||
re_append(p, RE_OP_ANY, 0);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case '*': {
|
|
||||||
if( iPrev<0 ) return "'*' without operand";
|
|
||||||
re_insert(p, iPrev, RE_OP_GOTO, p->nState - iPrev + 1);
|
|
||||||
re_append(p, RE_OP_FORK, iPrev - p->nState + 1);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case '+': {
|
|
||||||
if( iPrev<0 ) return "'+' without operand";
|
|
||||||
re_append(p, RE_OP_FORK, iPrev - p->nState);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case '?': {
|
|
||||||
if( iPrev<0 ) return "'?' without operand";
|
|
||||||
re_insert(p, iPrev, RE_OP_FORK, p->nState - iPrev+1);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case '{': {
|
|
||||||
int m = 0, n = 0;
|
|
||||||
int sz, j;
|
|
||||||
if( iPrev<0 ) return "'{m,n}' without operand";
|
|
||||||
while( (c=rePeek(p))>='0' && c<='9' ){ m = m*10 + c - '0'; p->sIn.i++; }
|
|
||||||
n = m;
|
|
||||||
if( c==',' ){
|
|
||||||
p->sIn.i++;
|
|
||||||
n = 0;
|
|
||||||
while( (c=rePeek(p))>='0' && c<='9' ){ n = n*10 + c-'0'; p->sIn.i++; }
|
|
||||||
}
|
|
||||||
if( c!='}' ) return "unmatched '{'";
|
|
||||||
if( n>0 && n<m ) return "n less than m in '{m,n}'";
|
|
||||||
p->sIn.i++;
|
|
||||||
sz = p->nState - iPrev;
|
|
||||||
if( m==0 ){
|
|
||||||
if( n==0 ) return "both m and n are zero in '{m,n}'";
|
|
||||||
re_insert(p, iPrev, RE_OP_FORK, sz+1);
|
|
||||||
n--;
|
|
||||||
}else{
|
|
||||||
for(j=1; j<m; j++) re_copy(p, iPrev, sz);
|
|
||||||
}
|
|
||||||
for(j=m; j<n; j++){
|
|
||||||
re_append(p, RE_OP_FORK, sz+1);
|
|
||||||
re_copy(p, iPrev, sz);
|
|
||||||
}
|
|
||||||
if( n==0 && m>0 ){
|
|
||||||
re_append(p, RE_OP_FORK, -sz);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case '[': {
|
|
||||||
int iFirst = p->nState;
|
|
||||||
if( rePeek(p)=='^' ){
|
|
||||||
re_append(p, RE_OP_CC_EXC, 0);
|
|
||||||
p->sIn.i++;
|
|
||||||
}else{
|
|
||||||
re_append(p, RE_OP_CC_INC, 0);
|
|
||||||
}
|
|
||||||
while( (c = p->xNextChar(&p->sIn))!=0 ){
|
|
||||||
if( c=='[' && rePeek(p)==':' ){
|
|
||||||
return "POSIX character classes not supported";
|
|
||||||
}
|
|
||||||
if( c=='\\' ) c = re_esc_char(p);
|
|
||||||
if( rePeek(p)=='-' ){
|
|
||||||
re_append(p, RE_OP_CC_RANGE, c);
|
|
||||||
p->sIn.i++;
|
|
||||||
c = p->xNextChar(&p->sIn);
|
|
||||||
if( c=='\\' ) c = re_esc_char(p);
|
|
||||||
re_append(p, RE_OP_CC_RANGE, c);
|
|
||||||
}else{
|
|
||||||
re_append(p, RE_OP_CC_VALUE, c);
|
|
||||||
}
|
|
||||||
if( rePeek(p)==']' ){ p->sIn.i++; break; }
|
|
||||||
}
|
|
||||||
if( c==0 ) return "unclosed '['";
|
|
||||||
p->aArg[iFirst] = p->nState - iFirst;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case '\\': {
|
|
||||||
int specialOp = 0;
|
|
||||||
switch( rePeek(p) ){
|
|
||||||
case 'b': specialOp = RE_OP_BOUNDARY; break;
|
|
||||||
case 'd': specialOp = RE_OP_DIGIT; break;
|
|
||||||
case 'D': specialOp = RE_OP_NOTDIGIT; break;
|
|
||||||
case 's': specialOp = RE_OP_SPACE; break;
|
|
||||||
case 'S': specialOp = RE_OP_NOTSPACE; break;
|
|
||||||
case 'w': specialOp = RE_OP_WORD; break;
|
|
||||||
case 'W': specialOp = RE_OP_NOTWORD; break;
|
|
||||||
}
|
|
||||||
if( specialOp ){
|
|
||||||
p->sIn.i++;
|
|
||||||
re_append(p, specialOp, 0);
|
|
||||||
}else{
|
|
||||||
c = re_esc_char(p);
|
|
||||||
re_append(p, RE_OP_MATCH, c);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
re_append(p, RE_OP_MATCH, c);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
iPrev = iStart;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Free and reclaim all the memory used by a previously compiled
|
|
||||||
** regular expression. Applications should invoke this routine once
|
|
||||||
** for every call to re_compile() to avoid memory leaks.
|
|
||||||
*/
|
|
||||||
static void re_free(ReCompiled *pRe){
|
|
||||||
if( pRe ){
|
|
||||||
sqlite3_free(pRe->aOp);
|
|
||||||
sqlite3_free(pRe->aArg);
|
|
||||||
sqlite3_free(pRe);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
** Compile a textual regular expression in zIn[] into a compiled regular
|
|
||||||
** expression suitable for us by re_match() and return a pointer to the
|
|
||||||
** compiled regular expression in *ppRe. Return NULL on success or an
|
|
||||||
** error message if something goes wrong.
|
|
||||||
*/
|
|
||||||
static const char *re_compile(ReCompiled **ppRe, const char *zIn, int noCase){
|
|
||||||
ReCompiled *pRe;
|
|
||||||
const char *zErr;
|
|
||||||
int i, j;
|
|
||||||
|
|
||||||
*ppRe = 0;
|
|
||||||
pRe = sqlite3_malloc( sizeof(*pRe) );
|
|
||||||
if( pRe==0 ){
|
|
||||||
return "out of memory";
|
|
||||||
}
|
|
||||||
memset(pRe, 0, sizeof(*pRe));
|
|
||||||
pRe->xNextChar = noCase ? re_next_char_nocase : re_next_char;
|
|
||||||
if( re_resize(pRe, 30) ){
|
|
||||||
re_free(pRe);
|
|
||||||
return "out of memory";
|
|
||||||
}
|
|
||||||
if( zIn[0]=='^' ){
|
|
||||||
zIn++;
|
|
||||||
}else{
|
|
||||||
re_append(pRe, RE_OP_ANYSTAR, 0);
|
|
||||||
}
|
|
||||||
pRe->sIn.z = (unsigned char*)zIn;
|
|
||||||
pRe->sIn.i = 0;
|
|
||||||
pRe->sIn.mx = (int)strlen(zIn);
|
|
||||||
zErr = re_subcompile_re(pRe);
|
|
||||||
if( zErr ){
|
|
||||||
re_free(pRe);
|
|
||||||
return zErr;
|
|
||||||
}
|
|
||||||
if( rePeek(pRe)=='$' && pRe->sIn.i+1>=pRe->sIn.mx ){
|
|
||||||
re_append(pRe, RE_OP_MATCH, RE_EOF);
|
|
||||||
re_append(pRe, RE_OP_ACCEPT, 0);
|
|
||||||
*ppRe = pRe;
|
|
||||||
}else if( pRe->sIn.i>=pRe->sIn.mx ){
|
|
||||||
re_append(pRe, RE_OP_ACCEPT, 0);
|
|
||||||
*ppRe = pRe;
|
|
||||||
}else{
|
|
||||||
re_free(pRe);
|
|
||||||
return "unrecognized character";
|
|
||||||
}
|
|
||||||
|
|
||||||
/* The following is a performance optimization. If the regex begins with
|
|
||||||
** ".*" (if the input regex lacks an initial "^") and afterwards there are
|
|
||||||
** one or more matching characters, enter those matching characters into
|
|
||||||
** zInit[]. The re_match() routine can then search ahead in the input
|
|
||||||
** string looking for the initial match without having to run the whole
|
|
||||||
** regex engine over the string. Do not worry able trying to match
|
|
||||||
** unicode characters beyond plane 0 - those are very rare and this is
|
|
||||||
** just an optimization. */
|
|
||||||
if( pRe->aOp[0]==RE_OP_ANYSTAR ){
|
|
||||||
for(j=0, i=1; j<sizeof(pRe->zInit)-2 && pRe->aOp[i]==RE_OP_MATCH; i++){
|
|
||||||
unsigned x = pRe->aArg[i];
|
|
||||||
if( x<=127 ){
|
|
||||||
pRe->zInit[j++] = (unsigned char)x;
|
|
||||||
}else if( x<=0xfff ){
|
|
||||||
pRe->zInit[j++] = (unsigned char)(0xc0 | (x>>6));
|
|
||||||
pRe->zInit[j++] = 0x80 | (x&0x3f);
|
|
||||||
}else if( x<=0xffff ){
|
|
||||||
pRe->zInit[j++] = (unsigned char)(0xd0 | (x>>12));
|
|
||||||
pRe->zInit[j++] = 0x80 | ((x>>6)&0x3f);
|
|
||||||
pRe->zInit[j++] = 0x80 | (x&0x3f);
|
|
||||||
}else{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if( j>0 && pRe->zInit[j-1]==0 ) j--;
|
|
||||||
pRe->nInit = j;
|
|
||||||
}
|
|
||||||
return pRe->zErr;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
** Implementation of the regexp() SQL function. This function implements
|
|
||||||
** the build-in REGEXP operator. The first argument to the function is the
|
|
||||||
** pattern and the second argument is the string. So, the SQL statements:
|
|
||||||
**
|
|
||||||
** A REGEXP B
|
|
||||||
**
|
|
||||||
** is implemented as regexp(B,A).
|
|
||||||
*/
|
|
||||||
static void re_sql_func(
|
|
||||||
sqlite3_context *context,
|
|
||||||
int argc,
|
|
||||||
sqlite3_value **argv
|
|
||||||
){
|
|
||||||
ReCompiled *pRe; /* Compiled regular expression */
|
|
||||||
const char *zPattern; /* The regular expression */
|
|
||||||
const unsigned char *zStr;/* String being searched */
|
|
||||||
const char *zErr; /* Compile error message */
|
|
||||||
int setAux = 0; /* True to invoke sqlite3_set_auxdata() */
|
|
||||||
|
|
||||||
pRe = sqlite3_get_auxdata(context, 0);
|
|
||||||
if( pRe==0 ){
|
|
||||||
zPattern = (const char*)sqlite3_value_text(argv[0]);
|
|
||||||
if( zPattern==0 ) return;
|
|
||||||
zErr = re_compile(&pRe, zPattern, 0);
|
|
||||||
if( zErr ){
|
|
||||||
re_free(pRe);
|
|
||||||
sqlite3_result_error(context, zErr, -1);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if( pRe==0 ){
|
|
||||||
sqlite3_result_error_nomem(context);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setAux = 1;
|
|
||||||
}
|
|
||||||
zStr = (const unsigned char*)sqlite3_value_text(argv[1]);
|
|
||||||
if( zStr!=0 ){
|
|
||||||
sqlite3_result_int(context, re_match(pRe, zStr, -1));
|
|
||||||
}
|
|
||||||
if( setAux ){
|
|
||||||
sqlite3_set_auxdata(context, 0, pRe, (void(*)(void*))re_free);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
** Invoke this routine to register the regexp() function with the
|
|
||||||
** SQLite database connection.
|
|
||||||
*/
|
|
||||||
#ifdef _WIN32
|
|
||||||
__declspec(dllexport)
|
|
||||||
#endif
|
|
||||||
int sqlite3_regexp_init(
|
|
||||||
sqlite3 *db,
|
|
||||||
char **pzErrMsg,
|
|
||||||
const sqlite3_api_routines *pApi
|
|
||||||
){
|
|
||||||
int rc = SQLITE_OK;
|
|
||||||
SQLITE_EXTENSION_INIT2(pApi);
|
|
||||||
rc = sqlite3_create_function(db, "regexp", 2, SQLITE_UTF8, 0, re_sql_func, 0, 0);
|
|
||||||
return rc;
|
|
||||||
}
|
|
@ -1,16 +0,0 @@
|
|||||||
package regexp
|
|
||||||
|
|
||||||
// #ifndef USE_LIBSQLITE3
|
|
||||||
// #include <sqlite3-binding.h>
|
|
||||||
// #else
|
|
||||||
// #include <sqlite3.h>
|
|
||||||
// #endif
|
|
||||||
//
|
|
||||||
// // Extension function defined in regexp.c.
|
|
||||||
// extern int sqlite3_regexp_init(sqlite3*, char**, const sqlite3_api_routines*);
|
|
||||||
//
|
|
||||||
// // Use constructor to register extension function with sqlite.
|
|
||||||
// void __attribute__((constructor)) init(void) {
|
|
||||||
// sqlite3_auto_extension((void*) sqlite3_regexp_init);
|
|
||||||
// }
|
|
||||||
import "C"
|
|
@ -11,6 +11,7 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -21,14 +22,22 @@ import (
|
|||||||
"github.com/99designs/gqlgen/graphql"
|
"github.com/99designs/gqlgen/graphql"
|
||||||
sq "github.com/Masterminds/squirrel"
|
sq "github.com/Masterminds/squirrel"
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
|
"github.com/mattn/go-sqlite3"
|
||||||
// Register `sqlite3` driver.
|
|
||||||
_ "github.com/mattn/go-sqlite3"
|
|
||||||
|
|
||||||
// Register `regexp()` function.
|
|
||||||
_ "github.com/dstotijn/hetty/pkg/db/sqlite/regexp"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var regexpFn = func(pattern string, value interface{}) (bool, error) {
|
||||||
|
switch v := value.(type) {
|
||||||
|
case string:
|
||||||
|
return regexp.MatchString(pattern, v)
|
||||||
|
case int64:
|
||||||
|
return regexp.MatchString(pattern, string(v))
|
||||||
|
case []byte:
|
||||||
|
return regexp.Match(pattern, v)
|
||||||
|
default:
|
||||||
|
return false, fmt.Errorf("unsupported type %T", v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Client implements reqlog.Repository.
|
// Client implements reqlog.Repository.
|
||||||
type Client struct {
|
type Client struct {
|
||||||
db *sqlx.DB
|
db *sqlx.DB
|
||||||
@ -43,6 +52,17 @@ type httpRequestLogsQuery struct {
|
|||||||
joinResponse bool
|
joinResponse bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
sql.Register("sqlite3_with_regexp", &sqlite3.SQLiteDriver{
|
||||||
|
ConnectHook: func(conn *sqlite3.SQLiteConn) error {
|
||||||
|
if err := conn.RegisterFunc("regexp", regexpFn, false); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func New(dbPath string) (*Client, error) {
|
func New(dbPath string) (*Client, error) {
|
||||||
if _, err := os.Stat(dbPath); os.IsNotExist(err) {
|
if _, err := os.Stat(dbPath); os.IsNotExist(err) {
|
||||||
if err := os.MkdirAll(dbPath, 0755); err != nil {
|
if err := os.MkdirAll(dbPath, 0755); err != nil {
|
||||||
@ -65,7 +85,7 @@ func (c *Client) OpenProject(name string) error {
|
|||||||
|
|
||||||
dbPath := filepath.Join(c.dbPath, name+".db")
|
dbPath := filepath.Join(c.dbPath, name+".db")
|
||||||
dsn := fmt.Sprintf("file:%v?%v", dbPath, opts.Encode())
|
dsn := fmt.Sprintf("file:%v?%v", dbPath, opts.Encode())
|
||||||
db, err := sqlx.Open("sqlite3", dsn)
|
db, err := sqlx.Open("sqlite3_with_regexp", dsn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("sqlite: could not open database: %v", err)
|
return fmt.Errorf("sqlite: could not open database: %v", err)
|
||||||
}
|
}
|
||||||
@ -218,7 +238,7 @@ func (c *Client) FindRequestLogs(
|
|||||||
var ruleExpr []sq.Sqlizer
|
var ruleExpr []sq.Sqlizer
|
||||||
for _, rule := range scope.Rules() {
|
for _, rule := range scope.Rules() {
|
||||||
if rule.URL != nil {
|
if rule.URL != nil {
|
||||||
ruleExpr = append(ruleExpr, sq.Expr("req.url regexp ?", rule.URL.String()))
|
ruleExpr = append(ruleExpr, sq.Expr("regexp(?, req.url)", rule.URL.String()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(ruleExpr) > 0 {
|
if len(ruleExpr) > 0 {
|
||||||
|
Reference in New Issue
Block a user