Bump the go-modules-updates group in /backend with 7 updates (#1995)
* Bump the go-modules-updates group in /backend with 7 updates Bumps the go-modules-updates group in /backend with 7 updates: | Package | From | To | | --- | --- | --- | | [github.com/alecthomas/chroma/v2](https://github.com/alecthomas/chroma) | `2.21.1` | `2.23.1` | | [github.com/go-chi/chi/v5](https://github.com/go-chi/chi) | `5.2.3` | `5.2.4` | | [github.com/go-pkgz/rest](https://github.com/go-pkgz/rest) | `1.20.6` | `1.21.0` | | [github.com/golang-jwt/jwt/v5](https://github.com/golang-jwt/jwt) | `5.3.0` | `5.3.1` | | [golang.org/x/crypto](https://github.com/golang/crypto) | `0.46.0` | `0.47.0` | | [golang.org/x/image](https://github.com/golang/image) | `0.34.0` | `0.35.0` | | [golang.org/x/net](https://github.com/golang/net) | `0.48.0` | `0.49.0` | Updates `github.com/alecthomas/chroma/v2` from 2.21.1 to 2.23.1 - [Release notes](https://github.com/alecthomas/chroma/releases) - [Commits](https://github.com/alecthomas/chroma/compare/v2.21.1...v2.23.1) Updates `github.com/go-chi/chi/v5` from 5.2.3 to 5.2.4 - [Release notes](https://github.com/go-chi/chi/releases) - [Changelog](https://github.com/go-chi/chi/blob/master/CHANGELOG.md) - [Commits](https://github.com/go-chi/chi/compare/v5.2.3...v5.2.4) Updates `github.com/go-pkgz/rest` from 1.20.6 to 1.21.0 - [Release notes](https://github.com/go-pkgz/rest/releases) - [Commits](https://github.com/go-pkgz/rest/compare/v1.20.6...v1.21.0) Updates `github.com/golang-jwt/jwt/v5` from 5.3.0 to 5.3.1 - [Release notes](https://github.com/golang-jwt/jwt/releases) - [Commits](https://github.com/golang-jwt/jwt/compare/v5.3.0...v5.3.1) Updates `golang.org/x/crypto` from 0.46.0 to 0.47.0 - [Commits](https://github.com/golang/crypto/compare/v0.46.0...v0.47.0) Updates `golang.org/x/image` from 0.34.0 to 0.35.0 - [Commits](https://github.com/golang/image/compare/v0.34.0...v0.35.0) Updates `golang.org/x/net` from 0.48.0 to 0.49.0 - [Commits](https://github.com/golang/net/compare/v0.48.0...v0.49.0) --- updated-dependencies: - dependency-name: github.com/alecthomas/chroma/v2 dependency-version: 2.23.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: go-modules-updates - dependency-name: github.com/go-chi/chi/v5 dependency-version: 5.2.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: go-modules-updates - dependency-name: github.com/go-pkgz/rest dependency-version: 1.21.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: go-modules-updates - dependency-name: github.com/golang-jwt/jwt/v5 dependency-version: 5.3.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: go-modules-updates - dependency-name: golang.org/x/crypto dependency-version: 0.47.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: go-modules-updates - dependency-name: golang.org/x/image dependency-version: 0.35.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: go-modules-updates - dependency-name: golang.org/x/net dependency-version: 0.49.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: go-modules-updates ... Signed-off-by: dependabot[bot] <support@github.com> * Run go mod tidy in examples directory Co-authored-by: paskal <712534+paskal@users.noreply.github.com> --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: paskal <712534+paskal@users.noreply.github.com>
This commit is contained in:
co-authored by
paskal
dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
parent
e3b0d63648
commit
638fa63e81
+11
@@ -0,0 +1,11 @@
|
||||
Chroma is a syntax highlighting library, tool and web playground for Go. It is based on Pygments and includes importers for it, so most of the same concepts from Pygments apply to Chroma.
|
||||
|
||||
This project is written in Go, uses Hermit to manage tooling, and Just for helper commands. Helper scripts are in ./scripts.
|
||||
|
||||
Language definitions are XML files defined in ./lexers/embedded/*.xml.
|
||||
|
||||
Styles/themes are defined in ./styles/*.xml.
|
||||
|
||||
The CLI can be run with `chroma`.
|
||||
|
||||
The web playground can be run with `chromad --csrf-key=moo`. It blocks, so should generally be run in the background. It also does not hot reload, so has to be manually restarted. The playground has two modes - for local development it uses the server itself to render, while for production running `just chromad` will compile ./cmd/libchromawasm into a WASM module that is bundled into `chromad`.
|
||||
+2
-3
@@ -7,7 +7,6 @@ FROM ubuntu:24.04 AS builder
|
||||
RUN apt-get update && apt-get install -y \
|
||||
curl \
|
||||
git \
|
||||
make \
|
||||
ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
@@ -25,8 +24,8 @@ ENV CGO_ENABLED=0
|
||||
ENV GOOS=linux
|
||||
ENV GOARCH=amd64
|
||||
|
||||
# Build the application using make
|
||||
RUN make build/chromad
|
||||
# Build the application using just
|
||||
RUN just chromad
|
||||
|
||||
# Runtime stage
|
||||
FROM alpine:3.23 AS runtime
|
||||
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
set positional-arguments := true
|
||||
set shell := ["bash", "-c"]
|
||||
|
||||
version := `git describe --tags --dirty --always`
|
||||
export GOOS := env("GOOS", "linux")
|
||||
export GOARCH := env("GOARCH", "amd64")
|
||||
|
||||
_help:
|
||||
@just -l
|
||||
|
||||
# Generate README.md from lexer definitions
|
||||
readme:
|
||||
#!/usr/bin/env bash
|
||||
GOOS= GOARCH= ./table.py
|
||||
|
||||
# Generate tokentype_string.go
|
||||
tokentype-string:
|
||||
go generate
|
||||
|
||||
# Format JavaScript files
|
||||
format-js:
|
||||
biome format --write cmd/chromad/static/index.js cmd/chromad/static/chroma.js
|
||||
|
||||
# Build chromad binary
|
||||
chromad: wasm-exec chroma-wasm
|
||||
#!/usr/bin/env bash
|
||||
rm -rf build
|
||||
mk cmd/chromad/static/index.min.js : cmd/chromad/static/{index,chroma}.js -- \
|
||||
esbuild --platform=browser --format=esm --bundle cmd/chromad/static/index.js --minify --external:./wasm_exec.js --outfile=cmd/chromad/static/index.min.js
|
||||
mk cmd/chromad/static/index.min.css : cmd/chromad/static/index.css -- \
|
||||
esbuild --bundle cmd/chromad/static/index.css --minify --outfile=cmd/chromad/static/index.min.css
|
||||
cd cmd/chromad && CGOENABLED=0 go build -ldflags="-X 'main.version={{ version }}'" -o ../../build/chromad .
|
||||
|
||||
# Copy wasm_exec.js from TinyGo
|
||||
wasm-exec:
|
||||
#!/usr/bin/env bash
|
||||
tinygoroot=$(tinygo env TINYGOROOT)
|
||||
mk cmd/chromad/static/wasm_exec.js : "$tinygoroot/targets/wasm_exec.js" -- \
|
||||
install -m644 "$tinygoroot/targets/wasm_exec.js" cmd/chromad/static/wasm_exec.js
|
||||
|
||||
# Build WASM binary
|
||||
chroma-wasm:
|
||||
#!/usr/bin/env bash
|
||||
if type tinygo > /dev/null 2>&1; then
|
||||
mk cmd/chromad/static/chroma.wasm : cmd/libchromawasm/main.go -- \
|
||||
tinygo build -no-debug -target wasm -o cmd/chromad/static/chroma.wasm cmd/libchromawasm/main.go
|
||||
else
|
||||
mk cmd/chromad/static/chroma.wasm : cmd/libchromawasm/main.go -- \
|
||||
GOOS=js GOARCH=wasm go build -o cmd/chromad/static/chroma.wasm cmd/libchromawasm/main.go
|
||||
fi
|
||||
|
||||
# Upload chromad to server
|
||||
upload: chromad
|
||||
scp build/chromad root@swapoff.org:
|
||||
ssh root@swapoff.org 'install -m755 ./chromad /srv/http/swapoff.org/bin && service chromad restart'
|
||||
-42
@@ -1,42 +0,0 @@
|
||||
.PHONY: chromad upload all
|
||||
|
||||
VERSION ?= $(shell git describe --tags --dirty --always)
|
||||
export GOOS ?= linux
|
||||
export GOARCH ?= amd64
|
||||
|
||||
all: README.md tokentype_string.go
|
||||
|
||||
README.md: lexers/*.go lexers/embedded/*.xml
|
||||
GOOS= GOARCH= ./table.py
|
||||
|
||||
tokentype_string.go: types.go
|
||||
go generate
|
||||
|
||||
.PHONY: format-js
|
||||
format-js:
|
||||
biome format --write cmd/chromad/static/{index.js,chroma.js}
|
||||
|
||||
.PHONY: chromad
|
||||
chromad: build/chromad
|
||||
|
||||
build/chromad: $(shell find cmd/chromad -name '*.go' -o -name '*.html' -o -name '*.css' -o -name '*.js') \
|
||||
cmd/chromad/static/wasm_exec.js \
|
||||
cmd/chromad/static/chroma.wasm
|
||||
rm -rf build
|
||||
esbuild --platform=node --bundle cmd/chromad/static/index.js --minify --outfile=cmd/chromad/static/index.min.js
|
||||
esbuild --bundle cmd/chromad/static/index.css --minify --outfile=cmd/chromad/static/index.min.css
|
||||
(export CGOENABLED=0 ; go build -C cmd/chromad -ldflags="-X 'main.version=$(VERSION)'" -o ../../build/chromad .)
|
||||
|
||||
cmd/chromad/static/wasm_exec.js: $(shell tinygo env TINYGOROOT)/targets/wasm_exec.js
|
||||
install -m644 $< $@
|
||||
|
||||
cmd/chromad/static/chroma.wasm: $(shell git ls-files | grep '\.go|\.xml')
|
||||
if type tinygo > /dev/null; then \
|
||||
tinygo build -no-debug -target wasm -o $@ cmd/libchromawasm/main.go; \
|
||||
else \
|
||||
GOOS=js GOARCH=wasm go build -o $@ cmd/libchromawasm/main.go; \
|
||||
fi
|
||||
|
||||
upload: build/chromad
|
||||
scp build/chromad root@swapoff.org: && \
|
||||
ssh root@swapoff.org 'install -m755 ./chromad /srv/http/swapoff.org/bin && service chromad restart'
|
||||
+1
-1
@@ -48,7 +48,7 @@ translators for Pygments lexers and styles.
|
||||
| J | J, Janet, Java, JavaScript, JSON, JSONata, Jsonnet, Julia, Jungle
|
||||
| K | Kakoune, Kotlin
|
||||
| L | Lean4, Lighttpd configuration file, LLVM, lox, Lua
|
||||
| M | Makefile, Mako, markdown, Mason, Materialize SQL dialect, Mathematica, Matlab, MCFunction, Meson, Metal, MiniZinc, MLIR, Modelica, Modula-2, Mojo, MonkeyC, MoonScript, MorrowindScript, Myghty, MySQL
|
||||
| M | Makefile, Mako, markdown, Markless, Mason, Materialize SQL dialect, Mathematica, Matlab, MCFunction, Meson, Metal, MiniZinc, MLIR, Modelica, Modula-2, Mojo, MonkeyC, MoonScript, MorrowindScript, Myghty, MySQL
|
||||
| N | NASM, Natural, NDISASM, Newspeak, Nginx configuration file, Nim, Nix, NSIS, Nu
|
||||
| O | Objective-C, ObjectPascal, OCaml, Octave, Odin, OnesEnterprise, OpenEdge ABL, OpenSCAD, Org Mode
|
||||
| P | PacmanConf, Perl, PHP, PHTML, Pig, PkgConfig, PL/pgSQL, plaintext, Plutus Core, Pony, PostgreSQL SQL dialect, PostScript, POVRay, PowerQuery, PowerShell, Prolog, Promela, PromQL, properties, Protocol Buffer, Protocol Buffer Text Format, PRQL, PSL, Puppet, Python, Python 2
|
||||
|
||||
+1
@@ -528,6 +528,7 @@ func (f *Formatter) styleToCSS(style *chroma.Style) map[chroma.TokenType]string
|
||||
}
|
||||
classes[chroma.Background] += `;` + f.tabWidthStyle()
|
||||
classes[chroma.PreWrapper] += classes[chroma.Background]
|
||||
classes[chroma.PreWrapper] += ` -webkit-text-size-adjust: none;`
|
||||
// Make PreWrapper a grid to show highlight style with full width.
|
||||
if len(f.highlightRanges) > 0 && f.customCSS[chroma.PreWrapper] == `` {
|
||||
classes[chroma.PreWrapper] += `display: grid;`
|
||||
|
||||
+3
-1
@@ -12,6 +12,7 @@
|
||||
<filename>*.ebuild</filename>
|
||||
<filename>*.eclass</filename>
|
||||
<filename>.env</filename>
|
||||
<filename>.env.*</filename>
|
||||
<filename>*.env</filename>
|
||||
<filename>*.exheres-0</filename>
|
||||
<filename>*.exlib</filename>
|
||||
@@ -23,6 +24,7 @@
|
||||
<filename>bash_*</filename>
|
||||
<filename>zshrc</filename>
|
||||
<filename>.zshrc</filename>
|
||||
<filename>APKBUILD</filename>
|
||||
<filename>PKGBUILD</filename>
|
||||
<mime_type>application/x-sh</mime_type>
|
||||
<mime_type>application/x-shellscript</mime_type>
|
||||
@@ -217,4 +219,4 @@
|
||||
</rule>
|
||||
</state>
|
||||
</rules>
|
||||
</lexer>
|
||||
</lexer>
|
||||
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
<lexer>
|
||||
<config>
|
||||
<name>KDL</name>
|
||||
<alias>kdl</alias>
|
||||
<filename>*.kdl</filename>
|
||||
</config>
|
||||
<rules>
|
||||
<state name="root">
|
||||
<rule pattern="((?<={|;)|^)\s*(?![/\\\{\}#;\[\]\=])[<>:\w\-_~,\'`!\?@\$%^&*+|\.\(\)\x{0080}-\x{0001f645}]+\d*?[<>:\w\-_~,\'`!\?@\$%^&*+|\.\(\)\x{0080}-\x{0001f645}]*?"><token type="NameLabel"/></rule>
|
||||
<rule pattern="(#true|#false|#null|#nan|#inf|#-inf)\b"><token type="KeywordConstant"/></rule>
|
||||
<rule pattern="[{}=;\\]"><token type="Operator"/></rule>
|
||||
<rule pattern="(\b([0-9-\+]|-|\+)[0-9_]*?\.[0-9][0-9_]*?([eE][+-]?[0-9_]+)?\b|\b[0-9][0-9_]*?(\.[0-9][0-9_]*?)?[eE][+-]?[0-9_]+\b)"><token type="LiteralNumberFloat"/></rule>
|
||||
<rule pattern="\b[0-9\-\+][0-9_]*\b"><token type="LiteralNumber"/></rule>
|
||||
<rule pattern="\b0x[a-fA-F0-9][a-fA-F0-9_]*?\b"><token type="LiteralNumberHex"/></rule>
|
||||
<rule pattern="\b0o[0-7][0-7_]*\b"><token type="LiteralNumberOct"/></rule>
|
||||
<rule pattern="\b0b[01][01_]*?\b"><token type="LiteralNumberBin"/></rule>
|
||||
<rule pattern="#+(\"""|").*?("""|")#+"><token type="LiteralString"/></rule>
|
||||
<rule pattern="#?""""><token type="LiteralString"/><push state="multiline_string"/></rule>
|
||||
<rule pattern="#?""><token type="LiteralString"/><push state="string"/></rule>
|
||||
<rule pattern="/\*"><token type="CommentMultiline"/><push state="comment"/></rule>
|
||||
<rule pattern="/\*!"><token type="LiteralStringDoc"/><push state="doccomment"/></rule>
|
||||
<rule pattern="/-\s*{"><token type="CommentMultiline"/><push state="slashdash_block_comment"/></rule>
|
||||
<rule pattern="\s*/-\s?[^\s=]*?\s?{"><token type="CommentMultiline"/><push state="slashdash_node_comment"/></rule>
|
||||
<rule pattern="(?<!^)\s*/-\s*(".*"|.*?)?\s"><token type="CommentSingle"/></rule>
|
||||
<rule pattern="(?<=^)\s*/-[^{]+{"><token type="CommentMultiline"/><push state="slashdash_node_with_children_comment"/></rule>
|
||||
<rule pattern="(\/\/(.*?)\n|(?<!^)\s*/-\s*?\s)"><token type="CommentSingle"/></rule>
|
||||
<rule pattern="(?![/\\\{\}#;\[\]\=])[<>:\w\-_~,\'`!\?@\$%^&*+|.\(\)\x{0080}-\x{0001f645}]+\d*?[<>:\w\-_~,\'`!\?@\$%^&*+|.\(\)\x{0080}-\x{0001f645}]*(=)"><token type="NameAttribute"/></rule>
|
||||
<rule pattern="(?![/\\{\}#;\[\]\=])[<>:\w\-_~,\'`!\?@\$%^&*+|.\(\)\x{0080}-\x{0001f645}]+\d*[<>:\w\-_~,\'`!\?@\$%^&*+|.\(\)\x{0080}-\x{0001f645}]*?"><token type="LiteralString"/></rule>
|
||||
<rule pattern="\s"><token type="TextWhitespace"/></rule>
|
||||
</state>
|
||||
<state name="string">
|
||||
<rule pattern=""#?"><token type="LiteralString"/><pop depth="1"/></rule>
|
||||
<rule pattern="\\['"\\nrt]|\\x[0-7][0-9a-fA-F]|\\0|\\u\{[0-9a-fA-F]{1,6}\}"><token type="LiteralStringEscape"/></rule>
|
||||
<rule pattern="[^\\"]+"><token type="LiteralString"/></rule>
|
||||
<rule pattern="\\"><token type="LiteralString"/></rule>
|
||||
</state>
|
||||
<state name="multiline_string">
|
||||
<rule pattern=""""#?"><token type="LiteralString"/><pop depth="1"/></rule>
|
||||
<rule pattern="\\['"\\nrt]|\\x[0-7][0-9a-fA-F]|\\0|\\u\{[0-9a-fA-F]{1,6}\}"><token type="LiteralStringEscape"/></rule>
|
||||
<rule pattern="""><token type="LiteralString"/></rule>
|
||||
<rule pattern="[^\\"]+"><token type="LiteralString"/></rule>
|
||||
<rule pattern="\\"><token type="LiteralString"/></rule>
|
||||
</state>
|
||||
<state name="slashdash_block_comment">
|
||||
<rule pattern="[^}]+"><token type="CommentMultiline"/></rule>
|
||||
<rule pattern="/-\s*{"><token type="CommentMultiline"/><push/></rule>
|
||||
<rule pattern="\}"><token type="CommentMultiline"/><pop depth="1"/></rule>
|
||||
<rule pattern="[\}]"><token type="CommentMultiline"/></rule>
|
||||
</state>
|
||||
<state name="slashdash_node_comment">
|
||||
<rule pattern="[^\}]+"><token type="CommentMultiline"/></rule>
|
||||
<rule pattern="^\s*?/-.*?\s?{"><token type="CommentMultiline"/><push/></rule>
|
||||
<rule pattern="\}"><token type="CommentMultiline"/><pop depth="1"/></rule>
|
||||
<rule pattern="[\}]"><token type="CommentMultiline"/></rule>
|
||||
</state>
|
||||
<state name="slashdash_node_with_children_comment">
|
||||
<rule pattern="[^\}]+"><token type="CommentMultiline"/></rule>
|
||||
<rule pattern="(?<=^)\s*/-[^{]+{"><token type="CommentMultiline"/><push/></rule>
|
||||
<rule pattern="\}"><token type="CommentMultiline"/><pop depth="1"/></rule>
|
||||
<rule pattern="[\}]"><token type="CommentMultiline"/></rule>
|
||||
</state>
|
||||
<state name="comment">
|
||||
<rule pattern="[^*/]+"><token type="CommentMultiline"/></rule>
|
||||
<rule pattern="/\*"><token type="CommentMultiline"/><push/></rule>
|
||||
<rule pattern="\*/"><token type="CommentMultiline"/><pop depth="1"/></rule>
|
||||
<rule pattern="[*/]"><token type="CommentMultiline"/></rule>
|
||||
</state>
|
||||
<state name="doccomment">
|
||||
<rule pattern="[^*/]+"><token type="LiteralStringDoc"/></rule>
|
||||
<rule pattern="/\*"><token type="LiteralStringDoc"/><push/></rule>
|
||||
<rule pattern="\*/"><token type="LiteralStringDoc"/><pop depth="1"/></rule>
|
||||
<rule pattern="[*/]"><token type="LiteralStringDoc"/></rule>
|
||||
</state>
|
||||
</rules>
|
||||
</lexer>
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
<lexer>
|
||||
<config>
|
||||
<name>microcad</name>
|
||||
<alias>µcad</alias>
|
||||
<filename>*.µcad</filename>
|
||||
<filename>*.ucad</filename>
|
||||
<filename>*.mcad</filename>
|
||||
<mime_type>text/microcad</mime_type>
|
||||
<ensure_nl>true</ensure_nl>
|
||||
</config>
|
||||
|
||||
<rules>
|
||||
<!-- Root state -->
|
||||
<state name="root">
|
||||
<rule pattern="\n">
|
||||
<token type="TextWhitespace"/>
|
||||
</rule>
|
||||
<rule pattern="\s+">
|
||||
<token type="TextWhitespace"/>
|
||||
</rule>
|
||||
|
||||
<!-- COMMENTS -->
|
||||
<rule pattern="///.*">
|
||||
<token type="LiteralStringDoc"/>
|
||||
</rule>
|
||||
|
||||
<rule pattern="//.*">
|
||||
<token type="CommentSingle"/>
|
||||
</rule>
|
||||
|
||||
<rule pattern="/\*.*">
|
||||
<token type="CommentMultiline"/>
|
||||
<push state="comment"/>
|
||||
</rule>
|
||||
|
||||
<!-- KEYWORDS -->
|
||||
<rule pattern="\b(pub|sketch|part|op|mod|use|fn|const|prop|init|return|if|else|mat|__builtin|or|and|not|as)\b">
|
||||
<token type="Keyword"/>
|
||||
</rule>
|
||||
|
||||
<!-- TYPES -->
|
||||
<rule pattern="\b(Integer|Scalar|String|Color|Length|Area|Volume|Angle|Weight|Density|Bool|Matrix[0-9])\b">
|
||||
<token type="KeywordType"/>
|
||||
</rule>
|
||||
|
||||
<!-- FUNCTION NAMES -->
|
||||
<rule pattern="\b([a-z_][a-zA-Z0-9_]*)\s*(?=\()">
|
||||
<token type="NameFunction"/>
|
||||
</rule>
|
||||
|
||||
<!-- OPERATORS -->
|
||||
<rule pattern="[+\-*/%&|<>^!@=]">
|
||||
<token type="Operator"/>
|
||||
</rule>
|
||||
|
||||
<!-- NUMBERS + UNITS -->
|
||||
<rule pattern="\b(([0-9]+(\.[0-9]+)?)(%|m²|cm²|mm²|µm²|in²|ft²|yd²|m³|cm³|mm³|µm³|in³|ft³|yd³|ml|cl|l|µl|cm|mm|m|µm|in|ft|yd|deg|°|grad|turn|rad|g|kg|lb|oz)?)|true|false\b">
|
||||
<token type="LiteralNumber"/>
|
||||
</rule>
|
||||
|
||||
<!-- NAMESPACES -->
|
||||
<rule pattern="([a-z_][a-z0-9_]*)(::)">
|
||||
<bygroups>
|
||||
<token type="NameNamespace"/>
|
||||
<token type="Operator"/>
|
||||
</bygroups>
|
||||
</rule>
|
||||
|
||||
<!-- CONSTANTS (ALL CAPS) -->
|
||||
<rule pattern="\b([A-Z_][A-Z0-9_]*)\b">
|
||||
<token type="NameConstant"/>
|
||||
</rule>
|
||||
|
||||
<!-- CUSTOM TYPES (Capitalized identifiers) -->
|
||||
<rule pattern="\b([A-Z_][a-zA-Z0-9_]*)\b">
|
||||
<token type="NameClass"/>
|
||||
</rule>
|
||||
|
||||
<!-- VARIABLES -->
|
||||
<rule pattern="\b([a-z_][a-zA-Z0-9_]*)\b">
|
||||
<token type="Name"/>
|
||||
</rule>
|
||||
|
||||
<!-- STRINGS -->
|
||||
<rule pattern=""[^"]*"">
|
||||
<token type="LiteralString"/>
|
||||
</rule>
|
||||
|
||||
<!-- PAREN GROUP -->
|
||||
<rule pattern="[{}()\[\],.;:]">
|
||||
<token type="Punctuation"/>
|
||||
</rule>
|
||||
|
||||
|
||||
<!-- BRACE GROUP -->
|
||||
<rule pattern="\{">
|
||||
<token type="Punctuation"/>
|
||||
<push state="brace"/>
|
||||
</rule>
|
||||
|
||||
<rule pattern="[\}\)]">
|
||||
<token type="Punctuation"/>
|
||||
</rule>
|
||||
|
||||
</state>
|
||||
|
||||
<!-- Parenthesized expression -->
|
||||
<state name="paren">
|
||||
<rule pattern="\)">
|
||||
<token type="Punctuation"/>
|
||||
<pop/>
|
||||
</rule>
|
||||
<rule>
|
||||
<include state="root"/>
|
||||
</rule>
|
||||
</state>
|
||||
|
||||
<!-- Braced expression -->
|
||||
<state name="brace">
|
||||
<rule pattern="\}">
|
||||
<token type="Punctuation"/>
|
||||
<pop/>
|
||||
</rule>
|
||||
<rule>
|
||||
<include state="root"/>
|
||||
</rule>
|
||||
</state>
|
||||
|
||||
<state name="comment">
|
||||
<!-- End of comment -->
|
||||
<rule pattern="\*/">
|
||||
<token type="CommentMultiline"/>
|
||||
<pop/>
|
||||
</rule>
|
||||
</state>
|
||||
|
||||
|
||||
</rules>
|
||||
</lexer>
|
||||
+105
-105
@@ -1,106 +1,106 @@
|
||||
<lexer>
|
||||
<config>
|
||||
<name>Modelica</name>
|
||||
<alias>modelica</alias>
|
||||
<filename>*.mo</filename>
|
||||
<mime_type>text/x-modelica</mime_type>
|
||||
<dot_all>true</dot_all>
|
||||
<ensure_nl>true</ensure_nl>
|
||||
</config>
|
||||
<rules>
|
||||
<state name="root">
|
||||
<!-- Comments -->
|
||||
<rule pattern="//[^\n\r]*">
|
||||
<token type="CommentSingle"/>
|
||||
</rule>
|
||||
<rule pattern="/[*].*?[*]/">
|
||||
<token type="CommentMultiline"/>
|
||||
</rule>
|
||||
|
||||
<!-- Whitespace and newlines -->
|
||||
<rule pattern="\s+">
|
||||
<token type="Text"/>
|
||||
</rule>
|
||||
<rule pattern="\n">
|
||||
<token type="Text"/>
|
||||
</rule>
|
||||
|
||||
<!-- Keywords (Modelica specific) -->
|
||||
<rule pattern="\b(model|equation|end|extends|function|type|record|connector|parameter|constant|if|then|else|for|in|when|assert|outer|algorithm|flow|discrete|input|output|loop|elseif|return|public|protected|external|real|integer|boolean|string|array|complex|union|tuple|class|der|time|tstart|tstop)\b">
|
||||
<token type="Keyword"/>
|
||||
</rule>
|
||||
|
||||
<!-- Type definitions -->
|
||||
<rule pattern="\b(boolean|integer|real|string|array|record|complex|union|tuple)\b">
|
||||
<token type="KeywordType"/>
|
||||
</rule>
|
||||
|
||||
<!-- Preprocessor directives -->
|
||||
<rule pattern="#[ \t]*(if|else|endif|define|include|error)\b">
|
||||
<token type="CommentPreproc"/>
|
||||
</rule>
|
||||
|
||||
<!-- Literals -->
|
||||
<rule pattern="\\"[^"\n]*\\"">
|
||||
<token type="LiteralString"/>
|
||||
</rule>
|
||||
<rule pattern="'[^'\n]'">
|
||||
<token type="LiteralStringChar"/>
|
||||
</rule>
|
||||
<rule pattern="\d+(\.\d+)?([eE][+-]?\d+)?">
|
||||
<token type="LiteralNumber"/>
|
||||
</rule>
|
||||
|
||||
<!-- Identifiers and names -->
|
||||
<rule pattern="@[A-Za-z_]\w*">
|
||||
<token type="Name"/>
|
||||
</rule>
|
||||
<rule pattern="[A-Za-z_]\w*">
|
||||
<token type="Name"/>
|
||||
</rule>
|
||||
|
||||
<!-- Punctuation -->
|
||||
<rule pattern="[{}(),;=.+\-*/&|!<>^%]">
|
||||
<token type="Punctuation"/>
|
||||
</rule>
|
||||
|
||||
<!-- Operators -->
|
||||
<rule pattern="(\+|\-|\*|\/|\^|\&|\||\<|\>|\%|\=|\!=|\<\=|\>\=)">
|
||||
<token type="Operator"/>
|
||||
</rule>
|
||||
</state>
|
||||
|
||||
<state name="function">
|
||||
<!-- Function name -->
|
||||
<rule pattern="[A-Za-z_]\w*">
|
||||
<token type="NameFunction"/>
|
||||
<pop depth="1"/>
|
||||
</rule>
|
||||
<rule>
|
||||
<pop depth="1"/>
|
||||
</rule>
|
||||
</state>
|
||||
|
||||
<state name="type">
|
||||
<!-- Type definition -->
|
||||
<rule pattern="[A-Za-z_]\w*">
|
||||
<token type="NameClass"/>
|
||||
<pop depth="1"/>
|
||||
</rule>
|
||||
<rule>
|
||||
<pop depth="1"/>
|
||||
</rule>
|
||||
</state>
|
||||
|
||||
<state name="record">
|
||||
<!-- Record definition -->
|
||||
<rule pattern="[A-Za-z_]\w*">
|
||||
<token type="NameClass"/>
|
||||
<pop depth="1"/>
|
||||
</rule>
|
||||
<rule>
|
||||
<pop depth="1"/>
|
||||
</rule>
|
||||
</state>
|
||||
</rules>
|
||||
<lexer>
|
||||
<config>
|
||||
<name>Modelica</name>
|
||||
<alias>modelica</alias>
|
||||
<filename>*.mo</filename>
|
||||
<mime_type>text/x-modelica</mime_type>
|
||||
<dot_all>true</dot_all>
|
||||
<ensure_nl>true</ensure_nl>
|
||||
</config>
|
||||
<rules>
|
||||
<state name="root">
|
||||
<!-- Comments -->
|
||||
<rule pattern="//[^\n\r]*">
|
||||
<token type="CommentSingle"/>
|
||||
</rule>
|
||||
<rule pattern="/[*].*?[*]/">
|
||||
<token type="CommentMultiline"/>
|
||||
</rule>
|
||||
|
||||
<!-- Whitespace and newlines -->
|
||||
<rule pattern="\s+">
|
||||
<token type="Text"/>
|
||||
</rule>
|
||||
<rule pattern="\n">
|
||||
<token type="Text"/>
|
||||
</rule>
|
||||
|
||||
<!-- Keywords (Modelica specific) -->
|
||||
<rule pattern="\b(model|equation|end|extends|function|type|record|connector|parameter|constant|if|then|else|for|in|when|assert|outer|algorithm|flow|discrete|input|output|loop|elseif|return|public|protected|external|real|integer|boolean|string|array|complex|union|tuple|class|der|time|tstart|tstop)\b">
|
||||
<token type="Keyword"/>
|
||||
</rule>
|
||||
|
||||
<!-- Type definitions -->
|
||||
<rule pattern="\b(boolean|integer|real|string|array|record|complex|union|tuple)\b">
|
||||
<token type="KeywordType"/>
|
||||
</rule>
|
||||
|
||||
<!-- Preprocessor directives -->
|
||||
<rule pattern="#[ \t]*(if|else|endif|define|include|error)\b">
|
||||
<token type="CommentPreproc"/>
|
||||
</rule>
|
||||
|
||||
<!-- Literals -->
|
||||
<rule pattern="\\"[^"\n]*\\"">
|
||||
<token type="LiteralString"/>
|
||||
</rule>
|
||||
<rule pattern="'[^'\n]'">
|
||||
<token type="LiteralStringChar"/>
|
||||
</rule>
|
||||
<rule pattern="\d+(\.\d+)?([eE][+-]?\d+)?">
|
||||
<token type="LiteralNumber"/>
|
||||
</rule>
|
||||
|
||||
<!-- Identifiers and names -->
|
||||
<rule pattern="@[A-Za-z_]\w*">
|
||||
<token type="Name"/>
|
||||
</rule>
|
||||
<rule pattern="[A-Za-z_]\w*">
|
||||
<token type="Name"/>
|
||||
</rule>
|
||||
|
||||
<!-- Punctuation -->
|
||||
<rule pattern="[{}(),;=.+\-*/&|!<>^%]">
|
||||
<token type="Punctuation"/>
|
||||
</rule>
|
||||
|
||||
<!-- Operators -->
|
||||
<rule pattern="(\+|\-|\*|\/|\^|\&|\||\<|\>|\%|\=|\!=|\<\=|\>\=)">
|
||||
<token type="Operator"/>
|
||||
</rule>
|
||||
</state>
|
||||
|
||||
<state name="function">
|
||||
<!-- Function name -->
|
||||
<rule pattern="[A-Za-z_]\w*">
|
||||
<token type="NameFunction"/>
|
||||
<pop depth="1"/>
|
||||
</rule>
|
||||
<rule>
|
||||
<pop depth="1"/>
|
||||
</rule>
|
||||
</state>
|
||||
|
||||
<state name="type">
|
||||
<!-- Type definition -->
|
||||
<rule pattern="[A-Za-z_]\w*">
|
||||
<token type="NameClass"/>
|
||||
<pop depth="1"/>
|
||||
</rule>
|
||||
<rule>
|
||||
<pop depth="1"/>
|
||||
</rule>
|
||||
</state>
|
||||
|
||||
<state name="record">
|
||||
<!-- Record definition -->
|
||||
<rule pattern="[A-Za-z_]\w*">
|
||||
<token type="NameClass"/>
|
||||
<pop depth="1"/>
|
||||
</rule>
|
||||
<rule>
|
||||
<pop depth="1"/>
|
||||
</rule>
|
||||
</state>
|
||||
</rules>
|
||||
</lexer>
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
<lexer>
|
||||
<config>
|
||||
<name>MoonBit</name>
|
||||
<alias>moonbit</alias>
|
||||
<alias>mbt</alias>
|
||||
<filename>*.mbt</filename>
|
||||
<ensure_nl>true</ensure_nl>
|
||||
</config>
|
||||
<rules>
|
||||
<state name="root">
|
||||
<rule pattern="#.*$"><token type="CommentPreproc"/></rule>
|
||||
<rule pattern="//.*$"><token type="CommentSingle"/></rule>
|
||||
<rule pattern="b?\'.*\'"><token type="Literal"/></rule>
|
||||
<rule pattern="#\|.*$"><token type="LiteralString"/></rule>
|
||||
<rule pattern="(b)(\")"><bygroups><token type="LiteralStringAffix"/><token type="LiteralString"/></bygroups><push state="string.inline"/></rule>
|
||||
<rule pattern="""><token type="LiteralString"/><push state="string.inline"/></rule>
|
||||
<rule pattern="\$\|"><token type="LiteralString"/><push state="string.multiline"/></rule>
|
||||
<rule pattern="0(b|B)[01]+"><token type="LiteralNumberBin"/></rule>
|
||||
<rule pattern="0(o|O)[0-7]+"><token type="LiteralNumberOct"/></rule>
|
||||
<rule pattern="0(x|X)[0-9a-fA-F][0-9a-fA-F_]*\.[0-9a-fA-F][0-9a-fA-F_]*(P|p)(\+|\-)?[0-9][0-9]*"><token type="LiteralNumberFloat"/></rule>
|
||||
<rule pattern="0(x|X)[0-9a-fA-F][0-9a-fA-F_]*\.?(P|p)(\+|\-)?[0-9][0-9]*"><token type="LiteralNumberFloat"/></rule>
|
||||
<rule pattern="0(x|X)[0-9a-fA-F][0-9a-fA-F_]*\.[0-9a-fA-F][0-9a-fA-F_]*"><token type="LiteralNumberFloat"/></rule>
|
||||
<rule pattern="0(x|X)[0-9a-fA-F][0-9a-fA-F_]*\."><token type="LiteralNumberFloat"/></rule>
|
||||
<rule pattern="0(x|X)[0-9a-fA-F][0-9a-fA-F_]*"><token type="LiteralNumberHex"/></rule>
|
||||
<rule pattern="\d(_|\d)*U?L"><token type="LiteralNumberIntegerLong"/></rule>
|
||||
<rule pattern="\d(_|\d)*U?"><token type="LiteralNumberInteger"/></rule>
|
||||
<rule pattern="\d+(.\d+)?"><token type="LiteralNumber"/></rule>
|
||||
<rule pattern="(type|type!|enum|struct|trait|typealias|traitalias)\b"><token type="KeywordDeclaration"/></rule>
|
||||
<rule pattern="(async|fn|const|let|mut|impl|with|derive|fnalias)\b"><token type="KeywordDeclaration"/></rule>
|
||||
<rule pattern="(self|Self)\b"><token type="Keyword"/></rule>
|
||||
<rule pattern="(guard|if|while|match|else|loop|for|in|is)\b"><token type="Keyword"/></rule>
|
||||
<rule pattern="(return|break|continue)\b"><token type="Keyword"/></rule>
|
||||
<rule pattern="(try|catch|raise|noraise)\b"><token type="Keyword"/></rule>
|
||||
<rule pattern="\bas\b"><token type="Keyword"/></rule>
|
||||
<rule pattern="(extern|pub|priv|pub\(all\)|pub\(readonly\)|pub\(open\)|test)\b"><token type="Keyword"/></rule>
|
||||
<rule pattern="(true|false)\b"><token type="KeywordConstant"/></rule>
|
||||
<rule pattern="(Eq|Compare|Hash|Show|Default|ToJson|FromJson)\b"><token type="NameBuiltin"/></rule>
|
||||
<rule pattern="(Array|FixedArray|Int|Int64|UInt|UInt64|Option|Result|Byte|Bool|Unit|String|Float|Double)\b"><token type="NameBuiltin"/></rule>
|
||||
<rule pattern="(\+|\-|\*|/|%|\|>|>>|<<|\&\&|\|\||\&|\||<|>|==)"><token type="Operator"/></rule>
|
||||
<rule pattern="(not|lsl|lsr|asr|op_add|op_sub|op_div|op_mul|op_mod|\.\.\.)"><token type="OperatorWord"/></rule>
|
||||
<rule pattern="@[A-Za-z][A-Za-z0-9_/]*\."><token type="NameNamespace"/></rule>
|
||||
<rule pattern="([a-z][A-Za-z0-9_]*)(\s+)(as)(\s+)([a-z][A-Za-z0-9_]*)"><bygroups><token type="NameFunction"/><token type="TextWhitespace"/><token type="Keyword"/><token type="TextWhitespace"/><token type="NameFunction"/></bygroups></rule>
|
||||
<rule pattern="([A-Za-z][A-Za-z0-9_]*)(::)([A-Za-z][A-Za-z0-9_]*)(\s+)(as)(\s+)([a-z][A-Za-z0-9_]*)"><bygroups><token type="NameClass"/><token type="Punctuation"/><token type="NameFunction"/><token type="TextWhitespace"/><token type="Keyword"/><token type="TextWhitespace"/><token type="NameFunction"/></bygroups></rule>
|
||||
<rule pattern="([A-Za-z][A-Za-z0-9_]*)(::)([A-Za-z][A-Za-z0-9_]*)"><bygroups><token type="NameClass"/><token type="Punctuation"/><token type="NameFunction"/></bygroups></rule>
|
||||
<rule pattern="([a-z][A-Za-z0-9_]*)(?=!?\()"><token type="NameFunction"/></rule>
|
||||
<rule pattern="Error"><token type="NameException"/></rule>
|
||||
<rule pattern="(=>)|(->)|[\(\)\{\}\[\]:,\.=!?~;]"><token type="Punctuation"/></rule>
|
||||
<rule pattern="[a-z][a-zA-Z0-9_]*"><token type="NameVariable"/></rule>
|
||||
<rule pattern="[A-Z_][a-zA-Z0-9_]*"><token type="NameClass"/></rule>
|
||||
<rule pattern="[\s]"><token type="TextWhitespace"/></rule>
|
||||
</state>
|
||||
<state name="string.inline">
|
||||
<rule><include state="escape"/></rule>
|
||||
<rule pattern="\\{"><token type="LiteralStringEscape"/><push state="interpolation"/></rule>
|
||||
<rule pattern="""><token type="LiteralString"/><pop depth="1"/></rule>
|
||||
<rule pattern="."><token type="LiteralStringDouble"/></rule>
|
||||
</state>
|
||||
<state name="string.multiline">
|
||||
<rule><include state="escape"/></rule>
|
||||
<rule pattern="\\{"><token type="LiteralStringEscape"/><push state="interpolation"/></rule>
|
||||
<rule pattern="\Z"><token type="LiteralString"/><pop depth="1"/></rule>
|
||||
<rule pattern="."><token type="LiteralString"/></rule>
|
||||
</state>
|
||||
<state name="interpolation">
|
||||
<rule pattern="}"><token type="LiteralStringEscape"/><pop depth="1"/></rule>
|
||||
<rule><include state="root"/></rule>
|
||||
</state>
|
||||
<state name="escape">
|
||||
<rule pattern="\\[0\\tnrb\"']"><token type="LiteralStringEscape"/></rule>
|
||||
<rule pattern="\\x[0-9a-fA-f]{2}"><token type="LiteralStringEscape"/></rule>
|
||||
<rule pattern="\\u[0-9a-fA-f]{4}"><token type="LiteralStringEscape"/></rule>
|
||||
<rule pattern="\\u[0-9a-fA-f]*"><token type="LiteralStringEscape"/></rule>
|
||||
</state>
|
||||
</rules>
|
||||
</lexer>
|
||||
+111
-40
@@ -8,7 +8,13 @@
|
||||
</config>
|
||||
<rules>
|
||||
<state name="string">
|
||||
<rule pattern="\\(x[a-fA-F0-9]{2}|u[a-fA-F0-9]{4}|U[a-fA-F0-9]{6}|[nr\\t\'"])">
|
||||
<rule pattern="\\x[0-9a-fA-F]{2}">
|
||||
<token type="LiteralStringEscape"/>
|
||||
</rule>
|
||||
<rule pattern="\\u\{[0-9a-fA-F]+\}">
|
||||
<token type="LiteralStringEscape"/>
|
||||
</rule>
|
||||
<rule pattern="\\[nrt'"\\]">
|
||||
<token type="LiteralStringEscape"/>
|
||||
</rule>
|
||||
<rule pattern="[^\\"\n]+">
|
||||
@@ -18,99 +24,164 @@
|
||||
<token type="LiteralString"/>
|
||||
<pop depth="1"/>
|
||||
</rule>
|
||||
<rule pattern="\\">
|
||||
<token type="LiteralString"/>
|
||||
</rule>
|
||||
</state>
|
||||
<state name="quoted_ident">
|
||||
<rule pattern="\\x[0-9a-fA-F]{2}">
|
||||
<token type="LiteralStringEscape"/>
|
||||
</rule>
|
||||
<rule pattern="\\u\{[0-9a-fA-F]+\}">
|
||||
<token type="LiteralStringEscape"/>
|
||||
</rule>
|
||||
<rule pattern="\\[nrt'"\\]">
|
||||
<token type="LiteralStringEscape"/>
|
||||
</rule>
|
||||
<rule pattern="[^\\"\n]+">
|
||||
<token type="Name"/>
|
||||
</rule>
|
||||
<rule pattern=""">
|
||||
<token type="Name"/>
|
||||
<pop depth="1"/>
|
||||
</rule>
|
||||
<rule pattern="\\">
|
||||
<token type="Name"/>
|
||||
</rule>
|
||||
</state>
|
||||
<state name="root">
|
||||
<!-- Whitespace -->
|
||||
<rule pattern="\n">
|
||||
<token type="TextWhitespace"/>
|
||||
</rule>
|
||||
<rule pattern="\s+">
|
||||
<token type="TextWhitespace"/>
|
||||
</rule>
|
||||
<rule pattern="//.*?\n">
|
||||
<!-- Comments -->
|
||||
<rule pattern="//!.*$">
|
||||
<token type="CommentSpecial"/>
|
||||
</rule>
|
||||
<rule pattern="///.*$">
|
||||
<token type="CommentSpecial"/>
|
||||
</rule>
|
||||
<rule pattern="//.*$">
|
||||
<token type="CommentSingle"/>
|
||||
</rule>
|
||||
<rule pattern="(unreachable|continue|errdefer|suspend|return|resume|cancel|break|catch|async|await|defer|asm|try)\b">
|
||||
<!-- Statement keywords -->
|
||||
<rule pattern="(break|return|continue|asm|defer|errdefer|unreachable|try|catch|suspend|resume|nosuspend)\b">
|
||||
<token type="Keyword"/>
|
||||
</rule>
|
||||
<rule pattern="(threadlocal|linksection|allowzero|stdcallcc|volatile|comptime|noalias|nakedcc|inline|export|packed|extern|align|const|pub|var)\b">
|
||||
<!-- Storage/reserved keywords -->
|
||||
<rule pattern="(const|var|extern|packed|export|pub|noalias|inline|noinline|comptime|volatile|allowzero|align|addrspace|linksection|threadlocal|callconv)\b">
|
||||
<token type="KeywordReserved"/>
|
||||
</rule>
|
||||
<rule pattern="(struct|union|error|enum)\b">
|
||||
<!-- Structure keywords -->
|
||||
<rule pattern="(struct|enum|union|error|opaque)\b">
|
||||
<token type="Keyword"/>
|
||||
</rule>
|
||||
<!-- Repeat keywords -->
|
||||
<rule pattern="(while|for)\b">
|
||||
<token type="Keyword"/>
|
||||
</rule>
|
||||
<rule pattern="(comptime_float|comptime_int|c_longdouble|c_ulonglong|c_longlong|c_voidi8|noreturn|c_ushort|anyerror|promise|c_short|c_ulong|c_uint|c_long|isize|c_int|usize|void|f128|i128|type|bool|u128|u16|f64|f32|u64|i16|f16|i32|u32|i64|u8|i0|u0)\b">
|
||||
<!-- Type keywords -->
|
||||
<rule pattern="(bool|void|noreturn|type|anyerror|anyopaque|f16|f32|f64|f80|f128|i8|u8|i16|u16|i32|u32|i64|u64|i128|u128|isize|usize|comptime_int|comptime_float|c_char|c_short|c_ushort|c_int|c_uint|c_long|c_ulong|c_longlong|c_ulonglong|c_longdouble)\b">
|
||||
<token type="KeywordType"/>
|
||||
</rule>
|
||||
<rule pattern="(undefined|false|true|null)\b">
|
||||
<!-- Constant keywords -->
|
||||
<rule pattern="(true|false|null|undefined)\b">
|
||||
<token type="KeywordConstant"/>
|
||||
</rule>
|
||||
<rule pattern="(switch|orelse|else|and|if|or)\b">
|
||||
<!-- Conditional keywords -->
|
||||
<rule pattern="(if|else|switch|and|or|orelse)\b">
|
||||
<token type="Keyword"/>
|
||||
</rule>
|
||||
<rule pattern="(usingnamespace|test|fn)\b">
|
||||
<!-- Other keywords -->
|
||||
<rule pattern="(fn|test|anyframe|anytype)\b">
|
||||
<token type="Keyword"/>
|
||||
</rule>
|
||||
<rule pattern="0x[0-9a-fA-F]+\.[0-9a-fA-F]+([pP][\-+]?[0-9a-fA-F]+)?">
|
||||
<!-- Hex float -->
|
||||
<rule pattern="0x[0-9a-fA-F][0-9a-fA-F_]*(?:\.[0-9a-fA-F][0-9a-fA-F_]*(?:[pP][-+]?[0-9][0-9_]*)?|[pP][-+]?[0-9][0-9_]*)">
|
||||
<token type="LiteralNumberFloat"/>
|
||||
</rule>
|
||||
<rule pattern="0x[0-9a-fA-F]+\.?[pP][\-+]?[0-9a-fA-F]+">
|
||||
<!-- Decimal float -->
|
||||
<rule pattern="[0-9][0-9_]*(?:\.[0-9][0-9_]*(?:[eE][-+]?[0-9][0-9_]*)?|[eE][-+]?[0-9][0-9_]*)">
|
||||
<token type="LiteralNumberFloat"/>
|
||||
</rule>
|
||||
<rule pattern="[0-9]+\.[0-9]+([eE][-+]?[0-9]+)?">
|
||||
<token type="LiteralNumberFloat"/>
|
||||
</rule>
|
||||
<rule pattern="[0-9]+\.?[eE][-+]?[0-9]+">
|
||||
<token type="LiteralNumberFloat"/>
|
||||
</rule>
|
||||
<rule pattern="0b(?:_?[01])+">
|
||||
<!-- Binary -->
|
||||
<rule pattern="0b[01][01_]*">
|
||||
<token type="LiteralNumberBin"/>
|
||||
</rule>
|
||||
<rule pattern="0o(?:_?[0-7])+">
|
||||
<!-- Octal -->
|
||||
<rule pattern="0o[0-7][0-7_]*">
|
||||
<token type="LiteralNumberOct"/>
|
||||
</rule>
|
||||
<rule pattern="0x(?:_?[0-9a-fA-F])+">
|
||||
<!-- Hex integer -->
|
||||
<rule pattern="0x[0-9a-fA-F][0-9a-fA-F_]*">
|
||||
<token type="LiteralNumberHex"/>
|
||||
</rule>
|
||||
<rule pattern="(?:_?[0-9])+">
|
||||
<!-- Decimal integer -->
|
||||
<rule pattern="[0-9][0-9_]*">
|
||||
<token type="LiteralNumberInteger"/>
|
||||
</rule>
|
||||
<rule pattern="\b[A-Za-z_]\w*(?=\s*\()">
|
||||
<token type="NameFunction"/>
|
||||
<!-- Quoted identifier -->
|
||||
<rule pattern="@"">
|
||||
<token type="Name"/>
|
||||
<push state="quoted_ident"/>
|
||||
</rule>
|
||||
<rule pattern="@[a-zA-Z_]\w*">
|
||||
<!-- Builtin -->
|
||||
<rule pattern="@[a-zA-Z_][a-zA-Z0-9_]*">
|
||||
<token type="NameBuiltin"/>
|
||||
</rule>
|
||||
<rule pattern="[a-zA-Z_]\w*">
|
||||
<!-- Function call -->
|
||||
<rule pattern="[a-zA-Z_][a-zA-Z0-9_]*(?=\s*\()">
|
||||
<token type="NameFunction"/>
|
||||
</rule>
|
||||
<!-- Identifier -->
|
||||
<rule pattern="[a-zA-Z_][a-zA-Z0-9_]*">
|
||||
<token type="Name"/>
|
||||
</rule>
|
||||
<rule pattern="\'\\\'\'">
|
||||
<token type="LiteralStringEscape"/>
|
||||
</rule>
|
||||
<rule pattern="\'\\(|x[a-fA-F0-9]{2}|u[a-fA-F0-9]{4}|U[a-fA-F0-9]{6}|[nr\\t\'"])\'">
|
||||
<token type="LiteralStringEscape"/>
|
||||
</rule>
|
||||
<rule pattern="\'[^\\\']\'">
|
||||
<token type="LiteralString"/>
|
||||
<!-- Character literal -->
|
||||
<rule pattern="'(?:\\x[0-9a-fA-F]{2}|\\u\{[0-9a-fA-F]+\}|\\[nrt'"\\]|[^\\'\n])'">
|
||||
<token type="LiteralStringChar"/>
|
||||
</rule>
|
||||
<!-- Multiline string -->
|
||||
<rule pattern="\\\\[^\n]*">
|
||||
<token type="LiteralStringHeredoc"/>
|
||||
</rule>
|
||||
<rule pattern="c\\\\[^\n]*">
|
||||
<token type="LiteralStringHeredoc"/>
|
||||
</rule>
|
||||
<rule pattern="c?"">
|
||||
<!-- String start -->
|
||||
<rule pattern=""">
|
||||
<token type="LiteralString"/>
|
||||
<push state="string"/>
|
||||
</rule>
|
||||
<rule pattern="[+%=><|^!?/\-*&~:]">
|
||||
<!-- 4-char operators -->
|
||||
<rule pattern="<<\|=">
|
||||
<token type="Operator"/>
|
||||
</rule>
|
||||
<rule pattern="[{}()\[\],.;]">
|
||||
<!-- 3-char operators -->
|
||||
<rule pattern="(?:\+%=|-%=|\*%=|\+\|=|-\|=|\*\|=|<<=|>>=|<<\|)">
|
||||
<token type="Operator"/>
|
||||
</rule>
|
||||
<!-- 2-char operators -->
|
||||
<rule pattern="(?:\+\+|\*\*|\|\||<<|>>|\+%|-%|\*%|\+\||-\||\*\||==|!=|<=|>=|\+=|-=|\*=|/=|%=|&=|\|=|\^=|\.\*|\.\?|\.\.)">
|
||||
<token type="Operator"/>
|
||||
</rule>
|
||||
<!-- 1-char operators -->
|
||||
<rule pattern="[+\-*/%&|^~!<>=]">
|
||||
<token type="Operator"/>
|
||||
</rule>
|
||||
<!-- Punctuation -->
|
||||
<rule pattern="\.\.\.">
|
||||
<token type="Punctuation"/>
|
||||
</rule>
|
||||
<rule pattern="=>">
|
||||
<token type="Punctuation"/>
|
||||
</rule>
|
||||
<rule pattern="->">
|
||||
<token type="Punctuation"/>
|
||||
</rule>
|
||||
<rule pattern="[{}()\[\],.;:?]">
|
||||
<token type="Punctuation"/>
|
||||
</rule>
|
||||
</state>
|
||||
</rules>
|
||||
</lexer>
|
||||
</lexer>
|
||||
|
||||
+168
@@ -0,0 +1,168 @@
|
||||
package lexers
|
||||
|
||||
import (
|
||||
. "github.com/alecthomas/chroma/v2" // nolint
|
||||
)
|
||||
|
||||
// Markless lexer.
|
||||
var Markless = Register(MustNewLexer(
|
||||
&Config{
|
||||
Name: "Markless",
|
||||
Aliases: []string{"mess"},
|
||||
Filenames: []string{"*.mess", "*.markless"},
|
||||
MimeTypes: []string{"text/x-markless"},
|
||||
},
|
||||
marklessRules,
|
||||
))
|
||||
|
||||
func marklessRules() Rules {
|
||||
return Rules{
|
||||
"root": {
|
||||
Include("block"),
|
||||
},
|
||||
// Block directives
|
||||
"block": {
|
||||
Include("header"),
|
||||
Include("ordered-list"),
|
||||
Include("unordered-list"),
|
||||
Include("code-block"),
|
||||
Include("blockquote"),
|
||||
Include("blockquote-header"),
|
||||
Include("align"),
|
||||
Include("comment"),
|
||||
Include("instruction"),
|
||||
Include("embed"),
|
||||
Include("footnote"),
|
||||
Include("horizontal-rule"),
|
||||
Include("paragraph"),
|
||||
},
|
||||
"header": {
|
||||
{`(# )(.*)$`, ByGroups(Keyword, GenericHeading), Push("inline")},
|
||||
{`(##+)(.*)$`, ByGroups(Keyword, GenericSubheading), Push("inline")},
|
||||
},
|
||||
"ordered-list": {
|
||||
{`([0-9]+\.)`, Keyword, nil},
|
||||
},
|
||||
"unordered-list": {
|
||||
{`(- )`, Keyword, nil},
|
||||
},
|
||||
"code-block": {
|
||||
{`(::+)( *)(\w*)([^\n]*)(\n)([\w\W]*?)(^\1$)`, UsingByGroup(3, 6, Keyword, TextWhitespace, NameFunction, String, TextWhitespace, Text, Keyword), nil},
|
||||
},
|
||||
"blockquote": {
|
||||
{`(\| )(.*)$`, ByGroups(Keyword, GenericInserted), nil},
|
||||
},
|
||||
"blockquote-header": {
|
||||
{`(~ )([^|\n]+)(\| )(.*?\n)`, ByGroups(Keyword, NameEntity, Keyword, GenericInserted), Push("inline-blockquote")},
|
||||
{`(~ )(.*)$`, ByGroups(Keyword, NameEntity), nil},
|
||||
},
|
||||
"inline-blockquote": {
|
||||
{`^( +)(\| )(.*$)`, ByGroups(TextWhitespace, Keyword, GenericInserted), nil},
|
||||
Default(Pop(1)),
|
||||
},
|
||||
"align": {
|
||||
{`(\|\|)|(\|<)|(\|>)|(><)`, Keyword, nil},
|
||||
},
|
||||
"comment": {
|
||||
{`(;[; ]).*?$`, CommentSingle, nil},
|
||||
},
|
||||
"instruction": {
|
||||
{`(! )([^ ]+)(.+?)$`, ByGroups(Keyword, NameFunction, NameVariable), nil},
|
||||
},
|
||||
"embed": {
|
||||
{`(\[ )([^ ]+)( )([^,]+)`, ByGroups(Keyword, NameFunction, TextWhitespace, String), Push("embed-options")},
|
||||
},
|
||||
"embed-options": {
|
||||
{`\\.`, Text, nil},
|
||||
{`,`, Punctuation, nil},
|
||||
{`\]?$`, Keyword, Pop(1)},
|
||||
// Generic key or key/value pair
|
||||
{`( *)([^, \]]+)([^,\]]+)?`, ByGroups(TextWhitespace, NameFunction, String), nil},
|
||||
{`.`, Text, nil},
|
||||
},
|
||||
"footnote": {
|
||||
{`(\[)([0-9]+)(\])`, ByGroups(Keyword, NameVariable, Keyword), Push("inline")},
|
||||
},
|
||||
"horizontal-rule": {
|
||||
{`(==+)$`, LiteralOther, nil},
|
||||
},
|
||||
"paragraph": {
|
||||
{` *`, TextWhitespace, Push("inline")},
|
||||
},
|
||||
// Inline directives
|
||||
"inline": {
|
||||
Include("escapes"),
|
||||
Include("dashes"),
|
||||
Include("newline"),
|
||||
Include("italic"),
|
||||
Include("underline"),
|
||||
Include("bold"),
|
||||
Include("strikethrough"),
|
||||
Include("code"),
|
||||
Include("compound"),
|
||||
Include("footnote-reference"),
|
||||
Include("subtext"),
|
||||
Include("subtext"),
|
||||
Include("url"),
|
||||
{`.`, Text, nil},
|
||||
{`\n`, TextWhitespace, Pop(1)},
|
||||
},
|
||||
"escapes": {
|
||||
{`\\.`, Text, nil},
|
||||
},
|
||||
"dashes": {
|
||||
{`-{2,3}`, TextPunctuation, nil},
|
||||
},
|
||||
"newline": {
|
||||
{`-/-`, TextWhitespace, nil},
|
||||
},
|
||||
"italic": {
|
||||
{`(//)(.*?)(\1)`, ByGroups(Keyword, GenericEmph, Keyword), nil},
|
||||
},
|
||||
"underline": {
|
||||
{`(__)(.*?)(\1)`, ByGroups(Keyword, GenericUnderline, Keyword), nil},
|
||||
},
|
||||
"bold": {
|
||||
{`(\*\*)(.*?)(\1)`, ByGroups(Keyword, GenericStrong, Keyword), nil},
|
||||
},
|
||||
"strikethrough": {
|
||||
{`(<-)(.*?)(->)`, ByGroups(Keyword, GenericDeleted, Keyword), nil},
|
||||
},
|
||||
"code": {
|
||||
{"(``+)(.*?)(\\1)", ByGroups(Keyword, LiteralStringBacktick, Keyword), nil},
|
||||
},
|
||||
"compound": {
|
||||
{`(''+)(.*?)(''\()`, ByGroups(Keyword, UsingSelf("inline"), Keyword), Push("compound-options")},
|
||||
},
|
||||
"compound-options": {
|
||||
{`\\.`, Text, nil},
|
||||
{`,`, Punctuation, nil},
|
||||
{`\)`, Keyword, Pop(1)},
|
||||
// Hex Color
|
||||
{` *#[0-9A-Fa-f]{3,6} *`, LiteralNumberHex, nil},
|
||||
// Named Color
|
||||
{` *(indian-red|light-coral|salmon|dark-salmon|light-salmon|crimson|red|firebrick|dark-red|pink|light-pink|hot-pink|deep-pink|medium-violet-red|pale-violet-red|coral|tomato|orange-red|dark-orange|orange|gold|yellow|light-yellow|lemon-chiffon|light-goldenrod-yellow|papayawhip|moccasin|peachpuff|pale-goldenrod|khaki|dark-khaki|lavender|thistle|plum|violet|orchid|fuchsia|magenta|medium-orchid|medium-purple|rebecca-purple|blue-violet|dark-violet|dark-orchid|dark-magenta|purple|indigo|slate-blue|dark-slate-blue|medium-slate-blue|green-yellow|chartreuse|lawn-green|lime|lime-green|pale-green|light-green|medium-spring-green|spring-green|medium-sea-green|sea-green|forest-green|green|dark-green|yellow-green|olive-drab|olive|dark-olive-green|medium-aquamarine|dark-sea-green|light-sea-green|dark-cyan|teal|aqua|cyan|light-cyan|pale-turquoise|aquamarine|turquoise|medium-turquoise|dark-turquoise|cadet-blue|steel-blue|light-steel-blue|powder-blue|light-blue|sky-blue|light-sky-blue|deep-sky-blue|dodger-blue|cornflower-blue|royal-blue|blue|medium-blue|dark-blue|navy|midnight-blue|cornsilk|blanched-almond|bisque|navajo-white|wheat|burlywood|tan|rosy-brown|sandy-brown|goldenrod|dark-goldenrod|peru|chocolate|saddle-brown|sienna|brown|maroon|white|snow|honeydew|mintcream|azure|alice-blue|ghost-white|white-smoke|seashell|beige|oldlace|floral-white|ivory|antique-white|linen|lavenderblush|mistyrose|gainsboro|light-gray|silver|dark-gray|gray|dim-gray|light-slate-gray|slate-gray|dark-slate-gray) *`, LiteralOther, nil},
|
||||
// Named size
|
||||
{` *(microscopic|tiny|small|normal|big|large|huge|gigantic) *`, NameTag, nil},
|
||||
// Options
|
||||
{` *(bold|italic|underline|strikethrough|subtext|supertext|spoiler) *`, NameBuiltin, nil},
|
||||
// URL. Note the missing ) and , in the match.
|
||||
{` *\w[-\w+.]*://[\w$\-_.+!*'(&/:;=?@z%#\\]+ *`, String, nil},
|
||||
// Generic key or key/value pair
|
||||
{`( *)([^, )]+)( [^,)]+)?`, ByGroups(TextWhitespace, NameFunction, String), nil},
|
||||
{`.`, Text, nil},
|
||||
},
|
||||
"footnote-reference": {
|
||||
{`(\[)([0-9]+)(\])`, ByGroups(Keyword, NameVariable, Keyword), nil},
|
||||
},
|
||||
"subtext": {
|
||||
{`(v\()(.*?)(\))`, ByGroups(Keyword, UsingSelf("inline"), Keyword), nil},
|
||||
},
|
||||
"supertext": {
|
||||
{`(\^\()(.*?)(\))`, ByGroups(Keyword, UsingSelf("inline"), Keyword), nil},
|
||||
},
|
||||
"url": {
|
||||
{`\w[-\w+.]*://[\w\$\-_.+!*'()&,/:;=?@z%#\\]+`, String, nil},
|
||||
},
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -101,7 +101,7 @@
|
||||
<entry type="GenericTraceback" style="#c55858"/>
|
||||
|
||||
<!-- Punctuation -->
|
||||
<entry type="Punctuation" style="#bdbdbd"/>
|
||||
<entry type="Punctuation" style="#c17ac8"/>
|
||||
<entry type="Text" style="#bdbdbd"/>
|
||||
<entry type="TextWhitespace" style="#bdbdbd"/>
|
||||
</style>
|
||||
|
||||
+1
-1
@@ -101,7 +101,7 @@
|
||||
<entry type="GenericTraceback" style="#ff6767"/>
|
||||
|
||||
<!-- Punctuation -->
|
||||
<entry type="Punctuation" style="#edecee"/>
|
||||
<entry type="Punctuation" style="#f694ff"/>
|
||||
<entry type="Text" style="#edecee"/>
|
||||
<entry type="TextWhitespace" style="#edecee"/>
|
||||
</style>
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
// Package chi is a small, idiomatic and composable router for building HTTP services.
|
||||
//
|
||||
// chi requires Go 1.14 or newer.
|
||||
// chi supports the four most recent major versions of Go.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
|
||||
+2
-7
@@ -2,6 +2,7 @@ package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"slices"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -29,13 +30,7 @@ func contentEncoding(ce string, charsets ...string) bool {
|
||||
_, ce = split(strings.ToLower(ce), ";")
|
||||
_, ce = split(ce, "charset=")
|
||||
ce, _ = split(ce, ";")
|
||||
for _, c := range charsets {
|
||||
if ce == c {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
return slices.Contains(charsets, ce)
|
||||
}
|
||||
|
||||
// Split a string in two parts, cleaning any whitespace.
|
||||
|
||||
+3
-3
@@ -25,7 +25,7 @@ const RequestIDKey ctxKeyRequestID = 0
|
||||
var RequestIDHeader = "X-Request-Id"
|
||||
|
||||
var prefix string
|
||||
var reqid uint64
|
||||
var reqid atomic.Uint64
|
||||
|
||||
// A quick note on the statistics here: we're trying to calculate the chance that
|
||||
// two randomly generated base62 prefixes will collide. We use the formula from
|
||||
@@ -69,7 +69,7 @@ func RequestID(next http.Handler) http.Handler {
|
||||
ctx := r.Context()
|
||||
requestID := r.Header.Get(RequestIDHeader)
|
||||
if requestID == "" {
|
||||
myid := atomic.AddUint64(&reqid, 1)
|
||||
myid := reqid.Add(1)
|
||||
requestID = fmt.Sprintf("%s-%06d", prefix, myid)
|
||||
}
|
||||
ctx = context.WithValue(ctx, RequestIDKey, requestID)
|
||||
@@ -92,5 +92,5 @@ func GetReqID(ctx context.Context) string {
|
||||
|
||||
// NextRequestID generates the next request ID in the sequence.
|
||||
func NextRequestID() uint64 {
|
||||
return atomic.AddUint64(&reqid, 1)
|
||||
return reqid.Add(1)
|
||||
}
|
||||
|
||||
+1
@@ -79,6 +79,7 @@ func (hr HeaderRouter) Handler(next http.Handler) http.Handler {
|
||||
if len(hr) == 0 {
|
||||
// skip if no routes set
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// find first matching header route, and continue
|
||||
|
||||
+9
-2
@@ -47,15 +47,22 @@ func RedirectSlashes(next http.Handler) http.Handler {
|
||||
} else {
|
||||
path = r.URL.Path
|
||||
}
|
||||
|
||||
if len(path) > 1 && path[len(path)-1] == '/' {
|
||||
// Trim all leading and trailing slashes (e.g., "//evil.com", "/some/path//")
|
||||
path = "/" + strings.Trim(path, "/")
|
||||
// Normalize backslashes to forward slashes to prevent "/\evil.com" style redirects
|
||||
// that some clients may interpret as protocol-relative.
|
||||
path = strings.ReplaceAll(path, `\`, `/`)
|
||||
|
||||
// Collapse leading/trailing slashes and force a single leading slash.
|
||||
path := "/" + strings.Trim(path, "/")
|
||||
|
||||
if r.URL.RawQuery != "" {
|
||||
path = fmt.Sprintf("%s?%s", path, r.URL.RawQuery)
|
||||
}
|
||||
http.Redirect(w, r, path, 301)
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
}
|
||||
return http.HandlerFunc(fn)
|
||||
|
||||
+4
-2
@@ -467,8 +467,10 @@ func (mx *Mux) routeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// Find the route
|
||||
if _, _, h := mx.tree.FindRoute(rctx, method, routePath); h != nil {
|
||||
if supportsPathValue {
|
||||
setPathValue(rctx, r)
|
||||
// Set http.Request path values from our request context
|
||||
for i, key := range rctx.URLParams.Keys {
|
||||
value := rctx.URLParams.Values[i]
|
||||
r.SetPathValue(key, value)
|
||||
}
|
||||
if supportsPattern {
|
||||
setPattern(rctx, r)
|
||||
|
||||
-21
@@ -1,21 +0,0 @@
|
||||
//go:build go1.22 && !tinygo
|
||||
// +build go1.22,!tinygo
|
||||
|
||||
|
||||
package chi
|
||||
|
||||
import "net/http"
|
||||
|
||||
// supportsPathValue is true if the Go version is 1.22 and above.
|
||||
//
|
||||
// If this is true, `net/http.Request` has methods `SetPathValue` and `PathValue`.
|
||||
const supportsPathValue = true
|
||||
|
||||
// setPathValue sets the path values in the Request value
|
||||
// based on the provided request context.
|
||||
func setPathValue(rctx *Context, r *http.Request) {
|
||||
for i, key := range rctx.URLParams.Keys {
|
||||
value := rctx.URLParams.Values[i]
|
||||
r.SetPathValue(key, value)
|
||||
}
|
||||
}
|
||||
-19
@@ -1,19 +0,0 @@
|
||||
//go:build !go1.22 || tinygo
|
||||
// +build !go1.22 tinygo
|
||||
|
||||
package chi
|
||||
|
||||
import "net/http"
|
||||
|
||||
// supportsPathValue is true if the Go version is 1.22 and above.
|
||||
//
|
||||
// If this is true, `net/http.Request` has methods `SetPathValue` and `PathValue`.
|
||||
const supportsPathValue = false
|
||||
|
||||
// setPathValue sets the path values in the Request value
|
||||
// based on the provided request context.
|
||||
//
|
||||
// setPathValue is only supported in Go 1.22 and above so
|
||||
// this is just a blank function so that it compiles.
|
||||
func setPathValue(rctx *Context, r *http.Request) {
|
||||
}
|
||||
+7
-14
@@ -71,6 +71,7 @@ func RegisterMethod(method string) {
|
||||
}
|
||||
mt := methodTyp(2 << n)
|
||||
methodMap[method] = mt
|
||||
reverseMethodMap[mt] = method
|
||||
mALL |= mt
|
||||
}
|
||||
|
||||
@@ -328,7 +329,7 @@ func (n *node) replaceChild(label, tail byte, child *node) {
|
||||
|
||||
func (n *node) getEdge(ntyp nodeTyp, label, tail byte, prefix string) *node {
|
||||
nds := n.children[ntyp]
|
||||
for i := 0; i < len(nds); i++ {
|
||||
for i := range nds {
|
||||
if nds[i].label == label && nds[i].tail == tail {
|
||||
if ntyp == ntRegexp && nds[i].prefix != prefix {
|
||||
continue
|
||||
@@ -429,9 +430,7 @@ func (n *node) findRoute(rctx *Context, method methodTyp, path string) *node {
|
||||
}
|
||||
|
||||
// serially loop through each node grouped by the tail delimiter
|
||||
for idx := 0; idx < len(nds); idx++ {
|
||||
xn = nds[idx]
|
||||
|
||||
for _, xn = range nds {
|
||||
// label for param nodes is the delimiter byte
|
||||
p := strings.IndexByte(xsearch, xn.tail)
|
||||
|
||||
@@ -770,20 +769,14 @@ func patParamKeys(pattern string) []string {
|
||||
}
|
||||
}
|
||||
|
||||
// longestPrefix finds the length of the shared prefix
|
||||
// of two strings
|
||||
func longestPrefix(k1, k2 string) int {
|
||||
max := len(k1)
|
||||
if l := len(k2); l < max {
|
||||
max = l
|
||||
}
|
||||
var i int
|
||||
for i = 0; i < max; i++ {
|
||||
// longestPrefix finds the length of the shared prefix of two strings
|
||||
func longestPrefix(k1, k2 string) (i int) {
|
||||
for i = 0; i < min(len(k1), len(k2)); i++ {
|
||||
if k1[i] != k2[i] {
|
||||
break
|
||||
}
|
||||
}
|
||||
return i
|
||||
return
|
||||
}
|
||||
|
||||
type nodes []*node
|
||||
|
||||
+2
@@ -19,6 +19,8 @@ linters:
|
||||
- unconvert
|
||||
- unparam
|
||||
- unused
|
||||
- modernize
|
||||
- testifylint
|
||||
settings:
|
||||
goconst:
|
||||
min-len: 2
|
||||
|
||||
+141
@@ -143,6 +143,22 @@ The `Rewrite` middleware is designed to rewrite the URL path based on a given ru
|
||||
|
||||
For example, `Rewrite("^/sites/(.*)/settings/$", "/sites/settings/$1")` will change request's URL from `/sites/id1/settings/` to `/sites/settings/id1`
|
||||
|
||||
### CleanPath middleware
|
||||
|
||||
Cleans double slashes from URL path. For example, requests to `/users//1` or `//users////1` will be cleaned to `/users/1` before routing. Trailing slashes are preserved: `/api//v1/` becomes `/api/v1/`. Note: dot segments (`.` and `..`) are intentionally not cleaned to preserve routing semantics.
|
||||
|
||||
```go
|
||||
router.Use(rest.CleanPath)
|
||||
```
|
||||
|
||||
### StripSlashes middleware
|
||||
|
||||
Removes trailing slashes from URL path. For example, `/users/` becomes `/users`. The root path `/` is preserved.
|
||||
|
||||
```go
|
||||
router.Use(rest.StripSlashes)
|
||||
```
|
||||
|
||||
### NoCache middleware
|
||||
|
||||
Sets a number of HTTP headers to prevent a router (handler's) response from being cached by an upstream proxy and/or client.
|
||||
@@ -166,6 +182,131 @@ RealIP is a middleware that sets a http.Request's RemoteAddr to the results of p
|
||||
|
||||
Only public IPs are accepted from headers; private/loopback/link-local IPs are skipped. This makes the middleware compatible with CDN setups like Cloudflare where the leftmost IP in `X-Forwarded-For` is the actual client.
|
||||
|
||||
### CORS middleware
|
||||
|
||||
Handles Cross-Origin Resource Sharing, allowing controlled access from different origins.
|
||||
|
||||
```go
|
||||
// allow all origins (default)
|
||||
router.Use(rest.CORS())
|
||||
|
||||
// specific origins with credentials
|
||||
router.Use(rest.CORS(
|
||||
rest.CorsAllowedOrigins("https://app.example.com", "https://admin.example.com"),
|
||||
rest.CorsAllowCredentials(true),
|
||||
rest.CorsMaxAge(86400),
|
||||
))
|
||||
|
||||
// full configuration
|
||||
router.Use(rest.CORS(
|
||||
rest.CorsAllowedOrigins("https://app.example.com"),
|
||||
rest.CorsAllowedMethods("GET", "POST", "PUT", "DELETE"),
|
||||
rest.CorsAllowedHeaders("Authorization", "Content-Type", "X-Custom-Header"),
|
||||
rest.CorsExposedHeaders("X-Request-Id", "X-Total-Count"),
|
||||
rest.CorsAllowCredentials(true),
|
||||
rest.CorsMaxAge(3600),
|
||||
))
|
||||
```
|
||||
|
||||
Features:
|
||||
- Automatic preflight (OPTIONS) handling
|
||||
- Origin validation with case-insensitive matching
|
||||
- Credentials support (reflects origin instead of `*`)
|
||||
- Configurable cache duration for preflight results
|
||||
|
||||
Available options:
|
||||
- `CorsAllowedOrigins(origins...)` - allowed origins (default: `*`)
|
||||
- `CorsAllowedMethods(methods...)` - allowed HTTP methods (default: GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD)
|
||||
- `CorsAllowedHeaders(headers...)` - allowed request headers (default: Accept, Content-Type, Authorization, X-Requested-With)
|
||||
- `CorsExposedHeaders(headers...)` - headers exposed to client
|
||||
- `CorsAllowCredentials(bool)` - enable credentials (cookies, auth headers)
|
||||
- `CorsMaxAge(seconds)` - preflight cache duration
|
||||
|
||||
### Secure middleware
|
||||
|
||||
Adds security headers to responses. By default sets: `X-Frame-Options`, `X-Content-Type-Options`, `Referrer-Policy`, `X-XSS-Protection`, and `Strict-Transport-Security` (for HTTPS only).
|
||||
|
||||
```go
|
||||
// with sensible defaults
|
||||
router.Use(rest.Secure())
|
||||
|
||||
// with full security headers for web apps (adds CSP and Permissions-Policy)
|
||||
router.Use(rest.Secure(rest.SecAllHeaders()))
|
||||
|
||||
// with custom options
|
||||
router.Use(rest.Secure(
|
||||
rest.SecFrameOptions("SAMEORIGIN"),
|
||||
rest.SecReferrerPolicy("no-referrer"),
|
||||
rest.SecHSTS(86400, true, true),
|
||||
rest.SecContentSecurityPolicy("default-src 'self'"),
|
||||
rest.SecPermissionsPolicy("geolocation=(), camera=()"),
|
||||
))
|
||||
```
|
||||
|
||||
Default headers:
|
||||
- `X-Frame-Options: DENY` - prevents clickjacking
|
||||
- `X-Content-Type-Options: nosniff` - prevents MIME-type sniffing
|
||||
- `Referrer-Policy: strict-origin-when-cross-origin` - controls referrer information
|
||||
- `X-XSS-Protection: 1; mode=block` - enables XSS filtering (legacy browsers)
|
||||
- `Strict-Transport-Security: max-age=31536000; includeSubDomains` - enforces HTTPS (only sent over HTTPS)
|
||||
|
||||
Available options:
|
||||
- `SecFrameOptions(value)` - set X-Frame-Options (DENY, SAMEORIGIN)
|
||||
- `SecContentTypeNosniff(enable)` - enable/disable nosniff
|
||||
- `SecReferrerPolicy(policy)` - set Referrer-Policy
|
||||
- `SecContentSecurityPolicy(policy)` - set Content-Security-Policy
|
||||
- `SecPermissionsPolicy(policy)` - set Permissions-Policy
|
||||
- `SecHSTS(maxAge, includeSubdomains, preload)` - configure HSTS
|
||||
- `SecXSSProtection(value)` - set X-XSS-Protection
|
||||
- `SecAllHeaders()` - convenience option that sets CSP and Permissions-Policy with restrictive defaults
|
||||
|
||||
### CSRF middleware
|
||||
|
||||
Provides Cross-Site Request Forgery protection using modern browser Fetch metadata headers (`Sec-Fetch-Site`, `Origin`). For Go 1.25+, this wraps the stdlib's `http.CrossOriginProtection`. For earlier versions, a compatible custom implementation is used.
|
||||
|
||||
```go
|
||||
// basic protection
|
||||
protection := rest.NewCrossOriginProtection()
|
||||
router.Use(protection.Handler)
|
||||
|
||||
// with trusted origins for cross-origin requests
|
||||
protection := rest.NewCrossOriginProtection()
|
||||
if err := protection.AddTrustedOrigin("https://mobile.example.com"); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if err := protection.AddTrustedOrigin("https://admin.example.com"); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
router.Use(protection.Handler)
|
||||
|
||||
// with bypass patterns for webhooks or OAuth
|
||||
protection := rest.NewCrossOriginProtection()
|
||||
protection.AddBypassPattern("/api/webhook")
|
||||
protection.AddBypassPattern("/oauth/")
|
||||
router.Use(protection.Handler)
|
||||
|
||||
// with custom deny handler
|
||||
protection := rest.NewCrossOriginProtection()
|
||||
protection.SetDenyHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "CSRF validation failed", http.StatusForbidden)
|
||||
}))
|
||||
router.Use(protection.Handler)
|
||||
```
|
||||
|
||||
How it works:
|
||||
- Safe methods (GET, HEAD, OPTIONS) are always allowed
|
||||
- Checks `Sec-Fetch-Site` header for "same-origin" or "none"
|
||||
- Falls back to comparing `Origin` header with request `Host`
|
||||
- Requests without these headers are assumed same-origin (non-browser clients)
|
||||
|
||||
Available methods:
|
||||
- `NewCrossOriginProtection()` - creates new CSRF protection middleware
|
||||
- `AddTrustedOrigin(origin)` - adds origin allowed for cross-origin requests (format: "scheme://host[:port]")
|
||||
- `AddBypassPattern(pattern)` - adds URL pattern that bypasses protection (for webhooks, OAuth, etc.)
|
||||
- `SetDenyHandler(handler)` - sets custom handler for rejected requests (default: 403 Forbidden)
|
||||
- `Check(request)` - manually validates a request, returns error if blocked
|
||||
- `Handler(handler)` - wraps an http.Handler with CSRF protection
|
||||
|
||||
### Maybe middleware
|
||||
|
||||
Maybe middleware allows changing the flow of the middleware stack execution depending on the return
|
||||
|
||||
+1
-4
@@ -156,10 +156,7 @@ func (b *Benchmarks) Stats(interval time.Duration) BenchmarkStats {
|
||||
}
|
||||
|
||||
// ensure we calculate rate based on actual interval
|
||||
actualInterval := fnInterval.Sub(stInterval)
|
||||
if actualInterval < time.Second {
|
||||
actualInterval = time.Second
|
||||
}
|
||||
actualInterval := max(fnInterval.Sub(stInterval), time.Second)
|
||||
|
||||
return BenchmarkStats{
|
||||
Requests: requests,
|
||||
|
||||
+180
@@ -0,0 +1,180 @@
|
||||
package rest
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// CORSConfig defines CORS middleware configuration.
|
||||
// Use CorsOpt functions to customize.
|
||||
type CORSConfig struct {
|
||||
// AllowedOrigins is a list of origins that may access the resource.
|
||||
// use "*" to allow all origins (not recommended with credentials).
|
||||
// default: ["*"]
|
||||
AllowedOrigins []string
|
||||
// AllowedMethods is a list of methods the client is allowed to use.
|
||||
// default: GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD
|
||||
AllowedMethods []string
|
||||
// AllowedHeaders is a list of headers the client is allowed to send.
|
||||
// default: Accept, Content-Type, Authorization, X-Requested-With
|
||||
AllowedHeaders []string
|
||||
// ExposedHeaders is a list of headers that are safe to expose to the client.
|
||||
// default: empty
|
||||
ExposedHeaders []string
|
||||
// AllowCredentials indicates whether the request can include credentials.
|
||||
// when true, AllowedOrigins cannot be "*" (browser security restriction).
|
||||
// default: false
|
||||
AllowCredentials bool
|
||||
// MaxAge indicates how long (in seconds) the results of a preflight can be cached.
|
||||
// default: 0 (no caching)
|
||||
MaxAge int
|
||||
}
|
||||
|
||||
// CorsOpt is a functional option for CORSConfig
|
||||
type CorsOpt func(*CORSConfig)
|
||||
|
||||
// defaultCORSConfig returns config with sensible defaults
|
||||
func defaultCORSConfig() CORSConfig {
|
||||
return CORSConfig{
|
||||
AllowedOrigins: []string{"*"},
|
||||
AllowedMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"},
|
||||
AllowedHeaders: []string{"Accept", "Content-Type", "Authorization", "X-Requested-With"},
|
||||
ExposedHeaders: []string{},
|
||||
AllowCredentials: false,
|
||||
MaxAge: 0,
|
||||
}
|
||||
}
|
||||
|
||||
// CorsAllowedOrigins sets the list of allowed origins.
|
||||
// Use "*" to allow all origins (not recommended with credentials).
|
||||
func CorsAllowedOrigins(origins ...string) CorsOpt {
|
||||
return func(c *CORSConfig) {
|
||||
c.AllowedOrigins = origins
|
||||
}
|
||||
}
|
||||
|
||||
// CorsAllowedMethods sets the list of allowed HTTP methods.
|
||||
func CorsAllowedMethods(methods ...string) CorsOpt {
|
||||
return func(c *CORSConfig) {
|
||||
c.AllowedMethods = methods
|
||||
}
|
||||
}
|
||||
|
||||
// CorsAllowedHeaders sets the list of allowed request headers.
|
||||
func CorsAllowedHeaders(headers ...string) CorsOpt {
|
||||
return func(c *CORSConfig) {
|
||||
c.AllowedHeaders = headers
|
||||
}
|
||||
}
|
||||
|
||||
// CorsExposedHeaders sets the list of headers exposed to the client.
|
||||
func CorsExposedHeaders(headers ...string) CorsOpt {
|
||||
return func(c *CORSConfig) {
|
||||
c.ExposedHeaders = headers
|
||||
}
|
||||
}
|
||||
|
||||
// CorsAllowCredentials enables or disables credentials.
|
||||
// When true, AllowedOrigins cannot be "*".
|
||||
func CorsAllowCredentials(allow bool) CorsOpt {
|
||||
return func(c *CORSConfig) {
|
||||
c.AllowCredentials = allow
|
||||
}
|
||||
}
|
||||
|
||||
// CorsMaxAge sets how long (in seconds) preflight results can be cached.
|
||||
func CorsMaxAge(seconds int) CorsOpt {
|
||||
return func(c *CORSConfig) {
|
||||
c.MaxAge = seconds
|
||||
}
|
||||
}
|
||||
|
||||
// CORS is middleware that handles Cross-Origin Resource Sharing.
|
||||
// It handles preflight OPTIONS requests and sets appropriate headers.
|
||||
// By default allows all origins with common methods and headers.
|
||||
func CORS(opts ...CorsOpt) func(http.Handler) http.Handler {
|
||||
cfg := defaultCORSConfig()
|
||||
for _, opt := range opts {
|
||||
opt(&cfg)
|
||||
}
|
||||
|
||||
// pre-compute joined strings for performance
|
||||
methodsStr := strings.Join(cfg.AllowedMethods, ", ")
|
||||
headersStr := strings.Join(cfg.AllowedHeaders, ", ")
|
||||
exposedStr := strings.Join(cfg.ExposedHeaders, ", ")
|
||||
|
||||
// check if wildcard is used
|
||||
allowAll := len(cfg.AllowedOrigins) == 1 && cfg.AllowedOrigins[0] == "*"
|
||||
|
||||
// build origin lookup for O(1) check (only when not allowing all)
|
||||
var originSet map[string]bool
|
||||
if !allowAll {
|
||||
originSet = make(map[string]bool, len(cfg.AllowedOrigins))
|
||||
for _, o := range cfg.AllowedOrigins {
|
||||
originSet[strings.ToLower(o)] = true
|
||||
}
|
||||
}
|
||||
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
origin := r.Header.Get("Origin")
|
||||
|
||||
// no origin header means same-origin or non-browser request
|
||||
if origin == "" {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// check if origin is allowed
|
||||
var allowed bool
|
||||
if allowAll {
|
||||
allowed = true
|
||||
} else {
|
||||
allowed = originSet[strings.ToLower(origin)]
|
||||
}
|
||||
if !allowed {
|
||||
// origin not allowed, proceed without CORS headers
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// set Vary header for caching
|
||||
w.Header().Add("Vary", "Origin")
|
||||
|
||||
// set allowed origin
|
||||
if allowAll && !cfg.AllowCredentials {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
} else {
|
||||
// reflect the specific origin (required for credentials)
|
||||
w.Header().Set("Access-Control-Allow-Origin", origin)
|
||||
}
|
||||
|
||||
// set credentials header if enabled
|
||||
if cfg.AllowCredentials {
|
||||
w.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||
}
|
||||
|
||||
// handle preflight request
|
||||
if r.Method == http.MethodOptions && r.Header.Get("Access-Control-Request-Method") != "" {
|
||||
// preflight request
|
||||
w.Header().Set("Access-Control-Allow-Methods", methodsStr)
|
||||
w.Header().Set("Access-Control-Allow-Headers", headersStr)
|
||||
|
||||
if cfg.MaxAge > 0 {
|
||||
w.Header().Set("Access-Control-Max-Age", strconv.Itoa(cfg.MaxAge))
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
|
||||
// actual request - set exposed headers
|
||||
if exposedStr != "" {
|
||||
w.Header().Set("Access-Control-Expose-Headers", exposedStr)
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
+207
@@ -0,0 +1,207 @@
|
||||
//go:build !go1.25
|
||||
|
||||
package rest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// CrossOriginProtection provides CSRF protection using modern browser Fetch metadata.
|
||||
// It validates requests using Sec-Fetch-Site and Origin headers, rejecting cross-origin
|
||||
// state-changing requests. Safe methods (GET, HEAD, OPTIONS) are always allowed.
|
||||
//
|
||||
// For Go 1.25+, this wraps the stdlib http.CrossOriginProtection.
|
||||
// For earlier versions, it provides an equivalent custom implementation.
|
||||
type CrossOriginProtection struct {
|
||||
mu sync.RWMutex
|
||||
trustedOrigins map[string]bool
|
||||
bypassPatterns []string
|
||||
denyHandler http.Handler
|
||||
}
|
||||
|
||||
// NewCrossOriginProtection creates a new CSRF protection middleware.
|
||||
func NewCrossOriginProtection() *CrossOriginProtection {
|
||||
return &CrossOriginProtection{
|
||||
trustedOrigins: make(map[string]bool),
|
||||
}
|
||||
}
|
||||
|
||||
// AddTrustedOrigin adds an origin that should be allowed to make cross-origin requests.
|
||||
// The origin must be in the format "scheme://host" or "scheme://host:port".
|
||||
// Returns an error if the origin format is invalid.
|
||||
func (c *CrossOriginProtection) AddTrustedOrigin(origin string) error {
|
||||
u, err := url.Parse(origin)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid origin: %w", err)
|
||||
}
|
||||
if u.Scheme == "" || u.Host == "" {
|
||||
return fmt.Errorf("origin must have scheme and host: %s", origin)
|
||||
}
|
||||
if u.Path != "" && u.Path != "/" {
|
||||
return fmt.Errorf("origin must not have path: %s", origin)
|
||||
}
|
||||
if u.RawQuery != "" || u.Fragment != "" {
|
||||
return fmt.Errorf("origin must not have query or fragment: %s", origin)
|
||||
}
|
||||
|
||||
normalized := strings.ToLower(u.Scheme) + "://" + strings.ToLower(u.Host)
|
||||
|
||||
c.mu.Lock()
|
||||
c.trustedOrigins[normalized] = true
|
||||
c.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddBypassPattern adds a URL pattern that should bypass CSRF protection.
|
||||
// Patterns follow the same syntax as http.ServeMux (e.g., "/api/webhook", "/oauth/").
|
||||
// Use sparingly and only for endpoints that have alternative authentication.
|
||||
func (c *CrossOriginProtection) AddBypassPattern(pattern string) {
|
||||
c.mu.Lock()
|
||||
c.bypassPatterns = append(c.bypassPatterns, pattern)
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
// SetDenyHandler sets a custom handler for rejected requests.
|
||||
// If not set, rejected requests receive a 403 Forbidden response.
|
||||
func (c *CrossOriginProtection) SetDenyHandler(h http.Handler) {
|
||||
c.mu.Lock()
|
||||
c.denyHandler = h
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
// Check validates a request against CSRF protection rules.
|
||||
// Returns nil if the request is allowed, or an error describing why it was rejected.
|
||||
func (c *CrossOriginProtection) Check(r *http.Request) error {
|
||||
// safe methods are always allowed
|
||||
if isSafeMethod(r.Method) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// check bypass patterns
|
||||
if c.matchesBypassPattern(r.URL.Path) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// check Sec-Fetch-Site header (modern browsers)
|
||||
secFetchSite := r.Header.Get("Sec-Fetch-Site")
|
||||
if secFetchSite != "" {
|
||||
switch secFetchSite {
|
||||
case "same-origin", "none":
|
||||
return nil
|
||||
case "cross-site", "same-site":
|
||||
// check if origin is trusted
|
||||
origin := r.Header.Get("Origin")
|
||||
if origin != "" && c.isOriginTrusted(origin) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("cross-origin request blocked: Sec-Fetch-Site=%s", secFetchSite)
|
||||
}
|
||||
}
|
||||
|
||||
// fallback: check Origin header against Host
|
||||
origin := r.Header.Get("Origin")
|
||||
if origin != "" {
|
||||
// check if origin is trusted
|
||||
if c.isOriginTrusted(origin) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// compare origin host with request host
|
||||
originURL, err := url.Parse(origin)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid Origin header: %w", err)
|
||||
}
|
||||
|
||||
requestHost := r.Host
|
||||
if requestHost == "" {
|
||||
requestHost = r.URL.Host
|
||||
}
|
||||
|
||||
// normalize hosts for comparison
|
||||
originHost := strings.ToLower(originURL.Host)
|
||||
requestHost = strings.ToLower(requestHost)
|
||||
|
||||
if originHost != requestHost {
|
||||
return fmt.Errorf("cross-origin request blocked: origin %s does not match host %s", originHost, requestHost)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// no Sec-Fetch-Site or Origin headers - assume same-origin or non-browser request
|
||||
return nil
|
||||
}
|
||||
|
||||
// Handler wraps an http.Handler with CSRF protection.
|
||||
// Rejected requests receive a 403 Forbidden response (or custom deny handler).
|
||||
func (c *CrossOriginProtection) Handler(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if err := c.Check(r); err != nil {
|
||||
c.mu.RLock()
|
||||
deny := c.denyHandler
|
||||
c.mu.RUnlock()
|
||||
|
||||
if deny != nil {
|
||||
deny.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
http.Error(w, "Forbidden - CSRF check failed", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
h.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// isSafeMethod returns true for HTTP methods that don't modify state.
|
||||
func isSafeMethod(method string) bool {
|
||||
switch method {
|
||||
case http.MethodGet, http.MethodHead, http.MethodOptions:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// isOriginTrusted checks if the origin is in the trusted list.
|
||||
func (c *CrossOriginProtection) isOriginTrusted(origin string) bool {
|
||||
u, err := url.Parse(origin)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
normalized := strings.ToLower(u.Scheme) + "://" + strings.ToLower(u.Host)
|
||||
|
||||
c.mu.RLock()
|
||||
trusted := c.trustedOrigins[normalized]
|
||||
c.mu.RUnlock()
|
||||
return trusted
|
||||
}
|
||||
|
||||
// matchesBypassPattern checks if the path matches any bypass pattern.
|
||||
func (c *CrossOriginProtection) matchesBypassPattern(path string) bool {
|
||||
c.mu.RLock()
|
||||
patterns := make([]string, len(c.bypassPatterns))
|
||||
copy(patterns, c.bypassPatterns)
|
||||
c.mu.RUnlock()
|
||||
|
||||
for _, pattern := range patterns {
|
||||
if matchPattern(pattern, path) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// matchPattern implements simple pattern matching similar to http.ServeMux.
|
||||
func matchPattern(pattern, path string) bool {
|
||||
// exact match
|
||||
if pattern == path {
|
||||
return true
|
||||
}
|
||||
// prefix match for patterns ending with /
|
||||
if strings.HasSuffix(pattern, "/") && strings.HasPrefix(path, pattern) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
//go:build go1.25
|
||||
|
||||
package rest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// CrossOriginProtection provides CSRF protection using modern browser Fetch metadata.
|
||||
// It validates requests using Sec-Fetch-Site and Origin headers, rejecting cross-origin
|
||||
// state-changing requests. Safe methods (GET, HEAD, OPTIONS) are always allowed.
|
||||
//
|
||||
// For Go 1.25+, this wraps the stdlib http.CrossOriginProtection.
|
||||
// For earlier versions, it provides an equivalent custom implementation.
|
||||
type CrossOriginProtection struct {
|
||||
stdlib *http.CrossOriginProtection
|
||||
}
|
||||
|
||||
// NewCrossOriginProtection creates a new CSRF protection middleware.
|
||||
func NewCrossOriginProtection() *CrossOriginProtection {
|
||||
return &CrossOriginProtection{
|
||||
stdlib: http.NewCrossOriginProtection(),
|
||||
}
|
||||
}
|
||||
|
||||
// AddTrustedOrigin adds an origin that should be allowed to make cross-origin requests.
|
||||
// The origin must be in the format "scheme://host" or "scheme://host:port".
|
||||
// Returns an error if the origin format is invalid.
|
||||
func (c *CrossOriginProtection) AddTrustedOrigin(origin string) error {
|
||||
u, err := url.Parse(origin)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid origin: %w", err)
|
||||
}
|
||||
if u.Scheme == "" || u.Host == "" {
|
||||
return fmt.Errorf("origin must have scheme and host: %s", origin)
|
||||
}
|
||||
if u.Path != "" && u.Path != "/" {
|
||||
return fmt.Errorf("origin must not have path: %s", origin)
|
||||
}
|
||||
if u.RawQuery != "" || u.Fragment != "" {
|
||||
return fmt.Errorf("origin must not have query or fragment: %s", origin)
|
||||
}
|
||||
|
||||
// normalize to lowercase for consistent case-insensitive matching
|
||||
normalized := strings.ToLower(u.Scheme) + "://" + strings.ToLower(u.Host)
|
||||
return c.stdlib.AddTrustedOrigin(normalized)
|
||||
}
|
||||
|
||||
// AddBypassPattern adds a URL pattern that should bypass CSRF protection.
|
||||
// Patterns follow the same syntax as http.ServeMux (e.g., "/api/webhook", "/oauth/").
|
||||
// Use sparingly and only for endpoints that have alternative authentication.
|
||||
func (c *CrossOriginProtection) AddBypassPattern(pattern string) {
|
||||
c.stdlib.AddInsecureBypassPattern(pattern)
|
||||
}
|
||||
|
||||
// SetDenyHandler sets a custom handler for rejected requests.
|
||||
// If not set, rejected requests receive a 403 Forbidden response.
|
||||
func (c *CrossOriginProtection) SetDenyHandler(h http.Handler) {
|
||||
c.stdlib.SetDenyHandler(h)
|
||||
}
|
||||
|
||||
// Check validates a request against CSRF protection rules.
|
||||
// Returns nil if the request is allowed, or an error describing why it was rejected.
|
||||
func (c *CrossOriginProtection) Check(r *http.Request) error {
|
||||
// the stdlib Check method panics or returns void, so we use our own check logic
|
||||
// by creating a test handler and seeing if it gets called
|
||||
|
||||
// safe methods are always allowed
|
||||
switch r.Method {
|
||||
case http.MethodGet, http.MethodHead, http.MethodOptions:
|
||||
return nil
|
||||
}
|
||||
|
||||
// use a test to determine if request would be allowed
|
||||
allowed := false
|
||||
testHandler := http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {
|
||||
allowed = true
|
||||
})
|
||||
|
||||
// create a response recorder to capture the result
|
||||
rec := &discardResponseWriter{}
|
||||
c.stdlib.Handler(testHandler).ServeHTTP(rec, r)
|
||||
|
||||
if !allowed {
|
||||
return fmt.Errorf("cross-origin request blocked by CSRF protection")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Handler wraps an http.Handler with CSRF protection.
|
||||
// Rejected requests receive a 403 Forbidden response (or custom deny handler).
|
||||
func (c *CrossOriginProtection) Handler(h http.Handler) http.Handler {
|
||||
return c.stdlib.Handler(h)
|
||||
}
|
||||
|
||||
// discardResponseWriter is a minimal ResponseWriter for testing.
|
||||
type discardResponseWriter struct {
|
||||
header http.Header
|
||||
}
|
||||
|
||||
func (d *discardResponseWriter) Header() http.Header {
|
||||
if d.header == nil {
|
||||
d.header = make(http.Header)
|
||||
}
|
||||
return d.header
|
||||
}
|
||||
func (d *discardResponseWriter) Write(b []byte) (int, error) { return len(b), nil }
|
||||
func (d *discardResponseWriter) WriteHeader(_ int) {}
|
||||
+1
-1
@@ -20,7 +20,7 @@ var gzDefaultContentTypes = []string{
|
||||
}
|
||||
|
||||
var gzPool = sync.Pool{
|
||||
New: func() interface{} { return gzip.NewWriter(io.Discard) },
|
||||
New: func() any { return gzip.NewWriter(io.Discard) },
|
||||
}
|
||||
|
||||
type gzipResponseWriter struct {
|
||||
|
||||
+2
-2
@@ -32,7 +32,7 @@ type Middleware struct {
|
||||
|
||||
// Backend is logging backend
|
||||
type Backend interface {
|
||||
Logf(format string, args ...interface{})
|
||||
Logf(format string, args ...any)
|
||||
}
|
||||
|
||||
type logParts struct {
|
||||
@@ -51,7 +51,7 @@ type logParts struct {
|
||||
|
||||
type stdBackend struct{}
|
||||
|
||||
func (s stdBackend) Logf(format string, args ...interface{}) {
|
||||
func (s stdBackend) Logf(format string, args ...any) {
|
||||
log.Printf(format, args...)
|
||||
}
|
||||
|
||||
|
||||
+7
-4
@@ -36,14 +36,17 @@ func AppInfo(app, author, version string) func(http.Handler) http.Handler {
|
||||
return f
|
||||
}
|
||||
|
||||
// Ping middleware response with pong to /ping. Stops chain if ping request detected
|
||||
// Ping middleware response with pong to /ping. Stops chain if ping request detected.
|
||||
// Handles both GET and HEAD methods - HEAD returns headers only without body,
|
||||
// which is useful for lightweight health checks by monitoring tools.
|
||||
func Ping(next http.Handler) http.Handler {
|
||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if r.Method == "GET" && strings.HasSuffix(strings.ToLower(r.URL.Path), "/ping") {
|
||||
if (r.Method == "GET" || r.Method == "HEAD") && strings.HasSuffix(strings.ToLower(r.URL.Path), "/ping") {
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte("pong"))
|
||||
if r.Method == "GET" {
|
||||
_, _ = w.Write([]byte("pong"))
|
||||
}
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
|
||||
+1
-2
@@ -58,8 +58,7 @@ func Get(r *http.Request) (string, error) {
|
||||
|
||||
// check X-Forwarded-For, find leftmost public IP
|
||||
if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
|
||||
addresses := strings.Split(xff, ",")
|
||||
for _, addr := range addresses {
|
||||
for addr := range strings.SplitSeq(xff, ",") {
|
||||
ip := strings.TrimSpace(addr)
|
||||
if parsedIP := net.ParseIP(ip); isPublicIP(parsedIP) {
|
||||
return ip, nil
|
||||
|
||||
+4
-5
@@ -13,7 +13,7 @@ import (
|
||||
type JSON map[string]any
|
||||
|
||||
// RenderJSON sends data as json
|
||||
func RenderJSON(w http.ResponseWriter, data interface{}) {
|
||||
func RenderJSON(w http.ResponseWriter, data any) {
|
||||
buf := &bytes.Buffer{}
|
||||
enc := json.NewEncoder(buf)
|
||||
enc.SetEscapeHTML(true)
|
||||
@@ -35,9 +35,8 @@ func RenderJSONFromBytes(w http.ResponseWriter, r *http.Request, data []byte) er
|
||||
}
|
||||
|
||||
// RenderJSONWithHTML allows html tags and forces charset=utf-8
|
||||
func RenderJSONWithHTML(w http.ResponseWriter, r *http.Request, v interface{}) error {
|
||||
|
||||
encodeJSONWithHTML := func(v interface{}) ([]byte, error) {
|
||||
func RenderJSONWithHTML(w http.ResponseWriter, r *http.Request, v any) error {
|
||||
encodeJSONWithHTML := func(v any) ([]byte, error) {
|
||||
buf := &bytes.Buffer{}
|
||||
enc := json.NewEncoder(buf)
|
||||
enc.SetEscapeHTML(false)
|
||||
@@ -55,7 +54,7 @@ func RenderJSONWithHTML(w http.ResponseWriter, r *http.Request, v interface{}) e
|
||||
}
|
||||
|
||||
// renderJSONWithStatus sends data as json and enforces status code
|
||||
func renderJSONWithStatus(w http.ResponseWriter, data interface{}, code int) {
|
||||
func renderJSONWithStatus(w http.ResponseWriter, data any, code int) {
|
||||
buf := &bytes.Buffer{}
|
||||
enc := json.NewEncoder(buf)
|
||||
enc.SetEscapeHTML(true)
|
||||
|
||||
+73
@@ -9,6 +9,79 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// CleanPath middleware cleans double slashes from URL path.
|
||||
// For example, if a request is made to /users//1 or //users////1,
|
||||
// it will be cleaned to /users/1 before routing.
|
||||
// Trailing slashes are preserved: /users//1/ becomes /users/1/.
|
||||
// Dot segments (. and ..) are intentionally NOT cleaned to preserve routing semantics.
|
||||
func CleanPath(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
rctx := r.Context()
|
||||
// skip if already cleaned
|
||||
if _, ok := rctx.Value(contextKey("cleanpath")).(bool); ok {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
p := r.URL.Path
|
||||
cleaned := cleanDoubleSlashes(p)
|
||||
|
||||
if cleaned != p {
|
||||
r.URL.Path = cleaned
|
||||
if r.URL.RawPath != "" {
|
||||
// clean double slashes in RawPath separately to preserve percent-encoding
|
||||
r.URL.RawPath = cleanDoubleSlashes(r.URL.RawPath)
|
||||
}
|
||||
rctx = context.WithValue(rctx, contextKey("cleanpath"), true)
|
||||
r = r.WithContext(rctx)
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// cleanDoubleSlashes removes consecutive slashes from path while preserving
|
||||
// trailing slashes and dot segments (. and ..).
|
||||
func cleanDoubleSlashes(p string) string {
|
||||
if p == "" || p == "/" {
|
||||
return p
|
||||
}
|
||||
|
||||
var b strings.Builder
|
||||
b.Grow(len(p))
|
||||
|
||||
prevSlash := false
|
||||
for i := 0; i < len(p); i++ {
|
||||
c := p[i]
|
||||
if c == '/' {
|
||||
if !prevSlash {
|
||||
b.WriteByte(c)
|
||||
}
|
||||
prevSlash = true
|
||||
} else {
|
||||
b.WriteByte(c)
|
||||
prevSlash = false
|
||||
}
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// StripSlashes middleware removes trailing slashes from URL path.
|
||||
// For example, /users/1/ becomes /users/1.
|
||||
// The root path "/" is preserved.
|
||||
func StripSlashes(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
p := r.URL.Path
|
||||
if len(p) > 1 && p[len(p)-1] == '/' {
|
||||
r.URL.Path = p[:len(p)-1]
|
||||
if r.URL.RawPath != "" {
|
||||
r.URL.RawPath = strings.TrimSuffix(r.URL.RawPath, "/")
|
||||
}
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// Rewrite middleware with from->to rule. Supports regex (like nginx) and prevents multiple rewrites
|
||||
// example: Rewrite(`^/sites/(.*)/settings/$`, `/sites/settings/$1`
|
||||
func Rewrite(from, to string) func(http.Handler) http.Handler {
|
||||
|
||||
+207
@@ -0,0 +1,207 @@
|
||||
package rest
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// SecureConfig defines security headers configuration.
|
||||
// Use SecOpt functions to customize.
|
||||
type SecureConfig struct {
|
||||
// xFrameOptions sets X-Frame-Options header. Default: DENY
|
||||
XFrameOptions string
|
||||
// xContentTypeOptions sets X-Content-Type-Options. Default: nosniff
|
||||
XContentTypeOptions string
|
||||
// ReferrerPolicy sets Referrer-Policy header. Default: strict-origin-when-cross-origin
|
||||
ReferrerPolicy string
|
||||
// ContentSecurityPolicy sets Content-Security-Policy header. Default: empty (not set)
|
||||
ContentSecurityPolicy string
|
||||
// PermissionsPolicy sets Permissions-Policy header. Default: empty (not set)
|
||||
PermissionsPolicy string
|
||||
// sTSSeconds sets max-age for Strict-Transport-Security. 0 disables.
|
||||
// only sent when request uses HTTPS. Default: 31536000 (1 year)
|
||||
STSSeconds int
|
||||
// sTSIncludeSubdomains adds includeSubDomains to HSTS. Default: true
|
||||
STSIncludeSubdomains bool
|
||||
// sTSPreload adds preload flag to HSTS. Default: false
|
||||
STSPreload bool
|
||||
// xSSProtection sets X-XSS-Protection header. Default: 1; mode=block
|
||||
// note: this header is deprecated in modern browsers but still useful for older ones
|
||||
XSSProtection string
|
||||
}
|
||||
|
||||
// SecOpt is a functional option for SecureConfig
|
||||
type SecOpt func(*SecureConfig)
|
||||
|
||||
// defaultSecureConfig returns config with sensible defaults
|
||||
func defaultSecureConfig() SecureConfig {
|
||||
return SecureConfig{
|
||||
XFrameOptions: "DENY",
|
||||
XContentTypeOptions: "nosniff",
|
||||
ReferrerPolicy: "strict-origin-when-cross-origin",
|
||||
STSSeconds: 31536000, // 1 year
|
||||
STSIncludeSubdomains: true,
|
||||
STSPreload: false,
|
||||
XSSProtection: "1; mode=block",
|
||||
}
|
||||
}
|
||||
|
||||
// SecFrameOptions sets X-Frame-Options header.
|
||||
// Common values: "DENY", "SAMEORIGIN"
|
||||
func SecFrameOptions(value string) SecOpt {
|
||||
return func(c *SecureConfig) {
|
||||
c.XFrameOptions = value
|
||||
}
|
||||
}
|
||||
|
||||
// SecContentTypeNosniff enables or disables X-Content-Type-Options: nosniff
|
||||
func SecContentTypeNosniff(enable bool) SecOpt {
|
||||
return func(c *SecureConfig) {
|
||||
if enable {
|
||||
c.XContentTypeOptions = "nosniff"
|
||||
} else {
|
||||
c.XContentTypeOptions = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SecReferrerPolicy sets Referrer-Policy header.
|
||||
// Common values: "no-referrer", "same-origin", "strict-origin", "strict-origin-when-cross-origin"
|
||||
func SecReferrerPolicy(policy string) SecOpt {
|
||||
return func(c *SecureConfig) {
|
||||
c.ReferrerPolicy = policy
|
||||
}
|
||||
}
|
||||
|
||||
// SecContentSecurityPolicy sets Content-Security-Policy header.
|
||||
// Example: "default-src 'self'; script-src 'self'"
|
||||
func SecContentSecurityPolicy(policy string) SecOpt {
|
||||
return func(c *SecureConfig) {
|
||||
c.ContentSecurityPolicy = policy
|
||||
}
|
||||
}
|
||||
|
||||
// SecPermissionsPolicy sets Permissions-Policy header.
|
||||
// Example: "geolocation=(), microphone=()"
|
||||
func SecPermissionsPolicy(policy string) SecOpt {
|
||||
return func(c *SecureConfig) {
|
||||
c.PermissionsPolicy = policy
|
||||
}
|
||||
}
|
||||
|
||||
// SecHSTS configures Strict-Transport-Security header.
|
||||
// maxAge is in seconds (0 disables HSTS), includeSubdomains and preload are optional flags.
|
||||
// Note: HSTS header is only sent when the request is over HTTPS.
|
||||
func SecHSTS(maxAge int, includeSubdomains, preload bool) SecOpt {
|
||||
return func(c *SecureConfig) {
|
||||
c.STSSeconds = maxAge
|
||||
c.STSIncludeSubdomains = includeSubdomains
|
||||
c.STSPreload = preload
|
||||
}
|
||||
}
|
||||
|
||||
// SecXSSProtection sets X-XSS-Protection header.
|
||||
// Set to empty string to disable. Common values: "0", "1", "1; mode=block"
|
||||
func SecXSSProtection(value string) SecOpt {
|
||||
return func(c *SecureConfig) {
|
||||
c.XSSProtection = value
|
||||
}
|
||||
}
|
||||
|
||||
// SecAllHeaders is a convenience option to set common headers for secure web applications.
|
||||
// Sets CSP with self-only policy and restrictive permissions.
|
||||
func SecAllHeaders() SecOpt {
|
||||
return func(c *SecureConfig) {
|
||||
c.ContentSecurityPolicy = "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; form-action 'self'; frame-ancestors 'none'"
|
||||
c.PermissionsPolicy = "geolocation=(), microphone=(), camera=()"
|
||||
}
|
||||
}
|
||||
|
||||
// Secure is middleware that adds security headers to responses.
|
||||
// By default it sets: X-Frame-Options, X-Content-Type-Options, Referrer-Policy,
|
||||
// X-XSS-Protection, and Strict-Transport-Security (for HTTPS only).
|
||||
// Use SecOpt functions to customize the configuration.
|
||||
func Secure(opts ...SecOpt) func(http.Handler) http.Handler {
|
||||
cfg := defaultSecureConfig()
|
||||
for _, opt := range opts {
|
||||
opt(&cfg)
|
||||
}
|
||||
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// set security headers
|
||||
if cfg.XFrameOptions != "" {
|
||||
w.Header().Set("X-Frame-Options", cfg.XFrameOptions)
|
||||
}
|
||||
if cfg.XContentTypeOptions != "" {
|
||||
w.Header().Set("X-Content-Type-Options", cfg.XContentTypeOptions)
|
||||
}
|
||||
if cfg.ReferrerPolicy != "" {
|
||||
w.Header().Set("Referrer-Policy", cfg.ReferrerPolicy)
|
||||
}
|
||||
if cfg.XSSProtection != "" {
|
||||
w.Header().Set("X-XSS-Protection", cfg.XSSProtection)
|
||||
}
|
||||
if cfg.ContentSecurityPolicy != "" {
|
||||
w.Header().Set("Content-Security-Policy", cfg.ContentSecurityPolicy)
|
||||
}
|
||||
if cfg.PermissionsPolicy != "" {
|
||||
w.Header().Set("Permissions-Policy", cfg.PermissionsPolicy)
|
||||
}
|
||||
|
||||
// HSTS only for HTTPS connections
|
||||
if cfg.STSSeconds > 0 && isHTTPS(r) {
|
||||
sts := "max-age=" + strconv.Itoa(cfg.STSSeconds)
|
||||
if cfg.STSIncludeSubdomains {
|
||||
sts += "; includeSubDomains"
|
||||
}
|
||||
if cfg.STSPreload {
|
||||
sts += "; preload"
|
||||
}
|
||||
w.Header().Set("Strict-Transport-Security", sts)
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// isHTTPS checks if the request is over HTTPS by examining TLS state and common proxy headers
|
||||
func isHTTPS(r *http.Request) bool {
|
||||
// direct TLS connection
|
||||
if r.TLS != nil {
|
||||
return true
|
||||
}
|
||||
// check common proxy headers (case-insensitive)
|
||||
if strings.EqualFold(r.Header.Get("X-Forwarded-Proto"), "https") {
|
||||
return true
|
||||
}
|
||||
// check RFC 7239 Forwarded header
|
||||
if forwarded := r.Header.Get("Forwarded"); forwarded != "" {
|
||||
if forwardedProtoIsHTTPS(forwarded) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// forwardedProtoIsHTTPS parses RFC 7239 Forwarded header to check for proto=https.
|
||||
// The header format is: Forwarded: for=1.2.3.4;proto=https;by=proxy, for=5.6.7.8
|
||||
// Parameters are separated by semicolons, multiple forwarded elements by commas.
|
||||
func forwardedProtoIsHTTPS(header string) bool {
|
||||
// split by comma to get individual forwarded elements
|
||||
for element := range strings.SplitSeq(header, ",") {
|
||||
// split by semicolon to get parameters within element
|
||||
for param := range strings.SplitSeq(element, ";") {
|
||||
param = strings.TrimSpace(param)
|
||||
// check for proto=https (case-insensitive per RFC 7239)
|
||||
if len(param) > 6 && strings.EqualFold(param[:6], "proto=") {
|
||||
if strings.EqualFold(strings.TrimSpace(param[6:]), "https") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
+6
-5
@@ -140,11 +140,12 @@ A common use case would be integrating with different 3rd party signature
|
||||
providers, like key management services from various cloud providers or Hardware
|
||||
Security Modules (HSMs) or to implement additional standards.
|
||||
|
||||
| Extension | Purpose | Repo |
|
||||
| --------- | -------------------------------------------------------------------------------------------------------- | ------------------------------------------ |
|
||||
| GCP | Integrates with multiple Google Cloud Platform signing tools (AppEngine, IAM API, Cloud KMS) | https://github.com/someone1/gcp-jwt-go |
|
||||
| AWS | Integrates with AWS Key Management Service, KMS | https://github.com/matelang/jwt-go-aws-kms |
|
||||
| JWKS | Provides support for JWKS ([RFC 7517](https://datatracker.ietf.org/doc/html/rfc7517)) as a `jwt.Keyfunc` | https://github.com/MicahParks/keyfunc |
|
||||
| Extension | Purpose | Repo |
|
||||
| --------- | -------------------------------------------------------------------------------------------------------- | ------------------------------------------------- |
|
||||
| GCP | Integrates with multiple Google Cloud Platform signing tools (AppEngine, IAM API, Cloud KMS) | https://github.com/someone1/gcp-jwt-go |
|
||||
| AWS | Integrates with AWS Key Management Service, KMS | https://github.com/matelang/jwt-go-aws-kms |
|
||||
| JWKS | Provides support for JWKS ([RFC 7517](https://datatracker.ietf.org/doc/html/rfc7517)) as a `jwt.Keyfunc` | https://github.com/MicahParks/keyfunc |
|
||||
| TPM | Integrates with Trusted Platform Module (TPM) | https://github.com/salrashid123/golang-jwt-tpm |
|
||||
|
||||
*Disclaimer*: Unless otherwise specified, these integrations are maintained by
|
||||
third parties and should not be considered as a primary offer by any of the
|
||||
|
||||
+1
-1
@@ -97,7 +97,7 @@ Backwards compatible API change that was missed in 2.0.0.
|
||||
|
||||
There were two major reasons for breaking backwards compatibility with this update. The first was a refactor required to expand the width of the RSA and HMAC-SHA signing implementations. There will likely be no required code changes to support this change.
|
||||
|
||||
The second update, while unfortunately requiring a small change in integration, is required to open up this library to other signing methods. Not all keys used for all signing methods have a single standard on-disk representation. Requiring `[]byte` as the type for all keys proved too limiting. Additionally, this implementation allows for pre-parsed tokens to be reused, which might matter in an application that parses a high volume of tokens with a small set of keys. Backwards compatibilty has been maintained for passing `[]byte` to the RSA signing methods, but they will also accept `*rsa.PublicKey` and `*rsa.PrivateKey`.
|
||||
The second update, while unfortunately requiring a small change in integration, is required to open up this library to other signing methods. Not all keys used for all signing methods have a single standard on-disk representation. Requiring `[]byte` as the type for all keys proved too limiting. Additionally, this implementation allows for pre-parsed tokens to be reused, which might matter in an application that parses a high volume of tokens with a small set of keys. Backwards compatibility has been maintained for passing `[]byte` to the RSA signing methods, but they will also accept `*rsa.PublicKey` and `*rsa.PrivateKey`.
|
||||
|
||||
It is likely the only integration change required here will be to change `func(t *jwt.Token) ([]byte, error)` to `func(t *jwt.Token) (interface{}, error)` when calling `Parse`.
|
||||
|
||||
|
||||
+13
-11
@@ -76,13 +76,6 @@ func (p *Parser) ParseWithClaims(tokenString string, claims Claims, keyFunc Keyf
|
||||
}
|
||||
}
|
||||
|
||||
// Decode signature
|
||||
token.Signature, err = p.DecodeSegment(parts[2])
|
||||
if err != nil {
|
||||
return token, newError("could not base64 decode signature", ErrTokenMalformed, err)
|
||||
}
|
||||
text := strings.Join(parts[0:2], ".")
|
||||
|
||||
// Lookup key(s)
|
||||
if keyFunc == nil {
|
||||
// keyFunc was not provided. short circuiting validation
|
||||
@@ -94,11 +87,14 @@ func (p *Parser) ParseWithClaims(tokenString string, claims Claims, keyFunc Keyf
|
||||
return token, newError("error while executing keyfunc", ErrTokenUnverifiable, err)
|
||||
}
|
||||
|
||||
// Join together header and claims in order to verify them with the signature
|
||||
text := strings.Join(parts[0:2], ".")
|
||||
switch have := got.(type) {
|
||||
case VerificationKeySet:
|
||||
if len(have.Keys) == 0 {
|
||||
return token, newError("keyfunc returned empty verification key set", ErrTokenUnverifiable)
|
||||
}
|
||||
|
||||
// Iterate through keys and verify signature, skipping the rest when a match is found.
|
||||
// Return the last error if no match is found.
|
||||
for _, key := range have.Keys {
|
||||
@@ -131,7 +127,7 @@ func (p *Parser) ParseWithClaims(tokenString string, claims Claims, keyFunc Keyf
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// ParseUnverified parses the token but doesn't validate the signature.
|
||||
// ParseUnverified parses the token but does not validate the signature.
|
||||
//
|
||||
// WARNING: Don't use this method unless you know what you're doing.
|
||||
//
|
||||
@@ -146,7 +142,7 @@ func (p *Parser) ParseUnverified(tokenString string, claims Claims) (token *Toke
|
||||
|
||||
token = &Token{Raw: tokenString}
|
||||
|
||||
// parse Header
|
||||
// Parse Header
|
||||
var headerBytes []byte
|
||||
if headerBytes, err = p.DecodeSegment(parts[0]); err != nil {
|
||||
return token, parts, newError("could not base64 decode header", ErrTokenMalformed, err)
|
||||
@@ -155,7 +151,7 @@ func (p *Parser) ParseUnverified(tokenString string, claims Claims) (token *Toke
|
||||
return token, parts, newError("could not JSON decode header", ErrTokenMalformed, err)
|
||||
}
|
||||
|
||||
// parse Claims
|
||||
// Parse Claims
|
||||
token.Claims = claims
|
||||
|
||||
claimBytes, err := p.DecodeSegment(parts[1])
|
||||
@@ -196,6 +192,12 @@ func (p *Parser) ParseUnverified(tokenString string, claims Claims) (token *Toke
|
||||
return token, parts, newError("signing method (alg) is unspecified", ErrTokenUnverifiable)
|
||||
}
|
||||
|
||||
// Parse token signature
|
||||
token.Signature, err = p.DecodeSegment(parts[2])
|
||||
if err != nil {
|
||||
return token, parts, newError("could not base64 decode signature", ErrTokenMalformed, err)
|
||||
}
|
||||
|
||||
return token, parts, nil
|
||||
}
|
||||
|
||||
@@ -216,7 +218,7 @@ func splitToken(token string) ([]string, bool) {
|
||||
parts[1] = claims
|
||||
// One more cut to ensure the signature is the last part of the token and there are no more
|
||||
// delimiters. This avoids an issue where malicious input could contain additional delimiters
|
||||
// causing unecessary overhead parsing tokens.
|
||||
// causing unnecessary overhead parsing tokens.
|
||||
signature, _, unexpected := strings.Cut(remain, tokenDelimiter)
|
||||
if unexpected {
|
||||
return nil, false
|
||||
|
||||
+9
-3
@@ -3,9 +3,7 @@ package jwt
|
||||
import "time"
|
||||
|
||||
// ParserOption is used to implement functional-style options that modify the
|
||||
// behavior of the parser. To add new options, just create a function (ideally
|
||||
// beginning with With or Without) that returns an anonymous function that takes
|
||||
// a *Parser type as input and manipulates its configuration accordingly.
|
||||
// behavior of the parser.
|
||||
type ParserOption func(*Parser)
|
||||
|
||||
// WithValidMethods is an option to supply algorithm methods that the parser
|
||||
@@ -66,6 +64,14 @@ func WithExpirationRequired() ParserOption {
|
||||
}
|
||||
}
|
||||
|
||||
// WithNotBeforeRequired returns the ParserOption to make nbf claim required.
|
||||
// By default nbf claim is optional.
|
||||
func WithNotBeforeRequired() ParserOption {
|
||||
return func(p *Parser) {
|
||||
p.validator.requireNbf = true
|
||||
}
|
||||
}
|
||||
|
||||
// WithAudience configures the validator to require any of the specified
|
||||
// audiences in the `aud` claim. Validation will fail if the audience is not
|
||||
// listed in the token or the `aud` claim is missing.
|
||||
|
||||
+4
-2
@@ -32,8 +32,8 @@ type Token struct {
|
||||
Method SigningMethod // Method is the signing method used or to be used
|
||||
Header map[string]any // Header is the first segment of the token in decoded form
|
||||
Claims Claims // Claims is the second segment of the token in decoded form
|
||||
Signature []byte // Signature is the third segment of the token in decoded form. Populated when you Parse a token
|
||||
Valid bool // Valid specifies if the token is valid. Populated when you Parse/Verify a token
|
||||
Signature []byte // Signature is the third segment of the token in decoded form. Populated when you [Parse] or sign a token
|
||||
Valid bool // Valid specifies if the token is valid. Populated when you [Parse] a token
|
||||
}
|
||||
|
||||
// New creates a new [Token] with the specified signing method and an empty map
|
||||
@@ -71,6 +71,8 @@ func (t *Token) SignedString(key any) (string, error) {
|
||||
return "", err
|
||||
}
|
||||
|
||||
t.Signature = sig
|
||||
|
||||
return sstr + "." + t.EncodeSegment(sig), nil
|
||||
}
|
||||
|
||||
|
||||
+6
-2
@@ -44,6 +44,9 @@ type Validator struct {
|
||||
// requireExp specifies whether the exp claim is required
|
||||
requireExp bool
|
||||
|
||||
// requireNbf specifies whether the nbf claim is required
|
||||
requireNbf bool
|
||||
|
||||
// verifyIat specifies whether the iat (Issued At) claim will be verified.
|
||||
// According to https://www.rfc-editor.org/rfc/rfc7519#section-4.1.6 this
|
||||
// only specifies the age of the token, but no validation check is
|
||||
@@ -111,8 +114,9 @@ func (v *Validator) Validate(claims Claims) error {
|
||||
}
|
||||
|
||||
// We always need to check not-before, but usage of the claim itself is
|
||||
// OPTIONAL.
|
||||
if err = v.verifyNotBefore(claims, now, false); err != nil {
|
||||
// OPTIONAL by default. requireNbf overrides this behavior and makes
|
||||
// the nbf claim mandatory.
|
||||
if err = v.verifyNotBefore(claims, now, v.requireNbf); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user