Compare commits

...
24 Commits
Author SHA1 Message Date
Joona HoikkalaandGitHub 4f5fad0d32 Prepare for point release (#150)
* Prepare for point release
2019-02-06 12:13:00 +02:00
Ward VandewegeandJoona Hoikkala 5651772837 Make sure to initialize the SOA record for the tcp server, too. (#149) 2019-02-06 12:09:01 +02:00
Joona HoikkalaandGitHub 09dc25d336 Update vendored dependencies (#147)
* Prepare readme for release

* Update vendored packages

* New version of dns dependency handles rejecting UPDATEs under the hood

* Go 1.11 required
2019-02-04 00:21:42 +02:00
Joona HoikkalaandGitHub a88ee29755 Prepare readme for release (#146) 2019-02-03 20:32:02 +02:00
Joona HoikkalaandGitHub de7fe3cb1d Fix responses to be more standards compliant (#145)
* Handle OPT questions (EDNS)

* Handle authoritative bit, and append SOA for authoritative NXDOMAIN responses

* Changelog entry

* Fix linter errors
2019-02-03 20:19:15 +02:00
Joona HoikkalaandGitHub 7a2f9f06b1 Refactoring DNS server part for safer paraller execution (#144)
* Refactoring DNS server part for safer paraller execution and better data structures

* Fix linter issues
2019-02-03 17:23:04 +02:00
Joona HoikkalaandGitHub d695f72963 Fix the default configuration SQLite db path (#143) 2019-02-01 09:55:26 +02:00
Marvin BlumandJoona Hoikkala aff13a02fb Added http health check endpoint. (#137)
* Added http health check endpoint.

* Fixed performing POST on GET endpoint.

* Explicitly return http status 200 in health check endpoint.

* Updated changelog.
2019-01-25 19:22:53 +02:00
Joona HoikkalaandGitHub 7fbb5261c8 Fix test not to assume preset UseHeaders value (#139) 2019-01-25 18:36:29 +02:00
Philip VieiraandJoona Hoikkala 2da94f1462 Clarify how to test that the DNS Lookup is working (#133)
Reasoning: Unless you specify that you're looking for a TXT record, you will not get the answer from the DNS server
2019-01-20 18:23:07 +02:00
Christopher CookandJoona Hoikkala ea4d125663 Add Certify The Web to clients list (#131)
Certify now supports acme-dns in the latest beta.
https://github.com/webprofusion/certify/issues/391
2018-12-18 11:05:01 +02:00
Joona HoikkalaandGitHub f64de0353d Fix Docker instructions and add option to bind both UDP and TCP DNS listeners (#130) 2018-12-13 12:19:10 +02:00
Joona HoikkalaandGitHub 20411b650f Update changelog and prepare for release v0.6 (#125) 2018-10-31 15:13:29 +02:00
Joona HoikkalaandGitHub 3bb130d055 Refuse dynamic updates properly (#124) 2018-10-31 15:01:42 +02:00
Joona HoikkalaandGitHub 1d85bf75b0 Added Traefik and cert-manager to client list (#123) 2018-10-31 01:13:10 +02:00
Joona HoikkalaandGitHub c2c5c5cd70 Better error handling in goroutines (#122)
* More robust goroutine error handling using channels

* Fix tests and make startup log msg saner

* Clarification to README and config file
2018-10-31 00:54:51 +02:00
golint fixerandJoona Hoikkala a09073da12 Fix golint import path (#121) 2018-10-29 10:29:34 +02:00
Julien SchmidtandJoona Hoikkala 4bb5e265ab README: use absolute path for sqlite DB in example config (#116) 2018-09-28 11:40:50 +03:00
Julien SchmidtandJoona Hoikkala b452d504c8 README: fix NS address in test command (#115) 2018-09-28 11:14:31 +03:00
Yannik SembritzkiandJoona Hoikkala f76790426d fix inconsistency between README and actual config (#114)
* fix inconsistency between README and actual config

* clarify example config in sync w/ config.cfg
2018-09-28 00:46:07 +03:00
Yannik SembritzkiandJoona Hoikkala f650e47fe5 fix README inconsistencies (#113) 2018-09-28 00:33:51 +03:00
Yannik SembritzkiandJoona Hoikkala dc0dd43017 simplify example dns config (#106) 2018-09-27 23:58:37 +03:00
Julien SchmidtandJoona Hoikkala c827ee4801 README: fix section of HTTP header vars in example config (#112) 2018-09-27 23:51:31 +03:00
Stefan BethkeandJoona Hoikkala db2a6bc288 Cmd line flag -c for config location (#108)
Add command line parsing and a flag `-c` to specify where the config
file should be loaded from.
2018-09-21 13:38:23 +03:00
736 changed files with 101282 additions and 33471 deletions
+1
View File
@@ -3,3 +3,4 @@ acme-dns.db
acme-dns.log acme-dns.log
.vagrant .vagrant
coverage.out coverage.out
.idea/
+2 -2
View File
@@ -1,10 +1,10 @@
language: go language: go
go: go:
- 1.9 - 1.11
env: env:
- "PATH=/home/travis/gopath/bin:$PATH" - "PATH=/home/travis/gopath/bin:$PATH"
before_install: before_install:
- go get github.com/golang/lint/golint - go get golang.org/x/lint/golint
- go get github.com/mattn/goveralls - go get github.com/mattn/goveralls
script: script:
- go vet - go vet
+2 -1
View File
@@ -1,4 +1,4 @@
FROM golang:1.9.2-alpine AS builder FROM golang:1.11.4-alpine3.8 AS builder
LABEL maintainer="joona@kuori.org" LABEL maintainer="joona@kuori.org"
RUN apk add --update gcc musl-dev git RUN apk add --update gcc musl-dev git
@@ -19,3 +19,4 @@ RUN apk --no-cache add ca-certificates && update-ca-certificates
VOLUME ["/etc/acme-dns", "/var/lib/acme-dns"] VOLUME ["/etc/acme-dns", "/var/lib/acme-dns"]
ENTRYPOINT ["./acme-dns"] ENTRYPOINT ["./acme-dns"]
EXPOSE 53 80 443 EXPOSE 53 80 443
EXPOSE 53/udp
Generated
+62 -54
View File
@@ -2,12 +2,12 @@
[[projects]] [[projects]]
digest = "1:289dd4d7abfb3ad2b5f728fbe9b1d5c1bf7d265a3eb9ef92869af1f7baba4c7a" digest = "1:e4b30804a381d7603b8a344009987c1ba351c26043501b23b8c7ce21f0b67474"
name = "github.com/BurntSushi/toml" name = "github.com/BurntSushi/toml"
packages = ["."] packages = ["."]
pruneopts = "" pruneopts = ""
revision = "b26d9c308763d68093482582cea63d69be07a0f0" revision = "3012a1dbe2e4bd1391d42b32f0577cb7bbc7f005"
version = "v0.3.0" version = "v0.3.1"
[[projects]] [[projects]]
digest = "1:9ceecb4271682fd824475d451b6abf02b99ad04e72f8de3408a9d8b7fd15b933" digest = "1:9ceecb4271682fd824475d451b6abf02b99ad04e72f8de3408a9d8b7fd15b933"
@@ -18,12 +18,12 @@
version = "v1.5" version = "v1.5"
[[projects]] [[projects]]
digest = "1:56c130d885a4aacae1dd9c7b71cfe39912c7ebc1ff7d2b46083c8812996dc43b" digest = "1:0deddd908b6b4b768cfc272c16ee61e7088a60f7fe2f06c547bd3d8e1f8b8e77"
name = "github.com/davecgh/go-spew" name = "github.com/davecgh/go-spew"
packages = ["spew"] packages = ["spew"]
pruneopts = "" pruneopts = ""
revision = "346938d642f2ec3594ed81d874461961cd0faa76" revision = "8991bc29aa16c548c550c7ff78260e27b9ab7c73"
version = "v1.1.0" version = "v1.1.1"
[[projects]] [[projects]]
branch = "master" branch = "master"
@@ -34,12 +34,12 @@
revision = "8d10e4a1bae52cd8b81ffdec3445890d6dccab3d" revision = "8d10e4a1bae52cd8b81ffdec3445890d6dccab3d"
[[projects]] [[projects]]
digest = "1:55848e643a99a9dfceb19e090ce67111328fbb1780f34c62a0430994ff85fb90" digest = "1:2defd14040b1ca18840524e9d0f9cfab0ea8d2f2d396b343d221f8bc8e21ab9d"
name = "github.com/fatih/structs" name = "github.com/fatih/structs"
packages = ["."] packages = ["."]
pruneopts = "" pruneopts = ""
revision = "a720dfa8df582c51dee1b36feabb906bde1588bd" revision = "4966fc68f5b7593aafa6cbbba2d65ec6e1416047"
version = "v1.0" version = "v1.1.0"
[[projects]] [[projects]]
branch = "master" branch = "master"
@@ -58,20 +58,20 @@
revision = "6f8212e8d10df7383609d3c377ca08884d8f3ec0" revision = "6f8212e8d10df7383609d3c377ca08884d8f3ec0"
[[projects]] [[projects]]
branch = "master" digest = "1:cea4aa2038169ee558bf507d5ea02c94ca85bcca28a4c7bb99fd59b31e43a686"
digest = "1:9abc49f39e3e23e262594bb4fb70abf74c0c99e94f99153f43b143805e850719"
name = "github.com/google/go-querystring" name = "github.com/google/go-querystring"
packages = ["query"] packages = ["query"]
pruneopts = "" pruneopts = ""
revision = "53e6ce116135b80d037921a7fdd5138cf32d7a8a" revision = "44c6ddd0a2342c386950e880b658017258da92fc"
version = "v1.0.0"
[[projects]] [[projects]]
digest = "1:c1d7e883c50a26ea34019320d8ae40fad86c9e5d56e63a1ba2cb618cef43e986" digest = "1:a25a2c5ae694b01713fb6cd03c3b1ac1ccc1902b9f0a922680a88ec254f968e1"
name = "github.com/google/uuid" name = "github.com/google/uuid"
packages = ["."] packages = ["."]
pruneopts = "" pruneopts = ""
revision = "064e2069ce9c359c118179501254f67d7d37ba24" revision = "9b3b1e0f5f99ae461456d768e7d301a7acdaa2d8"
version = "0.2" version = "v1.1.0"
[[projects]] [[projects]]
digest = "1:788735d9cba7f51b7cc86e6c7ba7b40b0b7c7b374bf3a0b5fa7c929fa2af2da8" digest = "1:788735d9cba7f51b7cc86e6c7ba7b40b0b7c7b374bf3a0b5fa7c929fa2af2da8"
@@ -82,15 +82,15 @@
version = "v1.1.0" version = "v1.1.0"
[[projects]] [[projects]]
digest = "1:3c818dada3e41bdb0f509f78e6775610f1bb179449ec8c4c86a45fae35460f3f" digest = "1:deeb6536ebbcb66ab11213ecfaac9daadfcd004dd057446a4980ffd6c15c36e2"
name = "github.com/julienschmidt/httprouter" name = "github.com/julienschmidt/httprouter"
packages = ["."] packages = ["."]
pruneopts = "" pruneopts = ""
revision = "8c199fb6259ffc1af525cc3ad52ee60ba8359669" revision = "348b672cd90d8190f8240323e372ecd1e66b59dc"
version = "v1.1" version = "v1.2.0"
[[projects]] [[projects]]
digest = "1:4d0614a5d2e5e394368521b087428b2996ae95ebc4699afabc61adac9b7cec38" digest = "1:064eb4d2277fb6a23944380644268b99acc0ae0576c72d0b6d7288d3c2fe4a1e"
name = "github.com/klauspost/compress" name = "github.com/klauspost/compress"
packages = [ packages = [
"flate", "flate",
@@ -98,51 +98,59 @@
"zlib", "zlib",
] ]
pruneopts = "" pruneopts = ""
revision = "b939724e787a27c0005cabe3f78e7ed7987ac74f" revision = "30be6041bed523c18e269a700ebd9c2ea9328574"
version = "v1.4.0" version = "v1.4.1"
[[projects]] [[projects]]
digest = "1:f0117357f14b0a625ddbbe25e23637291ac0276402fcfd25fc447422456364ce" digest = "1:198af4ad42013a3779477b597458f860a6a99e57e1674fa4f61eb04b8b5b768f"
name = "github.com/klauspost/cpuid" name = "github.com/klauspost/cpuid"
packages = ["."] packages = ["."]
pruneopts = "" pruneopts = ""
revision = "ae7887de9fa5d2db4eaa8174a7eff2c1ac00f2da" revision = "e7e905edc00ea8827e58662220139109efea09db"
version = "v1.1" version = "v1.2.0"
[[projects]]
digest = "1:6a874e3ddfb9db2b42bd8c85b6875407c702fa868eed20634ff489bc896ccfd3"
name = "github.com/konsorten/go-windows-terminal-sequences"
packages = ["."]
pruneopts = ""
revision = "5c8c8bd35d3832f5d134ae1e1e375b69a4d25242"
version = "v1.0.1"
[[projects]] [[projects]]
branch = "master" branch = "master"
digest = "1:09792d732b079867772cdbabdf7dc54ef9f9d04c998a9ce6226657151fccbb94" digest = "1:bc36cd980d0069137800b505af4937c6109f4dc7cefe39d7c2efc7c8d51528d6"
name = "github.com/lib/pq" name = "github.com/lib/pq"
packages = [ packages = [
".", ".",
"oid", "oid",
] ]
pruneopts = "" pruneopts = ""
revision = "90697d60dd844d5ef6ff15135d0203f65d2f53b8" revision = "9eb73efc1fcc404148b56765b0d3f61d9a5ef8ee"
[[projects]] [[projects]]
digest = "1:bc03901fc8f0965ccba8bc453eae21a9b04f95999eab664c7de6dc7290f4e8f4" digest = "1:8bbdb2b3dce59271877770d6fe7dcbb8362438fa7d2e1e1f688e4bf2aac72706"
name = "github.com/mattn/go-sqlite3" name = "github.com/mattn/go-sqlite3"
packages = ["."] packages = ["."]
pruneopts = "" pruneopts = ""
revision = "25ecb14adfc7543176f7d85291ec7dba82c6f7e4" revision = "c7c4067b79cc51e6dfdcef5c702e74b1e0fa7c75"
version = "v1.9.0" version = "v1.10.0"
[[projects]] [[projects]]
digest = "1:4c8d8358c45ba11ab7bb15df749d4df8664ff1582daead28bae58cf8cbe49890" digest = "1:103c1001b79009329824bc60f93f8f7150bf1ad31f18d29a476deb8a2de0f002"
name = "github.com/miekg/dns" name = "github.com/miekg/dns"
packages = ["."] packages = ["."]
pruneopts = "" pruneopts = ""
revision = "5a2b9fab83ff0f8bfc99684bd5f43a37abe560f1" revision = "8fc2e5773bbd308ca2fcc962fd8d25c1bd0f6743"
version = "v1.0.8" version = "v1.1.4"
[[projects]] [[projects]]
branch = "master"
digest = "1:fe67641b990bdc1802f8a1e462a4924210a8762a8a17b72e09656049c906b871" digest = "1:fe67641b990bdc1802f8a1e462a4924210a8762a8a17b72e09656049c906b871"
name = "github.com/moul/http2curl" name = "github.com/moul/http2curl"
packages = ["."] packages = ["."]
pruneopts = "" pruneopts = ""
revision = "9ac6cf4d929b2fa8fd2d2e6dec5bb0feb4f4911d" revision = "9ac6cf4d929b2fa8fd2d2e6dec5bb0feb4f4911d"
version = "v1.0.0"
[[projects]] [[projects]]
digest = "1:256484dbbcd271f9ecebc6795b2df8cad4c458dd0f5fd82a8c2fa0c29f233411" digest = "1:256484dbbcd271f9ecebc6795b2df8cad4c458dd0f5fd82a8c2fa0c29f233411"
@@ -153,12 +161,12 @@
version = "v1.0.0" version = "v1.0.0"
[[projects]] [[projects]]
digest = "1:78c9cf43ddeacd0e472f412082227a0fac2ae107ee60e9112156f9371f9912cf" digest = "1:5f47c69f85311c4dc292be6cc995a0a3fe8337a6ce38ef4f71e5b7efd5ad42e0"
name = "github.com/rs/cors" name = "github.com/rs/cors"
packages = ["."] packages = ["."]
pruneopts = "" pruneopts = ""
revision = "3fb1b69b103a84de38a19c3c6ec073dd6caa4d3f" revision = "9a47f48565a795472d43519dd49aac781f3034fb"
version = "v1.5.0" version = "v1.6.0"
[[projects]] [[projects]]
digest = "1:3962f553b77bf6c03fc07cd687a22dd3b00fe11aa14d31194f5505f5bb65cdc8" digest = "1:3962f553b77bf6c03fc07cd687a22dd3b00fe11aa14d31194f5505f5bb65cdc8"
@@ -169,38 +177,38 @@
version = "v1.0.0" version = "v1.0.0"
[[projects]] [[projects]]
digest = "1:3fcbf733a8d810a21265a7f2fe08a3353db2407da052b233f8b204b5afc03d9b" digest = "1:9a3c631555e0351fdc4e696577bb63afd90c399d782a8462dba9d100d7021db3"
name = "github.com/sirupsen/logrus" name = "github.com/sirupsen/logrus"
packages = [ packages = [
".", ".",
"hooks/test", "hooks/test",
] ]
pruneopts = "" pruneopts = ""
revision = "3e01752db0189b9157070a0e1668a620f9a85da2" revision = "e1e72e9de974bd926e5c56f83753fba2df402ce5"
version = "v1.0.6" version = "v1.3.0"
[[projects]] [[projects]]
digest = "1:c587772fb8ad29ad4db67575dad25ba17a51f072ff18a22b4f0257a4d9c24f75" digest = "1:381bcbeb112a51493d9d998bbba207a529c73dbb49b3fd789e48c63fac1f192c"
name = "github.com/stretchr/testify" name = "github.com/stretchr/testify"
packages = [ packages = [
"assert", "assert",
"require", "require",
] ]
pruneopts = "" pruneopts = ""
revision = "f35b8ab0b5a2cef36673838d662e249dd9c94686" revision = "ffdc059bfe9ce6a4e144ba849dbedead332c6053"
version = "v1.2.2" version = "v1.3.0"
[[projects]] [[projects]]
branch = "master"
digest = "1:857a9ecd5cb13379ecc8f798f6e6b6b574c98b9355657d91e068275f1120aaf7" digest = "1:857a9ecd5cb13379ecc8f798f6e6b6b574c98b9355657d91e068275f1120aaf7"
name = "github.com/valyala/bytebufferpool" name = "github.com/valyala/bytebufferpool"
packages = ["."] packages = ["."]
pruneopts = "" pruneopts = ""
revision = "e746df99fe4a3986f4d4f79e13c1e0117ce9c2f7" revision = "e746df99fe4a3986f4d4f79e13c1e0117ce9c2f7"
version = "v1.0.0"
[[projects]] [[projects]]
branch = "master" branch = "master"
digest = "1:ed95b0b73de30dc5507de18b0cd6a2b79dc0ef685eefeb27fd1386f7a4e04f2b" digest = "1:5838606075eb2e11d5b99d4526e36863a9315f006d322abfbf5a7fff73250271"
name = "github.com/valyala/fasthttp" name = "github.com/valyala/fasthttp"
packages = [ packages = [
".", ".",
@@ -208,7 +216,7 @@
"stackless", "stackless",
] ]
pruneopts = "" pruneopts = ""
revision = "e5f51c11919d4f66400334047b897ef0a94c6f3c" revision = "aaec9b0fe2d01bd3ff761345ba47bb94a9b0dc59"
[[projects]] [[projects]]
branch = "master" branch = "master"
@@ -227,12 +235,12 @@
revision = "bd5ef7bd5415a7ac448318e64f11a24cd21e594b" revision = "bd5ef7bd5415a7ac448318e64f11a24cd21e594b"
[[projects]] [[projects]]
branch = "master" digest = "1:c2c74271189fe48e23c25216570008b7ecc5782688d6efd81325886ac6f62ad0"
digest = "1:0dd2250939dcf18c0b1a7b0e364e13b083331f723a6853ff2950a0c4273ff6e2"
name = "github.com/xeipuuv/gojsonschema" name = "github.com/xeipuuv/gojsonschema"
packages = ["."] packages = ["."]
pruneopts = "" pruneopts = ""
revision = "6cd6dcbc9e7514bea255a9241665a6c2d0b37fb4" revision = "f971f3cd73b2899de6923801c147f075263e0c50"
version = "v1.1.0"
[[projects]] [[projects]]
branch = "master" branch = "master"
@@ -263,7 +271,7 @@
[[projects]] [[projects]]
branch = "master" branch = "master"
digest = "1:4cae11053a5fc8e7b08228fcc14d161d3e60b64ba508a8b216937da472690991" digest = "1:cbc1ebc01ec2ceb2c3cc7b9e33357dbc3eff173335f02d9f01603f14102602a7"
name = "golang.org/x/crypto" name = "golang.org/x/crypto"
packages = [ packages = [
"acme", "acme",
@@ -275,11 +283,11 @@
"ssh/terminal", "ssh/terminal",
] ]
pruneopts = "" pruneopts = ""
revision = "de0752318171da717af4ce24d0a2e8626afaeb11" revision = "b8fe1690c61389d7d2a8074a507d1d40c5d30448"
[[projects]] [[projects]]
branch = "master" branch = "master"
digest = "1:67c2d940f2d5c017ef88e9847709dca9b38d5fe82f1e33fb42ace515219f22f1" digest = "1:1e9704e5379e68ac473c28aeb3f7e7cd4036ae8a246bf0285b5ebdbb8e0cfacf"
name = "golang.org/x/net" name = "golang.org/x/net"
packages = [ packages = [
"bpf", "bpf",
@@ -291,18 +299,18 @@
"publicsuffix", "publicsuffix",
] ]
pruneopts = "" pruneopts = ""
revision = "f9ce57c11b242f0f1599cf25c89d8cb02c45295a" revision = "d26f9f9a57f3fab6a695bec0d84433c2c50f8bbf"
[[projects]] [[projects]]
branch = "master" branch = "master"
digest = "1:8812fbc18f45708b5580ed61267fefe5eeb29b36773bdfaad48b0843e3810c02" digest = "1:4d6f20f5bc52af99569b1be5ea55d4e22646d5e022b92f2d3d75347628614734"
name = "golang.org/x/sys" name = "golang.org/x/sys"
packages = [ packages = [
"unix", "unix",
"windows", "windows",
] ]
pruneopts = "" pruneopts = ""
revision = "f0d5e33068cb57c22a181f5df0ffda885309eb5a" revision = "7ae0202eb74c2b534255c71b5a15fa4115aabbcc"
[[projects]] [[projects]]
digest = "1:5acd3512b047305d49e8763eef7ba423901e85d5dd2fd1e71778a0ea8de10bd4" digest = "1:5acd3512b047305d49e8763eef7ba423901e85d5dd2fd1e71778a0ea8de10bd4"
+71 -29
View File
@@ -102,6 +102,28 @@ The method allows you to update the TXT answer contents of your unique subdomain
} }
``` ```
### Health check endpoint
The method can be used to check readiness and/or liveness of the server. It will return status code 200 on success or won't be reachable.
```GET /health```
#### Example using a Kubernetes deployment
```
# ...
readinessProbe:
httpGet:
path: /health
port: 80
periodSeconds: 2
initialDelaySeconds: 2
failureThreshold: 3
successThreshold: 1
livenessProbe:
# same as for readinessProbe...
```
## Self-hosted ## Self-hosted
You are encouraged to run your own acme-dns instance, because you are effectively authorizing the acme-dns server to act on your behalf in providing the answer to the challenging CA, making the instance able to request (and get issued) a TLS certificate for the domain that has CNAME pointing to it. You are encouraged to run your own acme-dns instance, because you are effectively authorizing the acme-dns server to act on your behalf in providing the answer to the challenging CA, making the instance able to request (and get issued) a TLS certificate for the domain that has CNAME pointing to it.
@@ -111,11 +133,11 @@ See the INSTALL section for information on how to do this.
## Installation ## Installation
1) Install [Go 1.9 or newer](https://golang.org/doc/install). 1) Install [Go 1.11 or newer](https://golang.org/doc/install).
2) Install acme-dns: `go get github.com/joohoi/acme-dns/...`. This will install acme-dns to `~/go/bin/acme-dns`. 2) Install acme-dns: `go get github.com/joohoi/acme-dns/...`. This will install acme-dns to `~/go/bin/acme-dns`.
3) Edit config.cfg to suit your needs (see [configuration](#configuration)). `acme-dns` will read the configuration file from `/etc/acme-dns/config.cfg` or `./config.cfg`. 3) Edit config.cfg to suit your needs (see [configuration](#configuration)). `acme-dns` will read the configuration file from `/etc/acme-dns/config.cfg` or `./config.cfg`, or a location specified with the `-c` flag.
4) If your system has systemd, you can optionally install acme-dns as a service so that it will start on boot and be tracked by systemd. This also allows us to add the `CAP_NET_BIND_SERVICE` capability so that acme-dns can be run by a user other than root. 4) If your system has systemd, you can optionally install acme-dns as a service so that it will start on boot and be tracked by systemd. This also allows us to add the `CAP_NET_BIND_SERVICE` capability so that acme-dns can be run by a user other than root.
@@ -149,6 +171,7 @@ See the INSTALL section for information on how to do this.
``` ```
docker run --rm --name acmedns \ docker run --rm --name acmedns \
-p 53:53 \ -p 53:53 \
-p 53:53/udp \
-p 80:80 \ -p 80:80 \
-v /path/to/your/config:/etc/acme-dns:ro \ -v /path/to/your/config:/etc/acme-dns:ro \
-v /path/to/your/data:/var/lib/acme-dns \ -v /path/to/your/data:/var/lib/acme-dns \
@@ -168,15 +191,15 @@ docker run --rm --name acmedns \
## DNS Records ## DNS Records
Note: In this documentation: Note: In this documentation:
- `example.com` is your domain name - `auth.example.org` is the hostname of the acme-dns server
- `auth.example.com` is the subdomain you want to use for acme-dns - acme-dns will serve `*.auth.example.org` records
- `198.51.100.1` is the **public** IP address of the system running acme-dns - `198.51.100.1` is the **public** IP address of the system running acme-dns
These values should be changed based on your environment. These values should be changed based on your environment.
You will need to add some DNS records on your domain's regular DNS server: You will need to add some DNS records on your domain's regular DNS server:
- `NS` record for `auth.example.com` pointing to `ns.auth.example.com` - `NS` record for `auth.example.org` pointing to `auth.example.org` (this means, that `auth.example.org` is responsible for any `*.auth.example.org` records)
- `A` record for `ns.auth.example.com` pointing to `198.51.100.1` - `A` record for `auth.example.org` pointing to `198.51.100.1`
- If using IPv6, an `AAAA` record pointing to the IPv6 address. - If using IPv6, an `AAAA` record pointing to the IPv6 address.
- Each domain you will be authenticating will need a `_acme-challenge` `CNAME` subdomain added. The [client](README.md#clients) you use will explain how to do this. - Each domain you will be authenticating will need a `_acme-challenge` `CNAME` subdomain added. The [client](README.md#clients) you use will explain how to do this.
@@ -184,12 +207,12 @@ You will need to add some DNS records on your domain's regular DNS server:
You may want to test that acme-dns is working before using it for real queries. You may want to test that acme-dns is working before using it for real queries.
1) Confirm that DNS lookups for the acme-dns subdomain works as expected: `dig auth.example.com`. 1) Confirm that DNS lookups for the acme-dns subdomain works as expected: `dig auth.example.org`.
2) Call the `/register` API endpoint to register a test domain: 2) Call the `/register` API endpoint to register a test domain:
``` ```
$ curl -X POST http://auth.example.com/register $ curl -X POST http://auth.example.org/register
{"username":"eabcdb41-d89f-4580-826f-3e62e9755ef2","password":"pbAXVjlIOE01xbut7YnAbkhMQIkcwoHO0ek2j4Q0","fulldomain":"d420c923-bbd7-4056-ab64-c3ca54c9b3cf.auth.example.com","subdomain":"d420c923-bbd7-4056-ab64-c3ca54c9b3cf","allowfrom":[]} {"username":"eabcdb41-d89f-4580-826f-3e62e9755ef2","password":"pbAXVjlIOE01xbut7YnAbkhMQIkcwoHO0ek2j4Q0","fulldomain":"d420c923-bbd7-4056-ab64-c3ca54c9b3cf.auth.example.org","subdomain":"d420c923-bbd7-4056-ab64-c3ca54c9b3cf","allowfrom":[]}
``` ```
3) Call the `/update` API endpoint to set a test TXT record. Pass the `username`, `password` and `subdomain` received from the `register` call performed above: 3) Call the `/update` API endpoint to set a test TXT record. Pass the `username`, `password` and `subdomain` received from the `register` call performed above:
@@ -198,40 +221,38 @@ $ curl -X POST \
-H "X-Api-User: eabcdb41-d89f-4580-826f-3e62e9755ef2" \ -H "X-Api-User: eabcdb41-d89f-4580-826f-3e62e9755ef2" \
-H "X-Api-Key: pbAXVjlIOE01xbut7YnAbkhMQIkcwoHO0ek2j4Q0" \ -H "X-Api-Key: pbAXVjlIOE01xbut7YnAbkhMQIkcwoHO0ek2j4Q0" \
-d '{"subdomain": "d420c923-bbd7-4056-ab64-c3ca54c9b3cf", "txt": "___validation_token_received_from_the_ca___"}' \ -d '{"subdomain": "d420c923-bbd7-4056-ab64-c3ca54c9b3cf", "txt": "___validation_token_received_from_the_ca___"}' \
http://auth.example.com/update http://auth.example.org/update
``` ```
Note: The `txt` field must be exactly 43 characters long, otherwise acme-dns will reject it Note: The `txt` field must be exactly 43 characters long, otherwise acme-dns will reject it
4) Perform a DNS lookup to the test subdomain to confirm that everything is working properly: 4) Perform a DNS lookup to the test subdomain to confirm the updated TXT record is being served:
``` ```
$ dig @ns.auth.example.com d420c923-bbd7-4056-ab64-c3ca54c9b3cf.auth.example.com $ dig -t txt @auth.example.org d420c923-bbd7-4056-ab64-c3ca54c9b3cf.auth.example.org
``` ```
## Configuration ## Configuration
```bash ```bash
[general] [general]
# dns interface # DNS interface. Note that systemd-resolved may reserve port 53 on 127.0.0.53
# In this case acme-dns will error out and you will need to define the listening interface
# for example: listen = "127.0.0.1:53"
listen = ":53" listen = ":53"
# protocol, "udp", "udp4", "udp6" or "tcp", "tcp4", "tcp6" # protocol, "both", "both4", "both6", "udp", "udp4", "udp6" or "tcp", "tcp4", "tcp6"
protocol = "udp" protocol = "both"
# domain name to serve the requests off of # domain name to serve the requests off of
domain = "auth.example.org" domain = "auth.example.org"
# zone name server # zone name server
nsname = "ns1.auth.example.org" nsname = "auth.example.org"
# admin email address, where @ is substituted with . # admin email address, where @ is substituted with .
nsadmin = "admin.example.org" nsadmin = "admin.example.org"
# predefined records served in addition to the TXT # predefined records served in addition to the TXT
records = [ records = [
# default A # domain pointing to the public IP of your acme-dns server
"auth.example.org. A 192.168.1.100", "auth.example.org. A 198.51.100.1",
# A # specify that auth.example.org will resolve any *.auth.example.org records
"ns1.auth.example.org. A 192.168.1.100", "auth.example.org. NS auth.example.org.",
"ns2.auth.example.org. A 192.168.1.100",
# NS
"auth.example.org. NS ns1.auth.example.org.",
"auth.example.org. NS ns2.auth.example.org.",
] ]
# debug messages from CORS etc # debug messages from CORS etc
debug = false debug = false
@@ -240,7 +261,7 @@ debug = false
# Database engine to use, sqlite3 or postgres # Database engine to use, sqlite3 or postgres
engine = "sqlite3" engine = "sqlite3"
# Connection string, filename for sqlite3 and postgres://$username:$password@$host/$db_name for postgres # Connection string, filename for sqlite3 and postgres://$username:$password@$host/$db_name for postgres
connection = "acme-dns.db" connection = "/var/lib/acme-dns/acme-dns.db"
# connection = "postgres://user:password@localhost/acmedns_db" # connection = "postgres://user:password@localhost/acmedns_db"
[api] [api]
@@ -265,6 +286,10 @@ acme_cache_dir = "api-certs"
corsorigins = [ corsorigins = [
"*" "*"
] ]
# use HTTP header to get the client ip
use_header = false
# header name to pull the ip address / list of ip addresses from
header_name = "X-Forwarded-For"
[logconfig] [logconfig]
# logging level: "error", "warning", "info" or "debug" # logging level: "error", "warning", "info" or "debug"
@@ -275,18 +300,17 @@ logtype = "stdout"
# logfile = "./acme-dns.log" # logfile = "./acme-dns.log"
# format, either "json" or "text" # format, either "json" or "text"
logformat = "text" logformat = "text"
# use HTTP header to get the client ip
use_header = false
# header name to pull the ip address / list of ip addresses from
header_name = "X-Forwarded-For"
``` ```
## Clients ## Clients
- acme.sh: [https://github.com/Neilpang/acme.sh](https://github.com/Neilpang/acme.sh) - acme.sh: [https://github.com/Neilpang/acme.sh](https://github.com/Neilpang/acme.sh)
- Certify The Web: [https://github.com/webprofusion/certify](https://github.com/webprofusion/certify)
- cert-manager: [https://github.com/jetstack/cert-manager](https://github.com/jetstack/cert-manager)
- Lego: [https://github.com/xenolf/lego](https://github.com/xenolf/lego) - Lego: [https://github.com/xenolf/lego](https://github.com/xenolf/lego)
- Posh-ACME: [https://github.com/rmbolger/Posh-ACME](https://github.com/rmbolger/Posh-ACME) - Posh-ACME: [https://github.com/rmbolger/Posh-ACME](https://github.com/rmbolger/Posh-ACME)
- Sewer: [https://github.com/komuw/sewer](https://github.com/komuw/sewer) - Sewer: [https://github.com/komuw/sewer](https://github.com/komuw/sewer)
- Traefik: [https://github.com/containous/traefik](https://github.com/containous/traefik)
### Authentication hooks ### Authentication hooks
@@ -300,6 +324,24 @@ header_name = "X-Forwarded-For"
## Changelog ## Changelog
- v0.7.1
- Changed
- Fixed: SOA record correctly added to the TCP DNS server when using both, UDP and TCP servers.
- v0.7
- New
- Added an endpoint to perform health checks
- Changed
- A new protocol selection for DNS server "both", that binds both - UDP and TCP ports.
- Refactored DNS server internals.
- Handle some aspects of DNS spec better.
- v0.6
- New
- Command line flag `-c` to specify location of config file.
- Proper refusal of dynamic update requests.
- Release signing
- Changed
- Better error messages for goroutines
- v0.5 - v0.5
- New - New
- Configurable certificate cache directory - Configurable certificate cache directory
+5
View File
@@ -93,3 +93,8 @@ func webUpdatePost(w http.ResponseWriter, r *http.Request, _ httprouter.Params)
w.WriteHeader(updStatus) w.WriteHeader(updStatus)
w.Write(upd) w.Write(upd)
} }
// Endpoint used to check the readiness and/or liveness (health) of the server.
func healthCheck(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
w.WriteHeader(http.StatusOK)
}
+9
View File
@@ -71,6 +71,7 @@ func setupRouter(debug bool, noauth bool) http.Handler {
Debug: Config.General.Debug, Debug: Config.General.Debug,
}) })
api.POST("/register", webRegisterPost) api.POST("/register", webRegisterPost)
api.GET("/health", healthCheck)
if noauth { if noauth {
api.POST("/update", noAuth(webUpdatePost)) api.POST("/update", noAuth(webUpdatePost))
} else { } else {
@@ -406,3 +407,11 @@ func TestApiManyUpdateWithIpCheckHeaders(t *testing.T) {
} }
Config.API.UseHeader = false Config.API.UseHeader = false
} }
func TestApiHealthCheck(t *testing.T) {
router := setupRouter(false, false)
server := httptest.NewServer(router)
defer server.Close()
e := getExpect(t, server)
e.GET("/health").Expect().Status(http.StatusOK)
}
+1
View File
@@ -6,6 +6,7 @@ import (
) )
func TestUpdateAllowedFromIP(t *testing.T) { func TestUpdateAllowedFromIP(t *testing.T) {
Config.API.UseHeader = false
userWithAllow := newACMETxt() userWithAllow := newACMETxt()
userWithAllow.AllowFrom = cidrslice{"192.168.1.2/32", "[::1]/128"} userWithAllow.AllowFrom = cidrslice{"192.168.1.2/32", "[::1]/128"}
userWithoutAllow := newACMETxt() userWithoutAllow := newACMETxt()
+11 -13
View File
@@ -1,24 +1,22 @@
[general] [general]
# dns interface # DNS interface. Note that systemd-resolved may reserve port 53 on 127.0.0.53
listen = ":53" # In this case acme-dns will error out and you will need to define the listening interface
# protocol, "udp", "udp4", "udp6" or "tcp", "tcp4", "tcp6" # for example: listen = "127.0.0.1:53"
protocol = "udp" listen = "127.0.0.1:53"
# protocol, "both", "both4", "both6", "udp", "udp4", "udp6" or "tcp", "tcp4", "tcp6"
protocol = "both"
# domain name to serve the requests off of # domain name to serve the requests off of
domain = "auth.example.org" domain = "auth.example.org"
# zone name server # zone name server
nsname = "ns1.auth.example.org" nsname = "auth.example.org"
# admin email address, where @ is substituted with . # admin email address, where @ is substituted with .
nsadmin = "admin.example.org" nsadmin = "admin.example.org"
# predefined records served in addition to the TXT # predefined records served in addition to the TXT
records = [ records = [
# default A # domain pointing to the public IP of your acme-dns server
"auth.example.org. A 192.168.1.100", "auth.example.org. A 198.51.100.1",
# A # specify that auth.example.org will resolve any *.auth.example.org records
"ns1.auth.example.org. A 192.168.1.100", "auth.example.org. NS auth.example.org.",
"ns2.auth.example.org. A 192.168.1.100",
# NS
"auth.example.org. NS ns1.auth.example.org.",
"auth.example.org. NS ns2.auth.example.org.",
] ]
# debug messages from CORS etc # debug messages from CORS etc
debug = false debug = false
+164 -77
View File
@@ -8,25 +8,182 @@ import (
"time" "time"
) )
func readQuery(m *dns.Msg) { // Records is a slice of ResourceRecords
type Records struct {
Records []dns.RR
}
// DNSServer is the main struct for acme-dns DNS server
type DNSServer struct {
DB database
Server *dns.Server
SOA dns.RR
Domains map[string]Records
}
// NewDNSServer parses the DNS records from config and returns a new DNSServer struct
func NewDNSServer(db database, addr string, proto string) *DNSServer {
var server DNSServer
server.Server = &dns.Server{Addr: addr, Net: proto}
server.DB = db
server.Domains = make(map[string]Records)
return &server
}
// Start starts the DNSServer
func (d *DNSServer) Start(errorChannel chan error) {
// DNS server part
dns.HandleFunc(".", d.handleRequest)
log.WithFields(log.Fields{"addr": d.Server.Addr, "proto": d.Server.Net}).Info("Listening DNS")
err := d.Server.ListenAndServe()
if err != nil {
errorChannel <- err
}
}
// ParseRecords parses a slice of DNS record string
func (d *DNSServer) ParseRecords(config DNSConfig) {
for _, v := range config.General.StaticRecords {
rr, err := dns.NewRR(strings.ToLower(v))
if err != nil {
log.WithFields(log.Fields{"error": err.Error(), "rr": v}).Warning("Could not parse RR from config")
continue
}
// Add parsed RR
d.appendRR(rr)
}
// Create serial
serial := time.Now().Format("2006010215")
// Add SOA
SOAstring := fmt.Sprintf("%s. SOA %s. %s. %s 28800 7200 604800 86400", strings.ToLower(config.General.Domain), strings.ToLower(config.General.Nsname), strings.ToLower(config.General.Nsadmin), serial)
soarr, err := dns.NewRR(SOAstring)
if err != nil {
log.WithFields(log.Fields{"error": err.Error(), "soa": SOAstring}).Error("Error while adding SOA record")
} else {
d.appendRR(soarr)
d.SOA = soarr
}
}
func (d *DNSServer) appendRR(rr dns.RR) {
addDomain := rr.Header().Name
_, ok := d.Domains[addDomain]
if !ok {
d.Domains[addDomain] = Records{[]dns.RR{rr}}
} else {
drecs := d.Domains[addDomain]
drecs.Records = append(drecs.Records, rr)
d.Domains[addDomain] = drecs
}
log.WithFields(log.Fields{"recordtype": dns.TypeToString[rr.Header().Rrtype], "domain": addDomain}).Debug("Adding new record to domain")
}
func (d *DNSServer) handleRequest(w dns.ResponseWriter, r *dns.Msg) {
m := new(dns.Msg)
m.SetReply(r)
if r.Opcode == dns.OpcodeQuery {
d.readQuery(m)
}
w.WriteMsg(m)
}
func (d *DNSServer) readQuery(m *dns.Msg) {
var authoritative = false
for _, que := range m.Question { for _, que := range m.Question {
if rr, rc, err := answer(que); err == nil { if rr, rc, auth, err := d.answer(que); err == nil {
if auth {
authoritative = auth
}
m.MsgHdr.Rcode = rc m.MsgHdr.Rcode = rc
for _, r := range rr { for _, r := range rr {
m.Answer = append(m.Answer, r) m.Answer = append(m.Answer, r)
} }
} }
} }
m.MsgHdr.Authoritative = authoritative
if authoritative {
if m.MsgHdr.Rcode == dns.RcodeNameError {
m.Answer = append(m.Answer, d.SOA)
}
}
} }
func answerTXT(q dns.Question) ([]dns.RR, int, error) { func (d *DNSServer) getRecord(q dns.Question) ([]dns.RR, error) {
var rr []dns.RR
var cnames []dns.RR
domain, ok := d.Domains[q.Name]
if !ok {
return rr, fmt.Errorf("No records for domain %s", q.Name)
}
for _, ri := range domain.Records {
if ri.Header().Rrtype == q.Qtype {
rr = append(rr, ri)
}
if ri.Header().Rrtype == dns.TypeCNAME {
cnames = append(cnames, ri)
}
}
if len(rr) == 0 {
return cnames, nil
}
return rr, nil
}
// answeringForDomain checks if we have any records for a domain
func (d *DNSServer) answeringForDomain(name string) bool {
_, ok := d.Domains[name]
return ok
}
func (d *DNSServer) isAuthoritative(q dns.Question) bool {
if d.answeringForDomain(q.Name) {
return true
}
domainParts := strings.Split(q.Name, ".")
for i := range domainParts {
if d.answeringForDomain(strings.Join(domainParts[i:], ".")) {
return true
}
}
return false
}
func (d *DNSServer) answer(q dns.Question) ([]dns.RR, int, bool, error) {
var rcode int
var authoritative = d.isAuthoritative(q)
if !d.answeringForDomain(q.Name) {
rcode = dns.RcodeNameError
}
r, _ := d.getRecord(q)
if q.Qtype == dns.TypeTXT {
txtRRs, err := d.answerTXT(q)
if err == nil {
for _, txtRR := range txtRRs {
r = append(r, txtRR)
}
}
}
if len(r) > 0 {
// Make sure that we return NOERROR if there were dynamic records for the domain
rcode = dns.RcodeSuccess
}
// Handle EDNS (no support at the moment)
if q.Qtype == dns.TypeOPT {
return []dns.RR{}, dns.RcodeFormatError, authoritative, nil
}
log.WithFields(log.Fields{"qtype": dns.TypeToString[q.Qtype], "domain": q.Name, "rcode": dns.RcodeToString[rcode]}).Debug("Answering question for domain")
return r, rcode, authoritative, nil
}
func (d *DNSServer) answerTXT(q dns.Question) ([]dns.RR, error) {
var ra []dns.RR var ra []dns.RR
rcode := dns.RcodeNameError
subdomain := sanitizeDomainQuestion(q.Name) subdomain := sanitizeDomainQuestion(q.Name)
atxt, err := DB.GetTXTForDomain(subdomain) atxt, err := d.DB.GetTXTForDomain(subdomain)
if err != nil { if err != nil {
log.WithFields(log.Fields{"error": err.Error()}).Debug("Error while trying to get record") log.WithFields(log.Fields{"error": err.Error()}).Debug("Error while trying to get record")
return ra, dns.RcodeNameError, err return ra, err
} }
for _, v := range atxt { for _, v := range atxt {
if len(v) > 0 { if len(v) > 0 {
@@ -34,77 +191,7 @@ func answerTXT(q dns.Question) ([]dns.RR, int, error) {
r.Hdr = dns.RR_Header{Name: q.Name, Rrtype: dns.TypeTXT, Class: dns.ClassINET, Ttl: 1} r.Hdr = dns.RR_Header{Name: q.Name, Rrtype: dns.TypeTXT, Class: dns.ClassINET, Ttl: 1}
r.Txt = append(r.Txt, v) r.Txt = append(r.Txt, v)
ra = append(ra, r) ra = append(ra, r)
rcode = dns.RcodeSuccess
} }
} }
return ra, nil
log.WithFields(log.Fields{"domain": q.Name}).Info("Answering TXT question for domain")
return ra, rcode, nil
}
func answer(q dns.Question) ([]dns.RR, int, error) {
if q.Qtype == dns.TypeTXT {
return answerTXT(q)
}
var r []dns.RR
var rcode = dns.RcodeSuccess
var domain = strings.ToLower(q.Name)
var rtype = q.Qtype
r, ok := RR.Records[rtype][domain]
if !ok {
r, ok = RR.Records[dns.TypeCNAME][domain]
if !ok {
rcode = dns.RcodeNameError
}
}
log.WithFields(log.Fields{"qtype": dns.TypeToString[rtype], "domain": domain, "rcode": dns.RcodeToString[rcode]}).Debug("Answering question for domain")
return r, rcode, nil
}
func handleRequest(w dns.ResponseWriter, r *dns.Msg) {
m := new(dns.Msg)
m.SetReply(r)
if r.Opcode == dns.OpcodeQuery {
readQuery(m)
}
w.WriteMsg(m)
}
// Parse config records
func (r *Records) Parse(config general) {
rrmap := make(map[uint16]map[string][]dns.RR)
for _, v := range config.StaticRecords {
rr, err := dns.NewRR(strings.ToLower(v))
if err != nil {
log.WithFields(log.Fields{"error": err.Error(), "rr": v}).Warning("Could not parse RR from config")
continue
}
// Add parsed RR to the list
rrmap = appendRR(rrmap, rr)
}
// Create serial
serial := time.Now().Format("2006010215")
// Add SOA
SOAstring := fmt.Sprintf("%s. SOA %s. %s. %s 28800 7200 604800 86400", strings.ToLower(config.Domain), strings.ToLower(config.Nsname), strings.ToLower(config.Nsadmin), serial)
soarr, err := dns.NewRR(SOAstring)
if err != nil {
log.WithFields(log.Fields{"error": err.Error(), "soa": SOAstring}).Error("Error while adding SOA record")
} else {
rrmap = appendRR(rrmap, soarr)
}
r.Records = rrmap
}
func appendRR(rrmap map[uint16]map[string][]dns.RR, rr dns.RR) map[uint16]map[string][]dns.RR {
_, ok := rrmap[rr.Header().Rrtype]
if !ok {
newrr := make(map[string][]dns.RR)
rrmap[rr.Header().Rrtype] = newrr
}
rrmap[rr.Header().Rrtype][rr.Header().Name] = append(rrmap[rr.Header().Rrtype][rr.Header().Name], rr)
log.WithFields(log.Fields{"recordtype": dns.TypeToString[rr.Header().Rrtype], "domain": rr.Header().Name}).Debug("Adding new record type to domain")
return rrmap
} }
+81 -56
View File
@@ -5,10 +5,10 @@ import (
"database/sql/driver" "database/sql/driver"
"errors" "errors"
"fmt" "fmt"
"testing"
"github.com/erikstmartin/go-testdb" "github.com/erikstmartin/go-testdb"
"github.com/miekg/dns" "github.com/miekg/dns"
"strings"
"testing"
) )
var resolv resolver var resolv resolver
@@ -18,20 +18,20 @@ type resolver struct {
server string server string
} }
func (r *resolver) lookup(host string, qtype uint16) ([]dns.RR, error) { func (r *resolver) lookup(host string, qtype uint16) (*dns.Msg, error) {
msg := new(dns.Msg) msg := new(dns.Msg)
msg.Id = dns.Id() msg.Id = dns.Id()
msg.Question = make([]dns.Question, 1) msg.Question = make([]dns.Question, 1)
msg.Question[0] = dns.Question{Name: dns.Fqdn(host), Qtype: qtype, Qclass: dns.ClassINET} msg.Question[0] = dns.Question{Name: dns.Fqdn(host), Qtype: qtype, Qclass: dns.ClassINET}
in, err := dns.Exchange(msg, r.server) in, err := dns.Exchange(msg, r.server)
if err != nil { if err != nil {
return []dns.RR{}, fmt.Errorf("Error querying the server [%v]", err) return in, fmt.Errorf("Error querying the server [%v]", err)
} }
if in != nil && in.Rcode != dns.RcodeSuccess { if in != nil && in.Rcode != dns.RcodeSuccess {
return []dns.RR{}, fmt.Errorf("Received error from the server [%s]", dns.RcodeToString[in.Rcode]) return in, fmt.Errorf("Received error from the server [%s]", dns.RcodeToString[in.Rcode])
} }
return in.Answer, nil return in, nil
} }
func hasExpectedTXTAnswer(answer []dns.RR, cmpTXT string) error { func hasExpectedTXTAnswer(answer []dns.RR, cmpTXT string) error {
@@ -51,25 +51,6 @@ func hasExpectedTXTAnswer(answer []dns.RR, cmpTXT string) error {
return errors.New("Expected answer not found") return errors.New("Expected answer not found")
} }
func findRecordFromMemory(rrstr string, host string, qtype uint16) error {
var errmsg = "No record found"
arr, _ := dns.NewRR(strings.ToLower(rrstr))
if arrQt, ok := RR.Records[qtype]; ok {
if arrHst, ok := arrQt[host]; ok {
for _, v := range arrHst {
if arr.String() == v.String() {
return nil
}
}
} else {
errmsg = "No records for domain"
}
} else {
errmsg = "No records for this type in DB"
}
return errors.New(errmsg)
}
func TestQuestionDBError(t *testing.T) { func TestQuestionDBError(t *testing.T) {
testdb.SetQueryWithArgsFunc(func(query string, args []driver.Value) (result driver.Rows, err error) { testdb.SetQueryWithArgsFunc(func(query string, args []driver.Value) (result driver.Rows, err error) {
columns := []string{"Username", "Password", "Subdomain", "Value", "LastActive"} columns := []string{"Username", "Password", "Subdomain", "Value", "LastActive"}
@@ -88,44 +69,36 @@ func TestQuestionDBError(t *testing.T) {
defer DB.SetBackend(oldDb) defer DB.SetBackend(oldDb)
q := dns.Question{Name: dns.Fqdn("whatever.tld"), Qtype: dns.TypeTXT, Qclass: dns.ClassINET} q := dns.Question{Name: dns.Fqdn("whatever.tld"), Qtype: dns.TypeTXT, Qclass: dns.ClassINET}
_, rcode, err := answerTXT(q) _, err = dnsserver.answerTXT(q)
if err == nil { if err == nil {
t.Errorf("Expected error but got none") t.Errorf("Expected error but got none")
} }
if rcode != dns.RcodeNameError {
t.Errorf("Expected [%s] rcode, but got [%s]", dns.RcodeToString[dns.RcodeNameError], dns.RcodeToString[rcode])
}
} }
func TestParse(t *testing.T) { func TestParse(t *testing.T) {
var testcfg = general{ var testcfg = DNSConfig{
Domain: ")", General: general{
Nsname: "ns1.auth.example.org", Domain: ")",
Nsadmin: "admin.example.org", Nsname: "ns1.auth.example.org",
StaticRecords: []string{}, Nsadmin: "admin.example.org",
Debug: false, StaticRecords: []string{},
Debug: false,
},
} }
var testRR Records dnsserver.ParseRecords(testcfg)
testRR.Parse(testcfg)
if !loggerHasEntryWithMessage("Error while adding SOA record") { if !loggerHasEntryWithMessage("Error while adding SOA record") {
t.Errorf("Expected SOA parsing to return error, but did not find one") t.Errorf("Expected SOA parsing to return error, but did not find one")
} }
} }
func TestResolveA(t *testing.T) { func TestResolveA(t *testing.T) {
resolv := resolver{server: "0.0.0.0:15353"} resolv := resolver{server: "127.0.0.1:15353"}
answer, err := resolv.lookup("auth.example.org", dns.TypeA) answer, err := resolv.lookup("auth.example.org", dns.TypeA)
if err != nil { if err != nil {
t.Errorf("%v", err) t.Errorf("%v", err)
} }
if len(answer) > 0 { if len(answer.Answer) == 0 {
err = findRecordFromMemory(answer[0].String(), "auth.example.org.", dns.TypeA)
if err != nil {
t.Errorf("Answer [%s] did not match the expected, got error: [%s], debug: [%q]", answer[0].String(), err, RR.Records)
}
} else {
t.Error("No answer for DNS query") t.Error("No answer for DNS query")
} }
@@ -135,8 +108,58 @@ func TestResolveA(t *testing.T) {
} }
} }
func TestEDNS(t *testing.T) {
resolv := resolver{server: "127.0.0.1:15353"}
answer, _ := resolv.lookup("auth.example.org", dns.TypeOPT)
if answer.Rcode != dns.RcodeFormatError {
t.Errorf("Was expecing FORMERR rcode for OPT query, but got [%s] instead.", dns.RcodeToString[answer.Rcode])
}
}
func TestResolveCNAME(t *testing.T) {
resolv := resolver{server: "127.0.0.1:15353"}
expected := "cn.example.org. 3600 IN CNAME something.example.org."
answer, err := resolv.lookup("cn.example.org", dns.TypeCNAME)
if err != nil {
t.Errorf("Got unexpected error: %s", err)
}
if len(answer.Answer) != 1 {
t.Errorf("Expected exactly 1 RR in answer, but got %d instead.", len(answer.Answer))
}
if answer.Answer[0].Header().Rrtype != dns.TypeCNAME {
t.Errorf("Expected a CNAME answer, but got [%s] instead.", dns.TypeToString[answer.Answer[0].Header().Rrtype])
}
if answer.Answer[0].String() != expected {
t.Errorf("Expected CNAME answer [%s] but got [%s] instead.", expected, answer.Answer[0].String())
}
}
func TestAuthoritative(t *testing.T) {
resolv := resolver{server: "127.0.0.1:15353"}
answer, _ := resolv.lookup("nonexistent.auth.example.org", dns.TypeA)
if answer.Rcode != dns.RcodeNameError {
t.Errorf("Was expecing NXDOMAIN rcode, but got [%s] instead.", dns.RcodeToString[answer.Rcode])
}
if len(answer.Answer) != 1 {
t.Errorf("Was expecting exactly one answer (SOA) for invalid subdomain, but got %d", len(answer.Answer))
}
if answer.Answer[0].Header().Rrtype != dns.TypeSOA {
t.Errorf("Was expecting SOA record as answer for NXDOMAIN but got [%s]", dns.TypeToString[answer.Answer[0].Header().Rrtype])
}
if !answer.MsgHdr.Authoritative {
t.Errorf("Was expecting authoritative bit to be set")
}
nanswer, _ := resolv.lookup("nonexsitent.nonauth.tld", dns.TypeA)
if len(nanswer.Answer) > 0 {
t.Errorf("Didn't expect answers for non authotitative domain query")
}
if nanswer.MsgHdr.Authoritative {
t.Errorf("Authoritative bit should not be set for non-authoritative domain.")
}
}
func TestResolveTXT(t *testing.T) { func TestResolveTXT(t *testing.T) {
resolv := resolver{server: "0.0.0.0:15353"} resolv := resolver{server: "127.0.0.1:15353"}
validTXT := "______________valid_response_______________" validTXT := "______________valid_response_______________"
atxt, err := DB.Register(cidrslice{}) atxt, err := DB.Register(cidrslice{})
@@ -172,18 +195,20 @@ func TestResolveTXT(t *testing.T) {
} }
} }
if len(answer) > 0 { if len(answer.Answer) > 0 {
if !test.getAnswer { if !test.getAnswer && answer.Answer[0].Header().Rrtype != dns.TypeSOA {
t.Errorf("Test %d: Expected no answer, but got: [%q]", i, answer) t.Errorf("Test %d: Expected no answer, but got: [%q]", i, answer)
} }
err = hasExpectedTXTAnswer(answer, test.expTXT) if test.getAnswer {
if err != nil { err = hasExpectedTXTAnswer(answer.Answer, test.expTXT)
if test.validAnswer { if err != nil {
t.Errorf("Test %d: %v", i, err) if test.validAnswer {
} t.Errorf("Test %d: %v", i, err)
} else { }
if !test.validAnswer { } else {
t.Errorf("Test %d: Answer was not expected to be valid, answer [%q], compared to [%s]", i, answer, test.expTXT) if !test.validAnswer {
t.Errorf("Test %d: Answer was not expected to be valid, answer [%q], compared to [%s]", i, answer, test.expTXT)
}
} }
} }
} else { } else {
+1
View File
@@ -8,6 +8,7 @@ services:
ports: ports:
- "443:443" - "443:443"
- "53:53" - "53:53"
- "53:53/udp"
- "80:80" - "80:80"
volumes: volumes:
- ./config:/etc/acme-dns:ro - ./config:/etc/acme-dns:ro
+51 -13
View File
@@ -4,9 +4,11 @@ package main
import ( import (
"crypto/tls" "crypto/tls"
"flag"
stdlog "log" stdlog "log"
"net/http" "net/http"
"os" "os"
"strings"
"syscall" "syscall"
"github.com/julienschmidt/httprouter" "github.com/julienschmidt/httprouter"
@@ -18,11 +20,13 @@ import (
func main() { func main() {
// Created files are not world writable // Created files are not world writable
syscall.Umask(0077) syscall.Umask(0077)
configPtr := flag.String("c", "/etc/acme-dns/config.cfg", "config file location")
flag.Parse()
// Read global config // Read global config
var err error var err error
if fileIsAccessible("/etc/acme-dns/config.cfg") { if fileIsAccessible(*configPtr) {
log.WithFields(log.Fields{"file": "/etc/acme-dns/config.cfg"}).Info("Using config file") log.WithFields(log.Fields{"file": *configPtr}).Info("Using config file")
Config, err = readConfig("/etc/acme-dns/config.cfg") Config, err = readConfig(*configPtr)
} else if fileIsAccessible("./config.cfg") { } else if fileIsAccessible("./config.cfg") {
log.WithFields(log.Fields{"file": "./config.cfg"}).Info("Using config file") log.WithFields(log.Fields{"file": "./config.cfg"}).Info("Using config file")
Config, err = readConfig("./config.cfg") Config, err = readConfig("./config.cfg")
@@ -37,9 +41,6 @@ func main() {
setupLogging(Config.Logconfig.Format, Config.Logconfig.Level) setupLogging(Config.Logconfig.Format, Config.Logconfig.Level)
// Read the default records in
RR.Parse(Config.General)
// Open database // Open database
newDB := new(acmedb) newDB := new(acmedb)
err = newDB.Init(Config.Database.Engine, Config.Database.Connection) err = newDB.Init(Config.Database.Engine, Config.Database.Connection)
@@ -52,16 +53,49 @@ func main() {
DB = newDB DB = newDB
defer DB.Close() defer DB.Close()
// Error channel for servers
errChan := make(chan error, 1)
// DNS server // DNS server
startDNS(Config.General.Listen, Config.General.Proto) if strings.HasPrefix(Config.General.Proto, "both") {
// Handle the case where DNS server should be started for both udp and tcp
udpProto := "udp"
tcpProto := "tcp"
if strings.HasSuffix(Config.General.Proto, "4") {
udpProto += "4"
tcpProto += "4"
} else if strings.HasSuffix(Config.General.Proto, "6") {
udpProto += "6"
tcpProto += "6"
}
dnsServerUDP := NewDNSServer(DB, Config.General.Listen, udpProto)
dnsServerUDP.ParseRecords(Config)
dnsServerTCP := NewDNSServer(DB, Config.General.Listen, tcpProto)
// No need to parse records from config again
dnsServerTCP.Domains = dnsServerUDP.Domains
dnsServerTCP.SOA = dnsServerUDP.SOA
go dnsServerUDP.Start(errChan)
go dnsServerTCP.Start(errChan)
} else {
dnsServer := NewDNSServer(DB, Config.General.Listen, Config.General.Proto)
dnsServer.ParseRecords(Config)
go dnsServer.Start(errChan)
}
// HTTP API // HTTP API
startHTTPAPI() go startHTTPAPI(errChan)
// block waiting for error
select {
case err = <-errChan:
if err != nil {
log.Fatal(err)
}
}
log.Debugf("Shutting down...") log.Debugf("Shutting down...")
} }
func startHTTPAPI() { func startHTTPAPI(errChan chan error) {
// Setup http logger // Setup http logger
logger := log.New() logger := log.New()
logwriter := logger.Writer() logwriter := logger.Writer()
@@ -81,13 +115,14 @@ func startHTTPAPI() {
api.POST("/register", webRegisterPost) api.POST("/register", webRegisterPost)
} }
api.POST("/update", Auth(webUpdatePost)) api.POST("/update", Auth(webUpdatePost))
api.GET("/health", healthCheck)
host := Config.API.IP + ":" + Config.API.Port host := Config.API.IP + ":" + Config.API.Port
cfg := &tls.Config{ cfg := &tls.Config{
MinVersion: tls.VersionTLS12, MinVersion: tls.VersionTLS12,
} }
var err error
switch Config.API.TLS { switch Config.API.TLS {
case "letsencrypt": case "letsencrypt":
m := autocert.Manager{ m := autocert.Manager{
@@ -106,7 +141,7 @@ func startHTTPAPI() {
ErrorLog: stdlog.New(logwriter, "", 0), ErrorLog: stdlog.New(logwriter, "", 0),
} }
log.WithFields(log.Fields{"host": host, "domain": Config.API.Domain}).Info("Listening HTTPS, using certificate from autocert") log.WithFields(log.Fields{"host": host, "domain": Config.API.Domain}).Info("Listening HTTPS, using certificate from autocert")
log.Fatal(srv.ListenAndServeTLS("", "")) err = srv.ListenAndServeTLS("", "")
case "cert": case "cert":
srv := &http.Server{ srv := &http.Server{
Addr: host, Addr: host,
@@ -115,9 +150,12 @@ func startHTTPAPI() {
ErrorLog: stdlog.New(logwriter, "", 0), ErrorLog: stdlog.New(logwriter, "", 0),
} }
log.WithFields(log.Fields{"host": host}).Info("Listening HTTPS") log.WithFields(log.Fields{"host": host}).Info("Listening HTTPS")
log.Fatal(srv.ListenAndServeTLS(Config.API.TLSCertFullchain, Config.API.TLSCertPrivkey)) err = srv.ListenAndServeTLS(Config.API.TLSCertFullchain, Config.API.TLSCertPrivkey)
default: default:
log.WithFields(log.Fields{"host": host}).Info("Listening HTTP") log.WithFields(log.Fields{"host": host}).Info("Listening HTTP")
log.Fatal(http.ListenAndServe(host, c.Handler(api))) err = http.ListenAndServe(host, c.Handler(api))
}
if err != nil {
errChan <- err
} }
} }
+17 -3
View File
@@ -7,10 +7,12 @@ import (
logrustest "github.com/sirupsen/logrus/hooks/test" logrustest "github.com/sirupsen/logrus/hooks/test"
"io/ioutil" "io/ioutil"
"os" "os"
"sync"
"testing" "testing"
) )
var loghook = new(logrustest.Hook) var loghook = new(logrustest.Hook)
var dnsserver *DNSServer
var ( var (
postgres = flag.Bool("postgres", false, "run integration tests against PostgreSQL") postgres = flag.Bool("postgres", false, "run integration tests against PostgreSQL")
@@ -19,6 +21,7 @@ var (
var records = []string{ var records = []string{
"auth.example.org. A 192.168.1.100", "auth.example.org. A 192.168.1.100",
"ns1.auth.example.org. A 192.168.1.101", "ns1.auth.example.org. A 192.168.1.101",
"cn.example.org CNAME something.example.org.",
"!''b', unparseable ", "!''b', unparseable ",
"ns2.auth.example.org. A 192.168.1.102", "ns2.auth.example.org. A 192.168.1.102",
} }
@@ -26,7 +29,6 @@ var records = []string{
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
setupTestLogger() setupTestLogger()
setupConfig() setupConfig()
RR.Parse(Config.General)
flag.Parse() flag.Parse()
newDb := new(acmedb) newDb := new(acmedb)
@@ -42,9 +44,19 @@ func TestMain(m *testing.M) {
_ = newDb.Init("sqlite3", ":memory:") _ = newDb.Init("sqlite3", ":memory:")
} }
DB = newDb DB = newDb
server := startDNS("0.0.0.0:15353", "udp") dnsserver = NewDNSServer(DB, Config.General.Listen, Config.General.Proto)
dnsserver.ParseRecords(Config)
// Make sure that we're not creating a race condition in tests
var wg sync.WaitGroup
wg.Add(1)
dnsserver.Server.NotifyStartedFunc = func() {
wg.Done()
}
go dnsserver.Start(make(chan error, 1))
wg.Wait()
exitval := m.Run() exitval := m.Run()
server.Shutdown() dnsserver.Server.Shutdown()
DB.Close() DB.Close()
os.Exit(exitval) os.Exit(exitval)
} }
@@ -57,6 +69,8 @@ func setupConfig() {
var generalcfg = general{ var generalcfg = general{
Domain: "auth.example.org", Domain: "auth.example.org",
Listen: "127.0.0.1:15353",
Proto: "udp",
Nsname: "ns1.auth.example.org", Nsname: "ns1.auth.example.org",
Nsadmin: "admin.example.org", Nsadmin: "admin.example.org",
StaticRecords: records, StaticRecords: records,
-9
View File
@@ -5,7 +5,6 @@ import (
"sync" "sync"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/miekg/dns"
) )
// Config is global configuration struct // Config is global configuration struct
@@ -14,14 +13,6 @@ var Config DNSConfig
// DB is used to access the database functions in acme-dns // DB is used to access the database functions in acme-dns
var DB database var DB database
// RR holds the static DNS records
var RR Records
// Records is for static records
type Records struct {
Records map[uint16]map[string][]dns.RR
}
// DNSConfig holds the config structure // DNSConfig holds the config structure
type DNSConfig struct { type DNSConfig struct {
General general General general
-9
View File
@@ -10,7 +10,6 @@ import (
"strings" "strings"
"github.com/BurntSushi/toml" "github.com/BurntSushi/toml"
"github.com/miekg/dns"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
@@ -108,14 +107,6 @@ func setupLogging(format string, level string) {
// TODO: file logging // TODO: file logging
} }
func startDNS(listen string, proto string) *dns.Server {
// DNS server part
dns.HandleFunc(".", handleRequest)
server := &dns.Server{Addr: listen, Net: proto}
go server.ListenAndServe()
return server
}
func getIPListFromHeader(header string) []string { func getIPListFromHeader(header string) []string {
iplist := []string{} iplist := []string{}
for _, v := range strings.Split(header, ",") { for _, v := range strings.Split(header, ",") {
+17 -10
View File
@@ -1,14 +1,21 @@
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE The MIT License (MIT)
Version 2, December 2004
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net> Copyright (c) 2013 TOML authors
Everyone is permitted to copy and distribute verbatim or modified Permission is hereby granted, free of charge, to any person obtaining a copy
copies of this license document, and changing it is allowed as long of this software and associated documentation files (the "Software"), to deal
as the name is changed. in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE The above copyright notice and this permission notice shall be included in
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION all copies or substantial portions of the Software.
0. You just DO WHAT THE FUCK YOU WANT TO.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
+17 -10
View File
@@ -1,14 +1,21 @@
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE The MIT License (MIT)
Version 2, December 2004
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net> Copyright (c) 2013 TOML authors
Everyone is permitted to copy and distribute verbatim or modified Permission is hereby granted, free of charge, to any person obtaining a copy
copies of this license document, and changing it is allowed as long of this software and associated documentation files (the "Software"), to deal
as the name is changed. in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE The above copyright notice and this permission notice shall be included in
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION all copies or substantial portions of the Software.
0. You just DO WHAT THE FUCK YOU WANT TO.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
+17 -10
View File
@@ -1,14 +1,21 @@
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE The MIT License (MIT)
Version 2, December 2004
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net> Copyright (c) 2013 TOML authors
Everyone is permitted to copy and distribute verbatim or modified Permission is hereby granted, free of charge, to any person obtaining a copy
copies of this license document, and changing it is allowed as long of this software and associated documentation files (the "Software"), to deal
as the name is changed. in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE The above copyright notice and this permission notice shall be included in
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION all copies or substantial portions of the Software.
0. You just DO WHAT THE FUCK YOU WANT TO.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
+17 -10
View File
@@ -1,14 +1,21 @@
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE The MIT License (MIT)
Version 2, December 2004
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net> Copyright (c) 2013 TOML authors
Everyone is permitted to copy and distribute verbatim or modified Permission is hereby granted, free of charge, to any person obtaining a copy
copies of this license document, and changing it is allowed as long of this software and associated documentation files (the "Software"), to deal
as the name is changed. in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE The above copyright notice and this permission notice shall be included in
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION all copies or substantial portions of the Software.
0. You just DO WHAT THE FUCK YOU WANT TO.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
+14
View File
@@ -16,6 +16,8 @@ age = 250
andrew = "gallant" andrew = "gallant"
kait = "brady" kait = "brady"
now = 1987-07-05T05:45:00Z now = 1987-07-05T05:45:00Z
nowEast = 2017-06-22T16:15:21+08:00
nowWest = 2017-06-22T02:14:36-06:00
yesOrNo = true yesOrNo = true
pi = 3.14 pi = 3.14
colors = [ colors = [
@@ -38,6 +40,8 @@ cauchy = "cat 2"
Pi float64 Pi float64
YesOrNo bool YesOrNo bool
Now time.Time Now time.Time
NowEast time.Time
NowWest time.Time
Andrew string Andrew string
Kait string Kait string
My map[string]cats My map[string]cats
@@ -53,11 +57,21 @@ cauchy = "cat 2"
if err != nil { if err != nil {
panic(err) panic(err)
} }
nowEast, err := time.Parse("2006-01-02T15:04:05-07:00", "2017-06-22T16:15:21+08:00")
if err != nil {
panic(err)
}
nowWest, err := time.Parse("2006-01-02T15:04:05-07:00", "2017-06-22T02:14:36-06:00")
if err != nil {
panic(err)
}
var answer = simple{ var answer = simple{
Age: 250, Age: 250,
Andrew: "gallant", Andrew: "gallant",
Kait: "brady", Kait: "brady",
Now: now, Now: now,
NowEast: nowEast,
NowWest: nowWest,
YesOrNo: true, YesOrNo: true,
Pi: 3.14, Pi: 3.14,
Colors: [][]string{ Colors: [][]string{
+1 -1
View File
@@ -775,7 +775,7 @@ func lexDatetime(lx *lexer) stateFn {
return lexDatetime return lexDatetime
} }
switch r { switch r {
case '-', 'T', ':', '.', 'Z': case '-', 'T', ':', '.', 'Z', '+':
return lexDatetime return lexDatetime
} }
+21 -7
View File
@@ -1,14 +1,28 @@
language: go language: go
go_import_path: github.com/davecgh/go-spew
go: go:
- 1.5.4 - 1.6.x
- 1.6.3 - 1.7.x
- 1.7 - 1.8.x
- 1.9.x
- 1.10.x
- tip
sudo: false
install: install:
- go get -v golang.org/x/tools/cmd/cover - go get -v github.com/alecthomas/gometalinter
- gometalinter --install
script: script:
- go test -v -tags=safe ./spew - export PATH=$PATH:$HOME/gopath/bin
- go test -v -tags=testcgo ./spew -covermode=count -coverprofile=profile.cov - export GORACE="halt_on_error=1"
- test -z "$(gometalinter --disable-all
--enable=gofmt
--enable=golint
--enable=vet
--enable=gosimple
--enable=unconvert
--deadline=4m ./spew | tee /dev/stderr)"
- go test -v -race -tags safe ./spew
- go test -v -race -tags testcgo ./spew -covermode=atomic -coverprofile=profile.cov
after_success: after_success:
- go get -v github.com/mattn/goveralls - go get -v github.com/mattn/goveralls
- export PATH=$PATH:$HOME/gopath/bin
- goveralls -coverprofile=profile.cov -service=travis-ci - goveralls -coverprofile=profile.cov -service=travis-ci
+1 -1
View File
@@ -2,7 +2,7 @@ ISC License
Copyright (c) 2012-2016 Dave Collins <dave@davec.name> Copyright (c) 2012-2016 Dave Collins <dave@davec.name>
Permission to use, copy, modify, and distribute this software for any Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies. copyright notice and this permission notice appear in all copies.
+4 -8
View File
@@ -1,12 +1,9 @@
go-spew go-spew
======= =======
[![Build Status](https://img.shields.io/travis/davecgh/go-spew.svg)] [![Build Status](https://img.shields.io/travis/davecgh/go-spew.svg)](https://travis-ci.org/davecgh/go-spew)
(https://travis-ci.org/davecgh/go-spew) [![ISC License] [![ISC License](http://img.shields.io/badge/license-ISC-blue.svg)](http://copyfree.org)
(http://img.shields.io/badge/license-ISC-blue.svg)](http://copyfree.org) [![Coverage Status] [![Coverage Status](https://img.shields.io/coveralls/davecgh/go-spew.svg)](https://coveralls.io/r/davecgh/go-spew?branch=master)
(https://img.shields.io/coveralls/davecgh/go-spew.svg)]
(https://coveralls.io/r/davecgh/go-spew?branch=master)
Go-spew implements a deep pretty printer for Go data structures to aid in Go-spew implements a deep pretty printer for Go data structures to aid in
debugging. A comprehensive suite of tests with 100% test coverage is provided debugging. A comprehensive suite of tests with 100% test coverage is provided
@@ -21,8 +18,7 @@ post about it
## Documentation ## Documentation
[![GoDoc](https://img.shields.io/badge/godoc-reference-blue.svg)] [![GoDoc](https://img.shields.io/badge/godoc-reference-blue.svg)](http://godoc.org/github.com/davecgh/go-spew/spew)
(http://godoc.org/github.com/davecgh/go-spew/spew)
Full `go doc` style documentation for the project can be viewed online without Full `go doc` style documentation for the project can be viewed online without
installing this package by using the excellent GoDoc site here: installing this package by using the excellent GoDoc site here:
+90 -97
View File
@@ -16,7 +16,9 @@
// when the code is not running on Google App Engine, compiled by GopherJS, and // when the code is not running on Google App Engine, compiled by GopherJS, and
// "-tags safe" is not added to the go build command line. The "disableunsafe" // "-tags safe" is not added to the go build command line. The "disableunsafe"
// tag is deprecated and thus should not be used. // tag is deprecated and thus should not be used.
// +build !js,!appengine,!safe,!disableunsafe // Go versions prior to 1.4 are disabled because they use a different layout
// for interfaces which make the implementation of unsafeReflectValue more complex.
// +build !js,!appengine,!safe,!disableunsafe,go1.4
package spew package spew
@@ -34,80 +36,49 @@ const (
ptrSize = unsafe.Sizeof((*byte)(nil)) ptrSize = unsafe.Sizeof((*byte)(nil))
) )
var ( type flag uintptr
// offsetPtr, offsetScalar, and offsetFlag are the offsets for the
// internal reflect.Value fields. These values are valid before golang
// commit ecccf07e7f9d which changed the format. The are also valid
// after commit 82f48826c6c7 which changed the format again to mirror
// the original format. Code in the init function updates these offsets
// as necessary.
offsetPtr = uintptr(ptrSize)
offsetScalar = uintptr(0)
offsetFlag = uintptr(ptrSize * 2)
// flagKindWidth and flagKindShift indicate various bits that the var (
// reflect package uses internally to track kind information. // flagRO indicates whether the value field of a reflect.Value
// // is read-only.
// flagRO indicates whether or not the value field of a reflect.Value is flagRO flag
// read-only.
// // flagAddr indicates whether the address of the reflect.Value's
// flagIndir indicates whether the value field of a reflect.Value is // value may be taken.
// the actual data or a pointer to the data. flagAddr flag
//
// These values are valid before golang commit 90a7c3c86944 which
// changed their positions. Code in the init function updates these
// flags as necessary.
flagKindWidth = uintptr(5)
flagKindShift = uintptr(flagKindWidth - 1)
flagRO = uintptr(1 << 0)
flagIndir = uintptr(1 << 1)
) )
func init() { // flagKindMask holds the bits that make up the kind
// Older versions of reflect.Value stored small integers directly in the // part of the flags field. In all the supported versions,
// ptr field (which is named val in the older versions). Versions // it is in the lower 5 bits.
// between commits ecccf07e7f9d and 82f48826c6c7 added a new field named const flagKindMask = flag(0x1f)
// scalar for this purpose which unfortunately came before the flag
// field, so the offset of the flag field is different for those
// versions.
//
// This code constructs a new reflect.Value from a known small integer
// and checks if the size of the reflect.Value struct indicates it has
// the scalar field. When it does, the offsets are updated accordingly.
vv := reflect.ValueOf(0xf00)
if unsafe.Sizeof(vv) == (ptrSize * 4) {
offsetScalar = ptrSize * 2
offsetFlag = ptrSize * 3
}
// Commit 90a7c3c86944 changed the flag positions such that the low // Different versions of Go have used different
// order bits are the kind. This code extracts the kind from the flags // bit layouts for the flags type. This table
// field and ensures it's the correct type. When it's not, the flag // records the known combinations.
// order has been changed to the newer format, so the flags are updated var okFlags = []struct {
// accordingly. ro, addr flag
upf := unsafe.Pointer(uintptr(unsafe.Pointer(&vv)) + offsetFlag) }{{
upfv := *(*uintptr)(upf) // From Go 1.4 to 1.5
flagKindMask := uintptr((1<<flagKindWidth - 1) << flagKindShift) ro: 1 << 5,
if (upfv&flagKindMask)>>flagKindShift != uintptr(reflect.Int) { addr: 1 << 7,
flagKindShift = 0 }, {
flagRO = 1 << 5 // Up to Go tip.
flagIndir = 1 << 6 ro: 1<<5 | 1<<6,
addr: 1 << 8,
}}
// Commit adf9b30e5594 modified the flags to separate the var flagValOffset = func() uintptr {
// flagRO flag into two bits which specifies whether or not the field, ok := reflect.TypeOf(reflect.Value{}).FieldByName("flag")
// field is embedded. This causes flagIndir to move over a bit if !ok {
// and means that flagRO is the combination of either of the panic("reflect.Value has no flag field")
// original flagRO bit and the new bit.
//
// This code detects the change by extracting what used to be
// the indirect bit to ensure it's set. When it's not, the flag
// order has been changed to the newer format, so the flags are
// updated accordingly.
if upfv&flagIndir == 0 {
flagRO = 3 << 5
flagIndir = 1 << 7
}
} }
return field.Offset
}()
// flagField returns a pointer to the flag field of a reflect.Value.
func flagField(v *reflect.Value) *flag {
return (*flag)(unsafe.Pointer(uintptr(unsafe.Pointer(v)) + flagValOffset))
} }
// unsafeReflectValue converts the passed reflect.Value into a one that bypasses // unsafeReflectValue converts the passed reflect.Value into a one that bypasses
@@ -119,34 +90,56 @@ func init() {
// This allows us to check for implementations of the Stringer and error // This allows us to check for implementations of the Stringer and error
// interfaces to be used for pretty printing ordinarily unaddressable and // interfaces to be used for pretty printing ordinarily unaddressable and
// inaccessible values such as unexported struct fields. // inaccessible values such as unexported struct fields.
func unsafeReflectValue(v reflect.Value) (rv reflect.Value) { func unsafeReflectValue(v reflect.Value) reflect.Value {
indirects := 1 if !v.IsValid() || (v.CanInterface() && v.CanAddr()) {
vt := v.Type() return v
upv := unsafe.Pointer(uintptr(unsafe.Pointer(&v)) + offsetPtr) }
rvf := *(*uintptr)(unsafe.Pointer(uintptr(unsafe.Pointer(&v)) + offsetFlag)) flagFieldPtr := flagField(&v)
if rvf&flagIndir != 0 { *flagFieldPtr &^= flagRO
vt = reflect.PtrTo(v.Type()) *flagFieldPtr |= flagAddr
indirects++ return v
} else if offsetScalar != 0 { }
// The value is in the scalar field when it's not one of the
// reference types. // Sanity checks against future reflect package changes
switch vt.Kind() { // to the type or semantics of the Value.flag field.
case reflect.Uintptr: func init() {
case reflect.Chan: field, ok := reflect.TypeOf(reflect.Value{}).FieldByName("flag")
case reflect.Func: if !ok {
case reflect.Map: panic("reflect.Value has no flag field")
case reflect.Ptr: }
case reflect.UnsafePointer: if field.Type.Kind() != reflect.TypeOf(flag(0)).Kind() {
default: panic("reflect.Value flag field has changed kind")
upv = unsafe.Pointer(uintptr(unsafe.Pointer(&v)) + }
offsetScalar) type t0 int
var t struct {
A t0
// t0 will have flagEmbedRO set.
t0
// a will have flagStickyRO set
a t0
}
vA := reflect.ValueOf(t).FieldByName("A")
va := reflect.ValueOf(t).FieldByName("a")
vt0 := reflect.ValueOf(t).FieldByName("t0")
// Infer flagRO from the difference between the flags
// for the (otherwise identical) fields in t.
flagPublic := *flagField(&vA)
flagWithRO := *flagField(&va) | *flagField(&vt0)
flagRO = flagPublic ^ flagWithRO
// Infer flagAddr from the difference between a value
// taken from a pointer and not.
vPtrA := reflect.ValueOf(&t).Elem().FieldByName("A")
flagNoPtr := *flagField(&vA)
flagPtr := *flagField(&vPtrA)
flagAddr = flagNoPtr ^ flagPtr
// Check that the inferred flags tally with one of the known versions.
for _, f := range okFlags {
if flagRO == f.ro && flagAddr == f.addr {
return
} }
} }
panic("reflect.Value read-only flag has changed semantics")
pv := reflect.NewAt(vt, upv)
rv = pv
for i := 0; i < indirects; i++ {
rv = rv.Elem()
}
return rv
} }
+1 -1
View File
@@ -16,7 +16,7 @@
// when the code is running on Google App Engine, compiled by GopherJS, or // when the code is running on Google App Engine, compiled by GopherJS, or
// "-tags safe" is added to the go build command line. The "disableunsafe" // "-tags safe" is added to the go build command line. The "disableunsafe"
// tag is deprecated and thus should not be used. // tag is deprecated and thus should not be used.
// +build js appengine safe disableunsafe // +build js appengine safe disableunsafe !go1.4
package spew package spew
+1 -1
View File
@@ -180,7 +180,7 @@ func printComplex(w io.Writer, c complex128, floatPrecision int) {
w.Write(closeParenBytes) w.Write(closeParenBytes)
} }
// printHexPtr outputs a uintptr formatted as hexidecimal with a leading '0x' // printHexPtr outputs a uintptr formatted as hexadecimal with a leading '0x'
// prefix to Writer w. // prefix to Writer w.
func printHexPtr(w io.Writer, p uintptr) { func printHexPtr(w io.Writer, p uintptr) {
// Null pointer. // Null pointer.
+5 -5
View File
@@ -35,16 +35,16 @@ var (
// cCharRE is a regular expression that matches a cgo char. // cCharRE is a regular expression that matches a cgo char.
// It is used to detect character arrays to hexdump them. // It is used to detect character arrays to hexdump them.
cCharRE = regexp.MustCompile("^.*\\._Ctype_char$") cCharRE = regexp.MustCompile(`^.*\._Ctype_char$`)
// cUnsignedCharRE is a regular expression that matches a cgo unsigned // cUnsignedCharRE is a regular expression that matches a cgo unsigned
// char. It is used to detect unsigned character arrays to hexdump // char. It is used to detect unsigned character arrays to hexdump
// them. // them.
cUnsignedCharRE = regexp.MustCompile("^.*\\._Ctype_unsignedchar$") cUnsignedCharRE = regexp.MustCompile(`^.*\._Ctype_unsignedchar$`)
// cUint8tCharRE is a regular expression that matches a cgo uint8_t. // cUint8tCharRE is a regular expression that matches a cgo uint8_t.
// It is used to detect uint8_t arrays to hexdump them. // It is used to detect uint8_t arrays to hexdump them.
cUint8tCharRE = regexp.MustCompile("^.*\\._Ctype_uint8_t$") cUint8tCharRE = regexp.MustCompile(`^.*\._Ctype_uint8_t$`)
) )
// dumpState contains information about the state of a dump operation. // dumpState contains information about the state of a dump operation.
@@ -143,10 +143,10 @@ func (d *dumpState) dumpPtr(v reflect.Value) {
// Display dereferenced value. // Display dereferenced value.
d.w.Write(openParenBytes) d.w.Write(openParenBytes)
switch { switch {
case nilFound == true: case nilFound:
d.w.Write(nilAngleBytes) d.w.Write(nilAngleBytes)
case cycleFound == true: case cycleFound:
d.w.Write(circularBytes) d.w.Write(circularBytes)
default: default:
+1 -1
View File
@@ -768,7 +768,7 @@ func addUintptrDumpTests() {
func addUnsafePointerDumpTests() { func addUnsafePointerDumpTests() {
// Null pointer. // Null pointer.
v := unsafe.Pointer(uintptr(0)) v := unsafe.Pointer(nil)
nv := (*unsafe.Pointer)(nil) nv := (*unsafe.Pointer)(nil)
pv := &v pv := &v
vAddr := fmt.Sprintf("%p", pv) vAddr := fmt.Sprintf("%p", pv)
+4 -2
View File
@@ -82,18 +82,20 @@ func addCgoDumpTests() {
v5Len := fmt.Sprintf("%d", v5l) v5Len := fmt.Sprintf("%d", v5l)
v5Cap := fmt.Sprintf("%d", v5c) v5Cap := fmt.Sprintf("%d", v5c)
v5t := "[6]testdata._Ctype_uint8_t" v5t := "[6]testdata._Ctype_uint8_t"
v5t2 := "[6]testdata._Ctype_uchar"
v5s := "(len=" + v5Len + " cap=" + v5Cap + ") " + v5s := "(len=" + v5Len + " cap=" + v5Cap + ") " +
"{\n 00000000 74 65 73 74 35 00 " + "{\n 00000000 74 65 73 74 35 00 " +
" |test5.|\n}" " |test5.|\n}"
addDumpTest(v5, "("+v5t+") "+v5s+"\n") addDumpTest(v5, "("+v5t+") "+v5s+"\n", "("+v5t2+") "+v5s+"\n")
// C typedefed unsigned char array. // C typedefed unsigned char array.
v6, v6l, v6c := testdata.GetCgoTypdefedUnsignedCharArray() v6, v6l, v6c := testdata.GetCgoTypdefedUnsignedCharArray()
v6Len := fmt.Sprintf("%d", v6l) v6Len := fmt.Sprintf("%d", v6l)
v6Cap := fmt.Sprintf("%d", v6c) v6Cap := fmt.Sprintf("%d", v6c)
v6t := "[6]testdata._Ctype_custom_uchar_t" v6t := "[6]testdata._Ctype_custom_uchar_t"
v6t2 := "[6]testdata._Ctype_uchar"
v6s := "(len=" + v6Len + " cap=" + v6Cap + ") " + v6s := "(len=" + v6Len + " cap=" + v6Cap + ") " +
"{\n 00000000 74 65 73 74 36 00 " + "{\n 00000000 74 65 73 74 36 00 " +
" |test6.|\n}" " |test6.|\n}"
addDumpTest(v6, "("+v6t+") "+v6s+"\n") addDumpTest(v6, "("+v6t+") "+v6s+"\n", "("+v6t2+") "+v6s+"\n")
} }
+2 -2
View File
@@ -182,10 +182,10 @@ func (f *formatState) formatPtr(v reflect.Value) {
// Display dereferenced value. // Display dereferenced value.
switch { switch {
case nilFound == true: case nilFound:
f.fs.Write(nilAngleBytes) f.fs.Write(nilAngleBytes)
case cycleFound == true: case cycleFound:
f.fs.Write(circularShortBytes) f.fs.Write(circularShortBytes)
default: default:
+3 -3
View File
@@ -1083,7 +1083,7 @@ func addUintptrFormatterTests() {
func addUnsafePointerFormatterTests() { func addUnsafePointerFormatterTests() {
// Null pointer. // Null pointer.
v := unsafe.Pointer(uintptr(0)) v := unsafe.Pointer(nil)
nv := (*unsafe.Pointer)(nil) nv := (*unsafe.Pointer)(nil)
pv := &v pv := &v
vAddr := fmt.Sprintf("%p", pv) vAddr := fmt.Sprintf("%p", pv)
@@ -1536,14 +1536,14 @@ func TestPrintSortedKeys(t *testing.T) {
t.Errorf("Sorted keys mismatch 3:\n %v %v", s, expected) t.Errorf("Sorted keys mismatch 3:\n %v %v", s, expected)
} }
s = cfg.Sprint(map[testStruct]int{testStruct{1}: 1, testStruct{3}: 3, testStruct{2}: 2}) s = cfg.Sprint(map[testStruct]int{{1}: 1, {3}: 3, {2}: 2})
expected = "map[ts.1:1 ts.2:2 ts.3:3]" expected = "map[ts.1:1 ts.2:2 ts.3:3]"
if s != expected { if s != expected {
t.Errorf("Sorted keys mismatch 4:\n %v %v", s, expected) t.Errorf("Sorted keys mismatch 4:\n %v %v", s, expected)
} }
if !spew.UnsafeDisabled { if !spew.UnsafeDisabled {
s = cfg.Sprint(map[testStructP]int{testStructP{1}: 1, testStructP{3}: 3, testStructP{2}: 2}) s = cfg.Sprint(map[testStructP]int{{1}: 1, {3}: 3, {2}: 2})
expected = "map[ts.1:1 ts.2:2 ts.3:3]" expected = "map[ts.1:1 ts.2:2 ts.3:3]"
if s != expected { if s != expected {
t.Errorf("Sorted keys mismatch 5:\n %v %v", s, expected) t.Errorf("Sorted keys mismatch 5:\n %v %v", s, expected)
+1 -4
View File
@@ -36,10 +36,7 @@ type dummyFmtState struct {
} }
func (dfs *dummyFmtState) Flag(f int) bool { func (dfs *dummyFmtState) Flag(f int) bool {
if f == int('+') { return f == int('+')
return true
}
return false
} }
func (dfs *dummyFmtState) Precision() (int, bool) { func (dfs *dummyFmtState) Precision() (int, bool) {
+5 -6
View File
@@ -16,7 +16,7 @@
// when the code is not running on Google App Engine, compiled by GopherJS, and // when the code is not running on Google App Engine, compiled by GopherJS, and
// "-tags safe" is not added to the go build command line. The "disableunsafe" // "-tags safe" is not added to the go build command line. The "disableunsafe"
// tag is deprecated and thus should not be used. // tag is deprecated and thus should not be used.
// +build !js,!appengine,!safe,!disableunsafe // +build !js,!appengine,!safe,!disableunsafe,go1.4
/* /*
This test file is part of the spew package rather than than the spew_test This test file is part of the spew package rather than than the spew_test
@@ -30,7 +30,6 @@ import (
"bytes" "bytes"
"reflect" "reflect"
"testing" "testing"
"unsafe"
) )
// changeKind uses unsafe to intentionally change the kind of a reflect.Value to // changeKind uses unsafe to intentionally change the kind of a reflect.Value to
@@ -38,13 +37,13 @@ import (
// fallback code which punts to the standard fmt library for new types that // fallback code which punts to the standard fmt library for new types that
// might get added to the language. // might get added to the language.
func changeKind(v *reflect.Value, readOnly bool) { func changeKind(v *reflect.Value, readOnly bool) {
rvf := (*uintptr)(unsafe.Pointer(uintptr(unsafe.Pointer(v)) + offsetFlag)) flags := flagField(v)
*rvf = *rvf | ((1<<flagKindWidth - 1) << flagKindShift)
if readOnly { if readOnly {
*rvf |= flagRO *flags |= flagRO
} else { } else {
*rvf &= ^uintptr(flagRO) *flags &^= flagRO
} }
*flags |= flagKindMask
} }
// TestAddedReflectValue tests functionaly of the dump and formatter code which // TestAddedReflectValue tests functionaly of the dump and formatter code which
+2
View File
@@ -1,6 +1,8 @@
language: go language: go
go: go:
- 1.7.x - 1.7.x
- 1.8.x
- 1.9.x
- tip - tip
sudo: false sudo: false
before_install: before_install:
+2 -2
View File
@@ -81,8 +81,8 @@ n := s.Names() // Get a []string
f := s.Field(name) // Get a *Field based on the given field name f := s.Field(name) // Get a *Field based on the given field name
f, ok := s.FieldOk(name) // Get a *Field based on the given field name f, ok := s.FieldOk(name) // Get a *Field based on the given field name
n := s.Name() // Get the struct name n := s.Name() // Get the struct name
h := s.HasZero() // Check if any field is initialized h := s.HasZero() // Check if any field is uninitialized
z := s.IsZero() // Check if all fields are initialized z := s.IsZero() // Check if all fields are uninitialized
``` ```
### Field methods ### Field methods
+1 -1
View File
@@ -135,7 +135,7 @@ func TestField_Set(t *testing.T) {
func TestField_NotSettable(t *testing.T) { func TestField_NotSettable(t *testing.T) {
a := map[int]Baz{ a := map[int]Baz{
4: Baz{ 4: {
A: "value", A: "value",
}, },
} }
+2 -4
View File
@@ -203,9 +203,7 @@ func (s *Struct) Values() []interface{} {
if IsStruct(val.Interface()) && !tagOpts.Has("omitnested") { if IsStruct(val.Interface()) && !tagOpts.Has("omitnested") {
// look out for embedded structs, and convert them to a // look out for embedded structs, and convert them to a
// []interface{} to be added to the final values slice // []interface{} to be added to the final values slice
for _, embeddedVal := range Values(val.Interface()) { t = append(t, Values(val.Interface())...)
t = append(t, embeddedVal)
}
} else { } else {
t = append(t, val.Interface()) t = append(t, val.Interface())
} }
@@ -573,7 +571,7 @@ func (s *Struct) nested(val reflect.Value) interface{} {
break break
} }
slices := make([]interface{}, val.Len(), val.Len()) slices := make([]interface{}, val.Len())
for x := 0; x < val.Len(); x++ { for x := 0; x < val.Len(); x++ {
slices[x] = s.nested(val.Index(x)) slices[x] = s.nested(val.Index(x))
} }
+1 -1
View File
@@ -301,7 +301,7 @@ func ExampleIsZero() {
Enabled bool Enabled bool
} }
// Nothing is initalized // Nothing is initialized
a := &Server{} a := &Server{}
isZeroA := IsZero(a) isZeroA := IsZero(a)
+9 -9
View File
@@ -380,7 +380,7 @@ func TestMap_NestedMapWithSliceIntValues(t *testing.T) {
b := &B{ b := &B{
Foo: map[string][]int{ Foo: map[string][]int{
"example_key": []int{80}, "example_key": {80},
}, },
} }
@@ -399,7 +399,7 @@ func TestMap_NestedMapWithSliceIntValues(t *testing.T) {
foo := in["Foo"].(map[string][]int) foo := in["Foo"].(map[string][]int)
if name := foo["example_key"]; name[0] != 80 { if name := foo["example_key"]; name[0] != 80 {
t.Errorf("Map nested struct's name field should give example, got: %s", name) t.Errorf("Map nested struct's name field should give example, got: %v", name)
} }
} }
@@ -418,7 +418,7 @@ func TestMap_NestedMapWithSliceStructValues(t *testing.T) {
b := &B{ b := &B{
Foo: map[string][]address{ Foo: map[string][]address{
"example_key": []address{ "example_key": {
{Country: "Turkey"}, {Country: "Turkey"},
}, },
}, },
@@ -463,8 +463,8 @@ func TestMap_NestedSliceWithStructValues(t *testing.T) {
p := person{ p := person{
Name: "test", Name: "test",
Addresses: []address{ Addresses: []address{
address{Country: "England"}, {Country: "England"},
address{Country: "Italy"}, {Country: "Italy"},
}, },
} }
mp := Map(p) mp := Map(p)
@@ -492,8 +492,8 @@ func TestMap_NestedSliceWithPointerOfStructValues(t *testing.T) {
p := person{ p := person{
Name: "test", Name: "test",
Addresses: []*address{ Addresses: []*address{
&address{Country: "England"}, {Country: "England"},
&address{Country: "Italy"}, {Country: "Italy"},
}, },
} }
mp := Map(p) mp := Map(p)
@@ -1430,14 +1430,14 @@ func TestPointer2Pointer(t *testing.T) {
func TestMap_InterfaceTypeWithMapValue(t *testing.T) { func TestMap_InterfaceTypeWithMapValue(t *testing.T) {
type A struct { type A struct {
Name string `structs:"name"` Name string `structs:"name"`
Ip string `structs:"ip"` IP string `structs:"ip"`
Query string `structs:"query"` Query string `structs:"query"`
Payload interface{} `structs:"payload"` Payload interface{} `structs:"payload"`
} }
a := A{ a := A{
Name: "test", Name: "test",
Ip: "127.0.0.1", IP: "127.0.0.1",
Query: "", Query: "",
Payload: map[string]string{"test_param": "test_param"}, Payload: map[string]string{"test_param": "test_param"},
} }
+1 -1
View File
@@ -5,7 +5,7 @@ import "strings"
// tagOptions contains a slice of tag options // tagOptions contains a slice of tag options
type tagOptions []string type tagOptions []string
// Has returns true if the given optiton is available in tagOptions // Has returns true if the given option is available in tagOptions
func (t tagOptions) Has(opt string) bool { func (t tagOptions) Has(opt string) bool {
for _, tagOpt := range t { for _, tagOpt := range t {
if tagOpt == opt { if tagOpt == opt {
+1
View File
@@ -0,0 +1 @@
module github.com/google/go-querystring
-4
View File
@@ -1,7 +1,3 @@
**This package is currently in development and the API may not be stable.**
The API will become stable with v1.
# uuid ![build status](https://travis-ci.org/google/uuid.svg?branch=master) # uuid ![build status](https://travis-ci.org/google/uuid.svg?branch=master)
The uuid package generates and inspects UUIDs based on The uuid package generates and inspects UUIDs based on
[RFC 4122](http://tools.ietf.org/html/rfc4122) [RFC 4122](http://tools.ietf.org/html/rfc4122)
+2 -2
View File
@@ -42,7 +42,7 @@ func NewDCESecurity(domain Domain, id uint32) (UUID, error) {
// NewDCEPerson returns a DCE Security (Version 2) UUID in the person // NewDCEPerson returns a DCE Security (Version 2) UUID in the person
// domain with the id returned by os.Getuid. // domain with the id returned by os.Getuid.
// //
// NewDCEPerson(Person, uint32(os.Getuid())) // NewDCESecurity(Person, uint32(os.Getuid()))
func NewDCEPerson() (UUID, error) { func NewDCEPerson() (UUID, error) {
return NewDCESecurity(Person, uint32(os.Getuid())) return NewDCESecurity(Person, uint32(os.Getuid()))
} }
@@ -50,7 +50,7 @@ func NewDCEPerson() (UUID, error) {
// NewDCEGroup returns a DCE Security (Version 2) UUID in the group // NewDCEGroup returns a DCE Security (Version 2) UUID in the group
// domain with the id returned by os.Getgid. // domain with the id returned by os.Getgid.
// //
// NewDCEGroup(Group, uint32(os.Getgid())) // NewDCESecurity(Group, uint32(os.Getgid()))
func NewDCEGroup() (UUID, error) { func NewDCEGroup() (UUID, error) {
return NewDCESecurity(Group, uint32(os.Getgid())) return NewDCESecurity(Group, uint32(os.Getgid()))
} }
+1
View File
@@ -0,0 +1 @@
module github.com/google/uuid
+1 -1
View File
@@ -27,7 +27,7 @@ var (
func NewHash(h hash.Hash, space UUID, data []byte, version int) UUID { func NewHash(h hash.Hash, space UUID, data []byte, version int) UUID {
h.Reset() h.Reset()
h.Write(space[:]) h.Write(space[:])
h.Write([]byte(data)) h.Write(data)
s := h.Sum(nil) s := h.Sum(nil)
var uuid UUID var uuid UUID
copy(uuid[:], s) copy(uuid[:], s)
-2
View File
@@ -15,8 +15,6 @@ func (uuid UUID) MarshalText() ([]byte, error) {
// UnmarshalText implements encoding.TextUnmarshaler. // UnmarshalText implements encoding.TextUnmarshaler.
func (uuid *UUID) UnmarshalText(data []byte) error { func (uuid *UUID) UnmarshalText(data []byte) error {
// See comment in ParseBytes why we do this.
// id, err := ParseBytes(data)
id, err := ParseBytes(data) id, err := ParseBytes(data)
if err == nil { if err == nil {
*uuid = id *uuid = id
+9 -23
View File
@@ -5,16 +5,14 @@
package uuid package uuid
import ( import (
"net"
"sync" "sync"
) )
var ( var (
nodeMu sync.Mutex nodeMu sync.Mutex
interfaces []net.Interface // cached list of interfaces ifname string // name of interface being used
ifname string // name of interface being used nodeID [6]byte // hardware for version 1 UUIDs
nodeID [6]byte // hardware for version 1 UUIDs zeroID [6]byte // nodeID with only 0's
zeroID [6]byte // nodeID with only 0's
) )
// NodeInterface returns the name of the interface from which the NodeID was // NodeInterface returns the name of the interface from which the NodeID was
@@ -39,20 +37,11 @@ func SetNodeInterface(name string) bool {
} }
func setNodeInterface(name string) bool { func setNodeInterface(name string) bool {
if interfaces == nil { iname, addr := getHardwareInterface(name) // null implementation for js
var err error if iname != "" && addr != nil {
interfaces, err = net.Interfaces() ifname = iname
if err != nil && name != "" { copy(nodeID[:], addr)
return false return true
}
}
for _, ifs := range interfaces {
if len(ifs.HardwareAddr) >= 6 && (name == "" || name == ifs.Name) {
copy(nodeID[:], ifs.HardwareAddr)
ifname = ifs.Name
return true
}
} }
// We found no interfaces with a valid hardware address. If name // We found no interfaces with a valid hardware address. If name
@@ -94,9 +83,6 @@ func SetNodeID(id []byte) bool {
// NodeID returns the 6 byte node id encoded in uuid. It returns nil if uuid is // NodeID returns the 6 byte node id encoded in uuid. It returns nil if uuid is
// not valid. The NodeID is only well defined for version 1 and 2 UUIDs. // not valid. The NodeID is only well defined for version 1 and 2 UUIDs.
func (uuid UUID) NodeID() []byte { func (uuid UUID) NodeID() []byte {
if len(uuid) != 16 {
return nil
}
var node [6]byte var node [6]byte
copy(node[:], uuid[10:]) copy(node[:], uuid[10:])
return node[:] return node[:]
+12
View File
@@ -0,0 +1,12 @@
// Copyright 2017 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build js
package uuid
// getHardwareInterface returns nil values for the JS version of the code.
// This remvoves the "net" dependency, because it is not used in the browser.
// Using the "net" library inflates the size of the transpiled JS code by 673k bytes.
func getHardwareInterface(name string) (string, []byte) { return "", nil }
+33
View File
@@ -0,0 +1,33 @@
// Copyright 2017 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build !js
package uuid
import "net"
var interfaces []net.Interface // cached list of interfaces
// getHardwareInterface returns the name and hardware address of interface name.
// If name is "" then the name and hardware address of one of the system's
// interfaces is returned. If no interfaces are found (name does not exist or
// there are no interfaces) then "", nil is returned.
//
// Only addresses of at least 6 bytes are returned.
func getHardwareInterface(name string) (string, []byte) {
if interfaces == nil {
var err error
interfaces, err = net.Interfaces()
if err != nil {
return "", nil
}
}
for _, ifs := range interfaces {
if len(ifs.HardwareAddr) >= 6 && (name == "" || name == ifs.Name) {
return ifs.Name, ifs.HardwareAddr
}
}
return "", nil
}
+1 -1
View File
@@ -16,7 +16,7 @@ var regressions = flag.Bool("regressions", false, "run uuid regression tests")
// TestClockSeqRace tests for a particular race condition of returning two // TestClockSeqRace tests for a particular race condition of returning two
// identical Version1 UUIDs. The duration of 1 minute was chosen as the race // identical Version1 UUIDs. The duration of 1 minute was chosen as the race
// condition, before being fixed, nearly always occured in under 30 seconds. // condition, before being fixed, nearly always occurred in under 30 seconds.
func TestClockSeqRace(t *testing.T) { func TestClockSeqRace(t *testing.T) {
if !*regressions { if !*regressions {
t.Skip("skipping regression tests") t.Skip("skipping regression tests")
+11 -10
View File
@@ -13,35 +13,36 @@ import (
// Currently, database types that map to string and []byte are supported. Please // Currently, database types that map to string and []byte are supported. Please
// consult database-specific driver documentation for matching types. // consult database-specific driver documentation for matching types.
func (uuid *UUID) Scan(src interface{}) error { func (uuid *UUID) Scan(src interface{}) error {
switch src.(type) { switch src := src.(type) {
case nil:
return nil
case string: case string:
// if an empty UUID comes from a table, we return a null UUID // if an empty UUID comes from a table, we return a null UUID
if src.(string) == "" { if src == "" {
return nil return nil
} }
// see Parse for required string format // see Parse for required string format
u, err := Parse(src.(string)) u, err := Parse(src)
if err != nil { if err != nil {
return fmt.Errorf("Scan: %v", err) return fmt.Errorf("Scan: %v", err)
} }
*uuid = u *uuid = u
case []byte:
b := src.([]byte)
case []byte:
// if an empty UUID comes from a table, we return a null UUID // if an empty UUID comes from a table, we return a null UUID
if len(b) == 0 { if len(src) == 0 {
return nil return nil
} }
// assumes a simple slice of bytes if 16 bytes // assumes a simple slice of bytes if 16 bytes
// otherwise attempts to parse // otherwise attempts to parse
if len(b) != 16 { if len(src) != 16 {
return uuid.Scan(string(b)) return uuid.Scan(string(src))
} }
copy((*uuid)[:], b) copy((*uuid)[:], src)
default: default:
return fmt.Errorf("Scan: unable to scan type %T into UUID", src) return fmt.Errorf("Scan: unable to scan type %T into UUID", src)
+14 -3
View File
@@ -10,9 +10,9 @@ import (
) )
func TestScan(t *testing.T) { func TestScan(t *testing.T) {
var stringTest string = "f47ac10b-58cc-0372-8567-0e02b2c3d479" stringTest := "f47ac10b-58cc-0372-8567-0e02b2c3d479"
var badTypeTest int = 6 badTypeTest := 6
var invalidTest string = "f47ac10b-58cc-0372-8567-0e02b2c3d4" invalidTest := "f47ac10b-58cc-0372-8567-0e02b2c3d4"
byteTest := make([]byte, 16) byteTest := make([]byte, 16)
byteTestUUID := Must(Parse(stringTest)) byteTestUUID := Must(Parse(stringTest))
@@ -90,6 +90,17 @@ func TestScan(t *testing.T) {
t.Error("UUID was not nil after scanning empty byte slice") t.Error("UUID was not nil after scanning empty byte slice")
} }
} }
uuid = UUID{}
err = (&uuid).Scan(nil)
if err != nil {
t.Fatal(err)
}
for _, v := range uuid {
if v != 0 {
t.Error("UUID was not nil after scanning nil")
}
}
} }
func TestValue(t *testing.T) { func TestValue(t *testing.T) {
+3 -3
View File
@@ -86,7 +86,7 @@ func clockSequence() int {
return int(clockSeq & 0x3fff) return int(clockSeq & 0x3fff)
} }
// SetClockSeq sets the clock sequence to the lower 14 bits of seq. Setting to // SetClockSequence sets the clock sequence to the lower 14 bits of seq. Setting to
// -1 causes a new sequence to be generated. // -1 causes a new sequence to be generated.
func SetClockSequence(seq int) { func SetClockSequence(seq int) {
defer timeMu.Unlock() defer timeMu.Unlock()
@@ -100,9 +100,9 @@ func setClockSequence(seq int) {
randomBits(b[:]) // clock sequence randomBits(b[:]) // clock sequence
seq = int(b[0])<<8 | int(b[1]) seq = int(b[0])<<8 | int(b[1])
} }
old_seq := clockSeq oldSeq := clockSeq
clockSeq = uint16(seq&0x3fff) | 0x8000 // Set our variant clockSeq = uint16(seq&0x3fff) | 0x8000 // Set our variant
if old_seq != clockSeq { if oldSeq != clockSeq {
lasttime = 0 lasttime = 0
} }
} }
+74 -20
View File
@@ -1,4 +1,4 @@
// Copyright 2016 Google Inc. All rights reserved. // Copyright 2018 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
@@ -35,20 +35,43 @@ const (
var rander = rand.Reader // random function var rander = rand.Reader // random function
// Parse decodes s into a UUID or returns an error. Both the UUID form of // Parse decodes s into a UUID or returns an error. Both the standard UUID
// xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx and // forms of xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx and
// urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx are decoded. // urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx are decoded as well as the
// Microsoft encoding {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx} and the raw hex
// encoding: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.
func Parse(s string) (UUID, error) { func Parse(s string) (UUID, error) {
var uuid UUID var uuid UUID
if len(s) != 36 { switch len(s) {
if len(s) != 36+9 { // xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
return uuid, fmt.Errorf("invalid UUID length: %d", len(s)) case 36:
}
// urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
case 36 + 9:
if strings.ToLower(s[:9]) != "urn:uuid:" { if strings.ToLower(s[:9]) != "urn:uuid:" {
return uuid, fmt.Errorf("invalid urn prefix: %q", s[:9]) return uuid, fmt.Errorf("invalid urn prefix: %q", s[:9])
} }
s = s[9:] s = s[9:]
// {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}
case 36 + 2:
s = s[1:]
// xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
case 32:
var ok bool
for i := range uuid {
uuid[i], ok = xtob(s[i*2], s[i*2+1])
if !ok {
return uuid, errors.New("invalid UUID format")
}
}
return uuid, nil
default:
return uuid, fmt.Errorf("invalid UUID length: %d", len(s))
} }
// s is now at least 36 bytes long
// it must be of the form xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
if s[8] != '-' || s[13] != '-' || s[18] != '-' || s[23] != '-' { if s[8] != '-' || s[13] != '-' || s[18] != '-' || s[23] != '-' {
return uuid, errors.New("invalid UUID format") return uuid, errors.New("invalid UUID format")
} }
@@ -58,11 +81,11 @@ func Parse(s string) (UUID, error) {
14, 16, 14, 16,
19, 21, 19, 21,
24, 26, 28, 30, 32, 34} { 24, 26, 28, 30, 32, 34} {
if v, ok := xtob(s[x], s[x+1]); !ok { v, ok := xtob(s[x], s[x+1])
if !ok {
return uuid, errors.New("invalid UUID format") return uuid, errors.New("invalid UUID format")
} else {
uuid[i] = v
} }
uuid[i] = v
} }
return uuid, nil return uuid, nil
} }
@@ -70,15 +93,29 @@ func Parse(s string) (UUID, error) {
// ParseBytes is like Parse, except it parses a byte slice instead of a string. // ParseBytes is like Parse, except it parses a byte slice instead of a string.
func ParseBytes(b []byte) (UUID, error) { func ParseBytes(b []byte) (UUID, error) {
var uuid UUID var uuid UUID
if len(b) != 36 { switch len(b) {
if len(b) != 36+9 { case 36: // xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
return uuid, fmt.Errorf("invalid UUID length: %d", len(b)) case 36 + 9: // urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
}
if !bytes.Equal(bytes.ToLower(b[:9]), []byte("urn:uuid:")) { if !bytes.Equal(bytes.ToLower(b[:9]), []byte("urn:uuid:")) {
return uuid, fmt.Errorf("invalid urn prefix: %q", b[:9]) return uuid, fmt.Errorf("invalid urn prefix: %q", b[:9])
} }
b = b[9:] b = b[9:]
case 36 + 2: // {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}
b = b[1:]
case 32: // xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
var ok bool
for i := 0; i < 32; i += 2 {
uuid[i/2], ok = xtob(b[i], b[i+1])
if !ok {
return uuid, errors.New("invalid UUID format")
}
}
return uuid, nil
default:
return uuid, fmt.Errorf("invalid UUID length: %d", len(b))
} }
// s is now at least 36 bytes long
// it must be of the form xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
if b[8] != '-' || b[13] != '-' || b[18] != '-' || b[23] != '-' { if b[8] != '-' || b[13] != '-' || b[18] != '-' || b[23] != '-' {
return uuid, errors.New("invalid UUID format") return uuid, errors.New("invalid UUID format")
} }
@@ -88,15 +125,32 @@ func ParseBytes(b []byte) (UUID, error) {
14, 16, 14, 16,
19, 21, 19, 21,
24, 26, 28, 30, 32, 34} { 24, 26, 28, 30, 32, 34} {
if v, ok := xtob(b[x], b[x+1]); !ok { v, ok := xtob(b[x], b[x+1])
if !ok {
return uuid, errors.New("invalid UUID format") return uuid, errors.New("invalid UUID format")
} else {
uuid[i] = v
} }
uuid[i] = v
} }
return uuid, nil return uuid, nil
} }
// MustParse is like Parse but panics if the string cannot be parsed.
// It simplifies safe initialization of global variables holding compiled UUIDs.
func MustParse(s string) UUID {
uuid, err := Parse(s)
if err != nil {
panic(`uuid: Parse(` + s + `): ` + err.Error())
}
return uuid
}
// FromBytes creates a new UUID from a byte slice. Returns an error if the slice
// does not have a length of 16. The bytes are copied from the slice.
func FromBytes(b []byte) (uuid UUID, err error) {
err = uuid.UnmarshalBinary(b)
return uuid, err
}
// Must returns uuid if err is nil and panics otherwise. // Must returns uuid if err is nil and panics otherwise.
func Must(uuid UUID, err error) UUID { func Must(uuid UUID, err error) UUID {
if err != nil { if err != nil {
@@ -123,7 +177,7 @@ func (uuid UUID) URN() string {
} }
func encodeHex(dst []byte, uuid UUID) { func encodeHex(dst []byte, uuid UUID) {
hex.Encode(dst[:], uuid[:4]) hex.Encode(dst, uuid[:4])
dst[8] = '-' dst[8] = '-'
hex.Encode(dst[9:13], uuid[4:6]) hex.Encode(dst[9:13], uuid[4:6])
dst[13] = '-' dst[13] = '-'
@@ -176,7 +230,7 @@ func (v Variant) String() string {
return fmt.Sprintf("BadVariant%d", int(v)) return fmt.Sprintf("BadVariant%d", int(v))
} }
// SetRand sets the random number generator to r, which implents io.Reader. // SetRand sets the random number generator to r, which implements io.Reader.
// If r.Read returns an error when the package requests random data then // If r.Read returns an error when the package requests random data then
// a panic will be issued. // a panic will be issued.
// //
+44 -11
View File
@@ -8,6 +8,7 @@ import (
"bytes" "bytes"
"fmt" "fmt"
"os" "os"
"runtime"
"strings" "strings"
"testing" "testing"
"time" "time"
@@ -58,12 +59,22 @@ var tests = []test{
{"f47ac10b-58cc-4372-e567-0e02b2c3d479", 4, Future, true}, {"f47ac10b-58cc-4372-e567-0e02b2c3d479", 4, Future, true},
{"f47ac10b-58cc-4372-f567-0e02b2c3d479", 4, Future, true}, {"f47ac10b-58cc-4372-f567-0e02b2c3d479", 4, Future, true},
{"f47ac10b158cc-5372-a567-0e02b2c3d479", 0, Invalid, false}, {"f47ac10b158cc-5372-a567-0e02b2c3d479", 0, Invalid, false},
{"f47ac10b-58cc25372-a567-0e02b2c3d479", 0, Invalid, false}, {"f47ac10b-58cc25372-a567-0e02b2c3d479", 0, Invalid, false},
{"f47ac10b-58cc-53723a567-0e02b2c3d479", 0, Invalid, false}, {"f47ac10b-58cc-53723a567-0e02b2c3d479", 0, Invalid, false},
{"f47ac10b-58cc-5372-a56740e02b2c3d479", 0, Invalid, false}, {"f47ac10b-58cc-5372-a56740e02b2c3d479", 0, Invalid, false},
{"f47ac10b-58cc-5372-a567-0e02-2c3d479", 0, Invalid, false}, {"f47ac10b-58cc-5372-a567-0e02-2c3d479", 0, Invalid, false},
{"g47ac10b-58cc-4372-a567-0e02b2c3d479", 0, Invalid, false}, {"g47ac10b-58cc-4372-a567-0e02b2c3d479", 0, Invalid, false},
{"{f47ac10b-58cc-0372-8567-0e02b2c3d479}", 0, RFC4122, true},
{"{f47ac10b-58cc-0372-8567-0e02b2c3d479", 0, Invalid, false},
{"f47ac10b-58cc-0372-8567-0e02b2c3d479}", 0, Invalid, false},
{"f47ac10b58cc037285670e02b2c3d479", 0, RFC4122, true},
{"f47ac10b58cc037285670e02b2c3d4790", 0, Invalid, false},
{"f47ac10b58cc037285670e02b2c3d47", 0, Invalid, false},
} }
var constants = []struct { var constants = []struct {
@@ -121,6 +132,25 @@ func TestUUID(t *testing.T) {
} }
} }
func TestFromBytes(t *testing.T) {
b := []byte{
0x7d, 0x44, 0x48, 0x40,
0x9d, 0xc0,
0x11, 0xd1,
0xb2, 0x45,
0x5f, 0xfd, 0xce, 0x74, 0xfa, 0xd2,
}
uuid, err := FromBytes(b)
if err != nil {
t.Fatalf("%s", err)
}
for i := 0; i < len(uuid); i++ {
if b[i] != uuid[i] {
t.Fatalf("FromBytes() got %v expected %v\b", uuid[:], b)
}
}
}
func TestConstants(t *testing.T) { func TestConstants(t *testing.T) {
for x, tt := range constants { for x, tt := range constants {
v, ok := tt.c.(fmt.Stringer) v, ok := tt.c.(fmt.Stringer)
@@ -246,7 +276,7 @@ func TestCoding(t *testing.T) {
if err != nil { if err != nil {
t.Errorf("Parse returned unexpected error %v", err) t.Errorf("Parse returned unexpected error %v", err)
} }
if data != data { if data != uuid {
t.Errorf("%s: decoded to %s, expected %s", text, uuid, data) t.Errorf("%s: decoded to %s, expected %s", text, uuid, data)
} }
} }
@@ -286,7 +316,7 @@ func TestVersion1(t *testing.T) {
case t1 > t2 && q1 == q2: case t1 > t2 && q1 == q2:
t.Error("time reversed") t.Error("time reversed")
case t1 < t2 && q1 != q2: case t1 < t2 && q1 != q2:
t.Error("clock sequence chaned unexpectedly") t.Error("clock sequence changed unexpectedly")
} }
} }
@@ -302,8 +332,10 @@ func TestNode(t *testing.T) {
if !SetNodeInterface("") { if !SetNodeInterface("") {
t.Error("SetNodeInterface failed") t.Error("SetNodeInterface failed")
} }
if ni := NodeInterface(); ni == "" { if runtime.GOARCH != "js" {
t.Error("NodeInterface returned an empty string") if ni := NodeInterface(); ni == "" {
t.Error("NodeInterface returned an empty string")
}
} }
ni := NodeID() ni := NodeID()
@@ -328,7 +360,7 @@ func TestNode(t *testing.T) {
} }
if ni := NodeInterface(); ni != "user" { if ni := NodeInterface(); ni != "user" {
t.Errorf("got inteface %q, want %q", ni, "user") t.Errorf("got interface %q, want %q", ni, "user")
} }
} }
@@ -372,8 +404,10 @@ func TestNodeID(t *testing.T) {
nid := []byte{1, 2, 3, 4, 5, 6} nid := []byte{1, 2, 3, 4, 5, 6}
SetNodeInterface("") SetNodeInterface("")
s := NodeInterface() s := NodeInterface()
if s == "" || s == "user" { if runtime.GOARCH != "js" {
t.Errorf("NodeInterface %q after SetInteface", s) if s == "" || s == "user" {
t.Errorf("NodeInterface %q after SetInterface", s)
}
} }
node1 := NodeID() node1 := NodeID()
if node1 == nil { if node1 == nil {
@@ -424,7 +458,7 @@ func TestDCE(t *testing.T) {
type badRand struct{} type badRand struct{}
func (r badRand) Read(buf []byte) (int, error) { func (r badRand) Read(buf []byte) (int, error) {
for i, _ := range buf { for i := range buf {
buf[i] = byte(i) buf[i] = byte(i)
} }
return len(buf), nil return len(buf), nil
@@ -435,13 +469,13 @@ func TestBadRand(t *testing.T) {
uuid1 := New() uuid1 := New()
uuid2 := New() uuid2 := New()
if uuid1 != uuid2 { if uuid1 != uuid2 {
t.Errorf("execpted duplicates, got %q and %q", uuid1, uuid2) t.Errorf("expected duplicates, got %q and %q", uuid1, uuid2)
} }
SetRand(nil) SetRand(nil)
uuid1 = New() uuid1 = New()
uuid2 = New() uuid2 = New()
if uuid1 == uuid2 { if uuid1 == uuid2 {
t.Errorf("unexecpted duplicates, got %q", uuid1) t.Errorf("unexpected duplicates, got %q", uuid1)
} }
} }
@@ -471,7 +505,6 @@ func parseBytesUnsafe(b []byte) (UUID, error) {
return Parse(*(*string)(unsafe.Pointer(&b))) return Parse(*(*string)(unsafe.Pointer(&b)))
} }
func BenchmarkParseBytesUnsafe(b *testing.B) { func BenchmarkParseBytesUnsafe(b *testing.B) {
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
_, err := parseBytesUnsafe(asBytes) _, err := parseBytesUnsafe(asBytes)
+1 -1
View File
@@ -13,7 +13,7 @@ import (
// or SetNodeInterface then it will be set automatically. If the NodeID cannot // or SetNodeInterface then it will be set automatically. If the NodeID cannot
// be set NewUUID returns nil. If clock sequence has not been set by // be set NewUUID returns nil. If clock sequence has not been set by
// SetClockSequence then it will be set automatically. If GetTime fails to // SetClockSequence then it will be set automatically. If GetTime fails to
// return the current NewUUID returns Nil and an error. // return the current NewUUID returns nil and an error.
// //
// In most cases, New should be used. // In most cases, New should be used.
func NewUUID() (UUID, error) { func NewUUID() (UUID, error) {
+3 -3
View File
@@ -6,7 +6,7 @@ package uuid
import "io" import "io"
// New is creates a new random UUID or panics. New is equivalent to // New creates a new random UUID or panics. New is equivalent to
// the expression // the expression
// //
// uuid.Must(uuid.NewRandom()) // uuid.Must(uuid.NewRandom())
@@ -14,12 +14,12 @@ func New() UUID {
return Must(NewRandom()) return Must(NewRandom())
} }
// NewRandom returns a Random (Version 4) UUID or panics. // NewRandom returns a Random (Version 4) UUID.
// //
// The strength of the UUIDs is based on the strength of the crypto/rand // The strength of the UUIDs is based on the strength of the crypto/rand
// package. // package.
// //
// A note about uniqueness derived from from the UUID Wikipedia entry: // A note about uniqueness derived from the UUID Wikipedia entry:
// //
// Randomly generated UUIDs have 122 random bits. One's annual risk of being // Randomly generated UUIDs have 122 random bits. One's annual risk of being
// hit by a meteorite is estimated to be one chance in 17 billion, that // hit by a meteorite is estimated to be one chance in 17 billion, that
+14 -4
View File
@@ -1,8 +1,18 @@
sudo: false sudo: false
language: go language: go
go: go:
- 1.1 - 1.7
- 1.2 - 1.8
- 1.3 - 1.9
- 1.4 - "1.10"
- tip - tip
before_install:
- go get golang.org/x/tools/cmd/cover
- go get github.com/mattn/goveralls
- go get github.com/golang/lint/golint
script:
- go test -v -covermode=count -coverprofile=coverage.out
- go vet ./...
- test -z "$(gofmt -d -s . | tee /dev/stderr)"
- test -z "$(golint ./... | tee /dev/stderr)"
- $HOME/gopath/bin/goveralls -coverprofile=coverage.out -service=travis-ci
+76 -133
View File
@@ -1,58 +1,37 @@
# HttpRouter [![Build Status](https://travis-ci.org/julienschmidt/httprouter.png?branch=master)](https://travis-ci.org/julienschmidt/httprouter) [![Coverage](http://gocover.io/_badge/github.com/julienschmidt/httprouter?0)](http://gocover.io/github.com/julienschmidt/httprouter) [![GoDoc](http://godoc.org/github.com/julienschmidt/httprouter?status.png)](http://godoc.org/github.com/julienschmidt/httprouter) # HttpRouter [![Build Status](https://travis-ci.org/julienschmidt/httprouter.svg?branch=master)](https://travis-ci.org/julienschmidt/httprouter) [![Coverage Status](https://coveralls.io/repos/github/julienschmidt/httprouter/badge.svg?branch=master)](https://coveralls.io/github/julienschmidt/httprouter?branch=master) [![GoDoc](https://godoc.org/github.com/julienschmidt/httprouter?status.svg)](http://godoc.org/github.com/julienschmidt/httprouter)
HttpRouter is a lightweight high performance HTTP request router HttpRouter is a lightweight high performance HTTP request router (also called *multiplexer* or just *mux* for short) for [Go](https://golang.org/).
(also called *multiplexer* or just *mux* for short) for [Go](http://golang.org/).
In contrast to the [default mux](http://golang.org/pkg/net/http/#ServeMux) of Go's net/http package, this router supports In contrast to the [default mux](https://golang.org/pkg/net/http/#ServeMux) of Go's `net/http` package, this router supports variables in the routing pattern and matches against the request method. It also scales better.
variables in the routing pattern and matches against the request method.
It also scales better.
The router is optimized for high performance and a small memory footprint. The router is optimized for high performance and a small memory footprint. It scales well even with very long paths and a large number of routes. A compressing dynamic trie (radix tree) structure is used for efficient matching.
It scales well even with very long paths and a large number of routes.
A compressing dynamic trie (radix tree) structure is used for efficient matching.
## Features ## Features
**Only explicit matches:** With other routers, like [http.ServeMux](http://golang.org/pkg/net/http/#ServeMux),
a requested URL path could match multiple patterns. Therefore they have some
awkward pattern priority rules, like *longest match* or *first registered,
first matched*. By design of this router, a request can only match exactly one
or no route. As a result, there are also no unintended matches, which makes it
great for SEO and improves the user experience.
**Stop caring about trailing slashes:** Choose the URL style you like, the **Only explicit matches:** With other routers, like [`http.ServeMux`](https://golang.org/pkg/net/http/#ServeMux), a requested URL path could match multiple patterns. Therefore they have some awkward pattern priority rules, like *longest match* or *first registered, first matched*. By design of this router, a request can only match exactly one or no route. As a result, there are also no unintended matches, which makes it great for SEO and improves the user experience.
router automatically redirects the client if a trailing slash is missing or if
there is one extra. Of course it only does so, if the new path has a handler.
If you don't like it, you can [turn off this behavior](http://godoc.org/github.com/julienschmidt/httprouter#Router.RedirectTrailingSlash).
**Path auto-correction:** Besides detecting the missing or additional trailing **Stop caring about trailing slashes:** Choose the URL style you like, the router automatically redirects the client if a trailing slash is missing or if there is one extra. Of course it only does so, if the new path has a handler. If you don't like it, you can [turn off this behavior](https://godoc.org/github.com/julienschmidt/httprouter#Router.RedirectTrailingSlash).
slash at no extra cost, the router can also fix wrong cases and remove
superfluous path elements (like `../` or `//`).
Is [CAPTAIN CAPS LOCK](http://www.urbandictionary.com/define.php?term=Captain+Caps+Lock) one of your users?
HttpRouter can help him by making a case-insensitive look-up and redirecting him
to the correct URL.
**Parameters in your routing pattern:** Stop parsing the requested URL path, **Path auto-correction:** Besides detecting the missing or additional trailing slash at no extra cost, the router can also fix wrong cases and remove superfluous path elements (like `../` or `//`). Is [CAPTAIN CAPS LOCK](http://www.urbandictionary.com/define.php?term=Captain+Caps+Lock) one of your users? HttpRouter can help him by making a case-insensitive look-up and redirecting him to the correct URL.
just give the path segment a name and the router delivers the dynamic value to
you. Because of the design of the router, path parameters are very cheap.
**Zero Garbage:** The matching and dispatching process generates zero bytes of **Parameters in your routing pattern:** Stop parsing the requested URL path, just give the path segment a name and the router delivers the dynamic value to you. Because of the design of the router, path parameters are very cheap.
garbage. In fact, the only heap allocations that are made, is by building the
slice of the key-value pairs for path parameters. If the request path contains
no parameters, not a single heap allocation is necessary.
**Best Performance:** [Benchmarks speak for themselves](https://github.com/julienschmidt/go-http-routing-benchmark). **Zero Garbage:** The matching and dispatching process generates zero bytes of garbage. The only heap allocations that are made are building the slice of the key-value pairs for path parameters, and building new context and request objects (the latter only in the standard `Handler`/`HandlerFunc` api). In the 3-argument API, if the request path contains no parameters not a single heap allocation is necessary.
See below for technical details of the implementation.
**No more server crashes:** You can set a [Panic handler](http://godoc.org/github.com/julienschmidt/httprouter#Router.PanicHandler) to deal with panics **Best Performance:** [Benchmarks speak for themselves](https://github.com/julienschmidt/go-http-routing-benchmark). See below for technical details of the implementation.
occurring during handling a HTTP request. The router then recovers and lets the
PanicHandler log what happened and deliver a nice error page.
Of course you can also set **custom [NotFound](http://godoc.org/github.com/julienschmidt/httprouter#Router.NotFound) and [MethodNotAllowed](http://godoc.org/github.com/julienschmidt/httprouter#Router.MethodNotAllowed) handlers** and [**serve static files**](http://godoc.org/github.com/julienschmidt/httprouter#Router.ServeFiles). **No more server crashes:** You can set a [Panic handler](https://godoc.org/github.com/julienschmidt/httprouter#Router.PanicHandler) to deal with panics occurring during handling a HTTP request. The router then recovers and lets the `PanicHandler` log what happened and deliver a nice error page.
**Perfect for APIs:** The router design encourages to build sensible, hierarchical RESTful APIs. Moreover it has builtin native support for [OPTIONS requests](http://zacstewart.com/2012/04/14/http-options-method.html) and `405 Method Not Allowed` replies.
Of course you can also set **custom [`NotFound`](https://godoc.org/github.com/julienschmidt/httprouter#Router.NotFound) and [`MethodNotAllowed`](https://godoc.org/github.com/julienschmidt/httprouter#Router.MethodNotAllowed) handlers** and [**serve static files**](https://godoc.org/github.com/julienschmidt/httprouter#Router.ServeFiles).
## Usage ## Usage
This is just a quick introduction, view the [GoDoc](http://godoc.org/github.com/julienschmidt/httprouter) for details. This is just a quick introduction, view the [GoDoc](http://godoc.org/github.com/julienschmidt/httprouter) for details.
Let's start with a trivial example: Let's start with a trivial example:
```go ```go
package main package main
@@ -81,12 +60,11 @@ func main() {
``` ```
### Named parameters ### Named parameters
As you can see, `:name` is a *named parameter*.
The values are accessible via `httprouter.Params`, which is just a slice of `httprouter.Param`s. As you can see, `:name` is a *named parameter*. The values are accessible via `httprouter.Params`, which is just a slice of `httprouter.Param`s. You can get the value of a parameter either by its index in the slice, or by using the `ByName(name)` method: `:name` can be retrived by `ByName("name")`.
You can get the value of a parameter either by its index in the slice, or by using the `ByName(name)` method:
`:name` can be retrived by `ByName("name")`.
Named parameters only match a single path segment: Named parameters only match a single path segment:
``` ```
Pattern: /user/:user Pattern: /user/:user
@@ -99,9 +77,9 @@ Pattern: /user/:user
**Note:** Since this router has only explicit matches, you can not register static routes and parameters for the same path segment. For example you can not register the patterns `/user/new` and `/user/:user` for the same request method at the same time. The routing of different request methods is independent from each other. **Note:** Since this router has only explicit matches, you can not register static routes and parameters for the same path segment. For example you can not register the patterns `/user/new` and `/user/:user` for the same request method at the same time. The routing of different request methods is independent from each other.
### Catch-All parameters ### Catch-All parameters
The second type are *catch-all* parameters and have the form `*name`.
Like the name suggests, they match everything. The second type are *catch-all* parameters and have the form `*name`. Like the name suggests, they match everything. Therefore they must always be at the **end** of the pattern:
Therefore they must always be at the **end** of the pattern:
``` ```
Pattern: /src/*filepath Pattern: /src/*filepath
@@ -111,11 +89,8 @@ Pattern: /src/*filepath
``` ```
## How does it work? ## How does it work?
The router relies on a tree structure which makes heavy use of *common prefixes*,
it is basically a *compact* [*prefix tree*](http://en.wikipedia.org/wiki/Trie) The router relies on a tree structure which makes heavy use of *common prefixes*, it is basically a *compact* [*prefix tree*](https://en.wikipedia.org/wiki/Trie) (or just [*Radix tree*](https://en.wikipedia.org/wiki/Radix_tree)). Nodes with a common prefix also share a common parent. Here is a short example what the routing tree for the `GET` request method could look like:
(or just [*Radix tree*](http://en.wikipedia.org/wiki/Radix_tree)).
Nodes with a common prefix also share a common parent. Here is a short example
what the routing tree for the `GET` request method could look like:
``` ```
Priority Path Handle Priority Path Handle
@@ -130,33 +105,15 @@ Priority Path Handle
1 | └team\ *<7> 1 | └team\ *<7>
1 └contact\ *<8> 1 └contact\ *<8>
``` ```
Every `*<num>` represents the memory address of a handler function (a pointer).
If you follow a path trough the tree from the root to the leaf, you get the
complete route path, e.g `\blog\:post\`, where `:post` is just a placeholder
([*parameter*](#named-parameters)) for an actual post name. Unlike hash-maps, a
tree structure also allows us to use dynamic parts like the `:post` parameter,
since we actually match against the routing patterns instead of just comparing
hashes. [As benchmarks show](https://github.com/julienschmidt/go-http-routing-benchmark),
this works very well and efficient.
Since URL paths have a hierarchical structure and make use only of a limited set Every `*<num>` represents the memory address of a handler function (a pointer). If you follow a path trough the tree from the root to the leaf, you get the complete route path, e.g `\blog\:post\`, where `:post` is just a placeholder ([*parameter*](#named-parameters)) for an actual post name. Unlike hash-maps, a tree structure also allows us to use dynamic parts like the `:post` parameter, since we actually match against the routing patterns instead of just comparing hashes. [As benchmarks show](https://github.com/julienschmidt/go-http-routing-benchmark), this works very well and efficient.
of characters (byte values), it is very likely that there are a lot of common
prefixes. This allows us to easily reduce the routing into ever smaller problems.
Moreover the router manages a separate tree for every request method.
For one thing it is more space efficient than holding a method->handle map in
every single node, for another thing is also allows us to greatly reduce the
routing problem before even starting the look-up in the prefix-tree.
For even better scalability, the child nodes on each tree level are ordered by Since URL paths have a hierarchical structure and make use only of a limited set of characters (byte values), it is very likely that there are a lot of common prefixes. This allows us to easily reduce the routing into ever smaller problems. Moreover the router manages a separate tree for every request method. For one thing it is more space efficient than holding a method->handle map in every single node, it also allows us to greatly reduce the routing problem before even starting the look-up in the prefix-tree.
priority, where the priority is just the number of handles registered in sub
nodes (children, grandchildren, and so on..).
This helps in two ways:
1. Nodes which are part of the most routing paths are evaluated first. This For even better scalability, the child nodes on each tree level are ordered by priority, where the priority is just the number of handles registered in sub nodes (children, grandchildren, and so on..). This helps in two ways:
helps to make as much routes as possible to be reachable as fast as possible.
2. It is some sort of cost compensation. The longest reachable path (highest 1. Nodes which are part of the most routing paths are evaluated first. This helps to make as much routes as possible to be reachable as fast as possible.
cost) can always be evaluated first. The following scheme visualizes the tree 2. It is some sort of cost compensation. The longest reachable path (highest cost) can always be evaluated first. The following scheme visualizes the tree structure. Nodes are evaluated from top to bottom and from left to right.
structure. Nodes are evaluated from top to bottom and from left to right.
``` ```
├------------ ├------------
@@ -168,47 +125,38 @@ structure. Nodes are evaluated from top to bottom and from left to right.
└- └-
``` ```
## Why doesn't this work with `http.Handler`?
## Why doesn't this work with http.Handler? **It does!** The router itself implements the `http.Handler` interface. Moreover the router provides convenient [adapters for `http.Handler`](https://godoc.org/github.com/julienschmidt/httprouter#Router.Handler)s and [`http.HandlerFunc`](https://godoc.org/github.com/julienschmidt/httprouter#Router.HandlerFunc)s which allows them to be used as a [`httprouter.Handle`](https://godoc.org/github.com/julienschmidt/httprouter#Router.Handle) when registering a route. The only disadvantage is, that no parameter values can be retrieved when a `http.Handler` or `http.HandlerFunc` is used, since there is no efficient way to pass the values with the existing function parameters. Therefore [`httprouter.Handle`](https://godoc.org/github.com/julienschmidt/httprouter#Router.Handle) has a third function parameter.
**It does!** The router itself implements the http.Handler interface.
Moreover the router provides convenient [adapters for http.Handler](http://godoc.org/github.com/julienschmidt/httprouter#Router.Handler)s and [http.HandlerFunc](http://godoc.org/github.com/julienschmidt/httprouter#Router.HandlerFunc)s
which allows them to be used as a [httprouter.Handle](http://godoc.org/github.com/julienschmidt/httprouter#Router.Handle) when registering a route.
The only disadvantage is, that no parameter values can be retrieved when a
http.Handler or http.HandlerFunc is used, since there is no efficient way to
pass the values with the existing function parameters.
Therefore [httprouter.Handle](http://godoc.org/github.com/julienschmidt/httprouter#Router.Handle) has a third function parameter.
Just try it out for yourself, the usage of HttpRouter is very straightforward. The package is compact and minimalistic, but also probably one of the easiest routers to set up. Just try it out for yourself, the usage of HttpRouter is very straightforward. The package is compact and minimalistic, but also probably one of the easiest routers to set up.
## Where can I find Middleware *X*? ## Where can I find Middleware *X*?
This package just provides a very efficient request router with a few extra
features. The router is just a [http.Handler](http://golang.org/pkg/net/http/#Handler), This package just provides a very efficient request router with a few extra features. The router is just a [`http.Handler`](https://golang.org/pkg/net/http/#Handler), you can chain any http.Handler compatible middleware before the router, for example the [Gorilla handlers](http://www.gorillatoolkit.org/pkg/handlers). Or you could [just write your own](https://justinas.org/writing-http-middleware-in-go/), it's very easy!
you can chain any http.Handler compatible middleware before the router,
for example the [Gorilla handlers](http://www.gorillatoolkit.org/pkg/handlers).
Or you could [just write your own](http://justinas.org/writing-http-middleware-in-go/),
it's very easy!
Alternatively, you could try [a web framework based on HttpRouter](#web-frameworks-based-on-httprouter). Alternatively, you could try [a web framework based on HttpRouter](#web-frameworks-based-on-httprouter).
### Multi-domain / Sub-domains ### Multi-domain / Sub-domains
Here is a quick example: Does your server serve multiple domains / hosts? Here is a quick example: Does your server serve multiple domains / hosts?
You want to use sub-domains? You want to use sub-domains?
Define a router per host! Define a router per host!
```go ```go
// We need an object that implements the http.Handler interface. // We need an object that implements the http.Handler interface.
// Therefore we need a type for which we implement the ServeHTTP method. // Therefore we need a type for which we implement the ServeHTTP method.
// We just use a map here, in which we map host names (with port) to http.Handlers // We just use a map here, in which we map host names (with port) to http.Handlers
type HostSwitch map[string]http.Handler type HostSwitch map[string]http.Handler
// Implement the ServerHTTP method on our new type // Implement the ServeHTTP method on our new type
func (hs HostSwitch) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (hs HostSwitch) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Check if a http.Handler is registered for the given host. // Check if a http.Handler is registered for the given host.
// If yes, use it to handle the request. // If yes, use it to handle the request.
if handler := hs[r.Host]; handler != nil { if handler := hs[r.Host]; handler != nil {
handler.ServeHTTP(w, r) handler.ServeHTTP(w, r)
} else { } else {
// Handle host names for wich no handler is registered // Handle host names for which no handler is registered
http.Error(w, "Forbidden", 403) // Or Redirect? http.Error(w, "Forbidden", 403) // Or Redirect?
} }
} }
@@ -230,94 +178,89 @@ func main() {
``` ```
### Basic Authentication ### Basic Authentication
Another quick example: Basic Authentification (RFC 2617) for handles:
Another quick example: Basic Authentication (RFC 2617) for handles:
```go ```go
package main package main
import ( import (
"bytes" "fmt"
"encoding/base64" "log"
"fmt" "net/http"
"github.com/julienschmidt/httprouter"
"net/http" "github.com/julienschmidt/httprouter"
"log"
"strings"
) )
func BasicAuth(h httprouter.Handle, user, pass []byte) httprouter.Handle { func BasicAuth(h httprouter.Handle, requiredUser, requiredPassword string) httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
const basicAuthPrefix string = "Basic "
// Get the Basic Authentication credentials // Get the Basic Authentication credentials
auth := r.Header.Get("Authorization") user, password, hasAuth := r.BasicAuth()
if strings.HasPrefix(auth, basicAuthPrefix) {
// Check credentials
payload, err := base64.StdEncoding.DecodeString(auth[len(basicAuthPrefix):])
if err == nil {
pair := bytes.SplitN(payload, []byte(":"), 2)
if len(pair) == 2 &&
bytes.Equal(pair[0], user) &&
bytes.Equal(pair[1], pass) {
// Delegate request to the given handle if hasAuth && user == requiredUser && password == requiredPassword {
h(w, r, ps) // Delegate request to the given handle
return h(w, r, ps)
} } else {
} // Request Basic Authentication otherwise
w.Header().Set("WWW-Authenticate", "Basic realm=Restricted")
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
} }
// Request Basic Authentication otherwise
w.Header().Set("WWW-Authenticate", "Basic realm=Restricted")
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
} }
} }
func Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { func Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
fmt.Fprint(w, "Not protected!\n") fmt.Fprint(w, "Not protected!\n")
} }
func Protected(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { func Protected(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
fmt.Fprint(w, "Protected!\n") fmt.Fprint(w, "Protected!\n")
} }
func main() { func main() {
user := []byte("gordon") user := "gordon"
pass := []byte("secret!") pass := "secret!"
router := httprouter.New() router := httprouter.New()
router.GET("/", Index) router.GET("/", Index)
router.GET("/protected/", BasicAuth(Protected, user, pass)) router.GET("/protected/", BasicAuth(Protected, user, pass))
log.Fatal(http.ListenAndServe(":8080", router)) log.Fatal(http.ListenAndServe(":8080", router))
} }
``` ```
## Chaining with the NotFound handler ## Chaining with the NotFound handler
**NOTE: It might be required to set [Router.HandleMethodNotAllowed](http://godoc.org/github.com/julienschmidt/httprouter#Router.HandleMethodNotAllowed) to `false` to avoid problems.** **NOTE: It might be required to set [`Router.HandleMethodNotAllowed`](https://godoc.org/github.com/julienschmidt/httprouter#Router.HandleMethodNotAllowed) to `false` to avoid problems.**
You can use another [http.HandlerFunc](http://golang.org/pkg/net/http/#HandlerFunc), for example another router, to handle requests which could not be matched by this router by using the [Router.NotFound](http://godoc.org/github.com/julienschmidt/httprouter#Router.NotFound) handler. This allows chaining. You can use another [`http.Handler`](https://golang.org/pkg/net/http/#Handler), for example another router, to handle requests which could not be matched by this router by using the [`Router.NotFound`](https://godoc.org/github.com/julienschmidt/httprouter#Router.NotFound) handler. This allows chaining.
### Static files ### Static files
The `NotFound` handler can for example be used to serve static files from the root path `/` (like an index.html file along with other assets):
The `NotFound` handler can for example be used to serve static files from the root path `/` (like an `index.html` file along with other assets):
```go ```go
// Serve static files from the ./public directory // Serve static files from the ./public directory
router.NotFound = http.FileServer(http.Dir("public")).ServeHTTP router.NotFound = http.FileServer(http.Dir("public"))
``` ```
But this approach sidesteps the strict core rules of this router to avoid routing problems. A cleaner approach is to use a distinct sub-path for serving files, like `/static/*filepath` or `/files/*filepath`. But this approach sidesteps the strict core rules of this router to avoid routing problems. A cleaner approach is to use a distinct sub-path for serving files, like `/static/*filepath` or `/files/*filepath`.
## Web Frameworks based on HttpRouter ## Web Frameworks based on HttpRouter
If the HttpRouter is a bit too minimalistic for you, you might try one of the following more high-level 3rd-party web frameworks building upon the HttpRouter package: If the HttpRouter is a bit too minimalistic for you, you might try one of the following more high-level 3rd-party web frameworks building upon the HttpRouter package:
* [Ace](https://github.com/plimble/ace): Blazing fast Go Web Framework * [Ace](https://github.com/plimble/ace): Blazing fast Go Web Framework
* [api2go](https://github.com/univedo/api2go): A JSON API Implementation for Go * [api2go](https://github.com/manyminds/api2go): A JSON API Implementation for Go
* [Gin](https://github.com/gin-gonic/gin): Features a martini-like API with much better performance * [Gin](https://github.com/gin-gonic/gin): Features a martini-like API with much better performance
* [Goat](https://github.com/bahlo/goat): A minimalistic REST API server in Go * [Goat](https://github.com/bahlo/goat): A minimalistic REST API server in Go
* [goMiddlewareChain](https://github.com/TobiEiss/goMiddlewareChain): An express.js-like-middleware-chain
* [Hikaru](https://github.com/najeira/hikaru): Supports standalone and Google AppEngine * [Hikaru](https://github.com/najeira/hikaru): Supports standalone and Google AppEngine
* [Hitch](https://github.com/nbio/hitch): Hitch ties httprouter, [httpcontext](https://github.com/nbio/httpcontext), and middleware up in a bow * [Hitch](https://github.com/nbio/hitch): Hitch ties httprouter, [httpcontext](https://github.com/nbio/httpcontext), and middleware up in a bow
* [httpway](https://github.com/corneldamian/httpway): Simple middleware extension with context for httprouter and a server with gracefully shutdown support
* [kami](https://github.com/guregu/kami): A tiny web framework using x/net/context * [kami](https://github.com/guregu/kami): A tiny web framework using x/net/context
* [Medeina](https://github.com/imdario/medeina): Inspired by Ruby's Roda and Cuba * [Medeina](https://github.com/imdario/medeina): Inspired by Ruby's Roda and Cuba
* [Neko](https://github.com/rocwong/neko): A lightweight web application framework for Golang * [Neko](https://github.com/rocwong/neko): A lightweight web application framework for Golang
* [River](https://github.com/abiosoft/river): River is a simple and lightweight REST server
* [Roxanna](https://github.com/iamthemuffinman/Roxanna): An amalgamation of httprouter, better logging, and hot reload * [Roxanna](https://github.com/iamthemuffinman/Roxanna): An amalgamation of httprouter, better logging, and hot reload
* [siesta](https://github.com/VividCortex/siesta): Composable HTTP handlers with contexts * [siesta](https://github.com/VividCortex/siesta): Composable HTTP handlers with contexts
* [xmux](https://github.com/rs/xmux): xmux is a httprouter fork on top of xhandler (net/context aware)
+38
View File
@@ -0,0 +1,38 @@
// +build go1.7
package httprouter
import (
"context"
"net/http"
)
type paramsKey struct{}
// ParamsKey is the request context key under which URL params are stored.
//
// This is only present from go 1.7.
var ParamsKey = paramsKey{}
// Handler is an adapter which allows the usage of an http.Handler as a
// request handle. With go 1.7+, the Params will be available in the
// request context under ParamsKey.
func (r *Router) Handler(method, path string, handler http.Handler) {
r.Handle(method, path,
func(w http.ResponseWriter, req *http.Request, p Params) {
ctx := req.Context()
ctx = context.WithValue(ctx, ParamsKey, p)
req = req.WithContext(ctx)
handler.ServeHTTP(w, req)
},
)
}
// ParamsFromContext pulls the URL parameters from a request context,
// or returns nil if none are present.
//
// This is only present from go 1.7.
func ParamsFromContext(ctx context.Context) Params {
p, _ := ctx.Value(ParamsKey).(Params)
return p
}
+16
View File
@@ -0,0 +1,16 @@
// +build !go1.7
package httprouter
import "net/http"
// Handler is an adapter which allows the usage of an http.Handler as a
// request handle. With go 1.7+, the Params will be available in the
// request context under ParamsKey.
func (r *Router) Handler(method, path string, handler http.Handler) {
r.Handle(method, path,
func(w http.ResponseWriter, req *http.Request, _ Params) {
handler.ServeHTTP(w, req)
},
)
}
+3 -3
View File
@@ -41,7 +41,7 @@ func CleanPath(p string) string {
buf[0] = '/' buf[0] = '/'
} }
trailing := n > 2 && p[n-1] == '/' trailing := n > 1 && p[n-1] == '/'
// A bit more clunky without a 'lazybuf' like the path package, but the loop // A bit more clunky without a 'lazybuf' like the path package, but the loop
// gets completely inlined (bufApp). So in contrast to the path package this // gets completely inlined (bufApp). So in contrast to the path package this
@@ -59,11 +59,11 @@ func CleanPath(p string) string {
case p[r] == '.' && p[r+1] == '/': case p[r] == '.' && p[r+1] == '/':
// . element // . element
r++ r += 2
case p[r] == '.' && p[r+1] == '.' && (r+2 == n || p[r+2] == '/'): case p[r] == '.' && p[r+1] == '.' && (r+2 == n || p[r+2] == '/'):
// .. element: remove to last / // .. element: remove to last /
r += 2 r += 3
if w > 1 { if w > 1 {
// can backtrack // can backtrack
+1
View File
@@ -22,6 +22,7 @@ var cleanTests = []struct {
// missing root // missing root
{"", "/"}, {"", "/"},
{"a/", "/a/"},
{"abc", "/abc"}, {"abc", "/abc"},
{"abc/def", "/abc/def"}, {"abc/def", "/abc/def"},
{"a/b/c", "/a/b/c"}, {"a/b/c", "/a/b/c"},
+66 -30
View File
@@ -55,7 +55,7 @@
// //
// Catch-all parameters match anything until the path end, including the // Catch-all parameters match anything until the path end, including the
// directory index (the '/' before the catch-all). Since they match anything // directory index (the '/' before the catch-all). Since they match anything
// until the end, catch-all paramerters must always be the final path element. // until the end, catch-all parameters must always be the final path element.
// Path: /files/*filepath // Path: /files/*filepath
// //
// Requests: // Requests:
@@ -138,14 +138,20 @@ type Router struct {
// handler. // handler.
HandleMethodNotAllowed bool HandleMethodNotAllowed bool
// Configurable http.HandlerFunc which is called when no matching route is // If enabled, the router automatically replies to OPTIONS requests.
// found. If it is not set, http.NotFound is used. // Custom OPTIONS handlers take priority over automatic replies.
NotFound http.HandlerFunc HandleOPTIONS bool
// Configurable http.HandlerFunc which is called when a request // Configurable http.Handler which is called when no matching route is
// found. If it is not set, http.NotFound is used.
NotFound http.Handler
// Configurable http.Handler which is called when a request
// cannot be routed and HandleMethodNotAllowed is true. // cannot be routed and HandleMethodNotAllowed is true.
// If it is not set, http.Error with http.StatusMethodNotAllowed is used. // If it is not set, http.Error with http.StatusMethodNotAllowed is used.
MethodNotAllowed http.HandlerFunc // The "Allow" header with allowed request methods is set before the handler
// is called.
MethodNotAllowed http.Handler
// Function to handle panics recovered from http handlers. // Function to handle panics recovered from http handlers.
// It should be used to generate a error page and return the http error code // It should be used to generate a error page and return the http error code
@@ -165,6 +171,7 @@ func New() *Router {
RedirectTrailingSlash: true, RedirectTrailingSlash: true,
RedirectFixedPath: true, RedirectFixedPath: true,
HandleMethodNotAllowed: true, HandleMethodNotAllowed: true,
HandleOPTIONS: true,
} }
} }
@@ -229,16 +236,6 @@ func (r *Router) Handle(method, path string, handle Handle) {
root.addRoute(path, handle) root.addRoute(path, handle)
} }
// Handler is an adapter which allows the usage of an http.Handler as a
// request handle.
func (r *Router) Handler(method, path string, handler http.Handler) {
r.Handle(method, path,
func(w http.ResponseWriter, req *http.Request, _ Params) {
handler.ServeHTTP(w, req)
},
)
}
// HandlerFunc is an adapter which allows the usage of an http.HandlerFunc as a // HandlerFunc is an adapter which allows the usage of an http.HandlerFunc as a
// request handle. // request handle.
func (r *Router) HandlerFunc(method, path string, handler http.HandlerFunc) { func (r *Router) HandlerFunc(method, path string, handler http.HandlerFunc) {
@@ -286,15 +283,53 @@ func (r *Router) Lookup(method, path string) (Handle, Params, bool) {
return nil, nil, false return nil, nil, false
} }
func (r *Router) allowed(path, reqMethod string) (allow string) {
if path == "*" { // server-wide
for method := range r.trees {
if method == "OPTIONS" {
continue
}
// add request method to list of allowed methods
if len(allow) == 0 {
allow = method
} else {
allow += ", " + method
}
}
} else { // specific path
for method := range r.trees {
// Skip the requested method - we already tried this one
if method == reqMethod || method == "OPTIONS" {
continue
}
handle, _, _ := r.trees[method].getValue(path)
if handle != nil {
// add request method to list of allowed methods
if len(allow) == 0 {
allow = method
} else {
allow += ", " + method
}
}
}
}
if len(allow) > 0 {
allow += ", OPTIONS"
}
return
}
// ServeHTTP makes the router implement the http.Handler interface. // ServeHTTP makes the router implement the http.Handler interface.
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
if r.PanicHandler != nil { if r.PanicHandler != nil {
defer r.recv(w, req) defer r.recv(w, req)
} }
if root := r.trees[req.Method]; root != nil { path := req.URL.Path
path := req.URL.Path
if root := r.trees[req.Method]; root != nil {
if handle, ps, tsr := root.getValue(path); handle != nil { if handle, ps, tsr := root.getValue(path); handle != nil {
handle(w, req, ps) handle(w, req, ps)
return return
@@ -331,18 +366,19 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
} }
} }
// Handle 405 if req.Method == "OPTIONS" && r.HandleOPTIONS {
if r.HandleMethodNotAllowed { // Handle OPTIONS requests
for method := range r.trees { if allow := r.allowed(path, req.Method); len(allow) > 0 {
// Skip the requested method - we already tried this one w.Header().Set("Allow", allow)
if method == req.Method { return
continue }
} } else {
// Handle 405
handle, _, _ := r.trees[method].getValue(req.URL.Path) if r.HandleMethodNotAllowed {
if handle != nil { if allow := r.allowed(path, req.Method); len(allow) > 0 {
w.Header().Set("Allow", allow)
if r.MethodNotAllowed != nil { if r.MethodNotAllowed != nil {
r.MethodNotAllowed(w, req) r.MethodNotAllowed.ServeHTTP(w, req)
} else { } else {
http.Error(w, http.Error(w,
http.StatusText(http.StatusMethodNotAllowed), http.StatusText(http.StatusMethodNotAllowed),
@@ -356,7 +392,7 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// Handle 404 // Handle 404
if r.NotFound != nil { if r.NotFound != nil {
r.NotFound(w, req) r.NotFound.ServeHTTP(w, req)
} else { } else {
http.NotFound(w, req) http.NotFound(w, req)
} }
+173 -21
View File
@@ -68,11 +68,11 @@ func TestRouter(t *testing.T) {
} }
type handlerStruct struct { type handlerStruct struct {
handeled *bool handled *bool
} }
func (h handlerStruct) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (h handlerStruct) ServeHTTP(w http.ResponseWriter, r *http.Request) {
*h.handeled = true *h.handled = true
} }
func TestRouterAPI(t *testing.T) { func TestRouterAPI(t *testing.T) {
@@ -174,26 +174,175 @@ func TestRouterRoot(t *testing.T) {
} }
} }
func TestRouterChaining(t *testing.T) {
router1 := New()
router2 := New()
router1.NotFound = router2
fooHit := false
router1.POST("/foo", func(w http.ResponseWriter, req *http.Request, _ Params) {
fooHit = true
w.WriteHeader(http.StatusOK)
})
barHit := false
router2.POST("/bar", func(w http.ResponseWriter, req *http.Request, _ Params) {
barHit = true
w.WriteHeader(http.StatusOK)
})
r, _ := http.NewRequest("POST", "/foo", nil)
w := httptest.NewRecorder()
router1.ServeHTTP(w, r)
if !(w.Code == http.StatusOK && fooHit) {
t.Errorf("Regular routing failed with router chaining.")
t.FailNow()
}
r, _ = http.NewRequest("POST", "/bar", nil)
w = httptest.NewRecorder()
router1.ServeHTTP(w, r)
if !(w.Code == http.StatusOK && barHit) {
t.Errorf("Chained routing failed with router chaining.")
t.FailNow()
}
r, _ = http.NewRequest("POST", "/qax", nil)
w = httptest.NewRecorder()
router1.ServeHTTP(w, r)
if !(w.Code == http.StatusNotFound) {
t.Errorf("NotFound behavior failed with router chaining.")
t.FailNow()
}
}
func TestRouterOPTIONS(t *testing.T) {
handlerFunc := func(_ http.ResponseWriter, _ *http.Request, _ Params) {}
router := New()
router.POST("/path", handlerFunc)
// test not allowed
// * (server)
r, _ := http.NewRequest("OPTIONS", "*", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, r)
if !(w.Code == http.StatusOK) {
t.Errorf("OPTIONS handling failed: Code=%d, Header=%v", w.Code, w.Header())
} else if allow := w.Header().Get("Allow"); allow != "POST, OPTIONS" {
t.Error("unexpected Allow header value: " + allow)
}
// path
r, _ = http.NewRequest("OPTIONS", "/path", nil)
w = httptest.NewRecorder()
router.ServeHTTP(w, r)
if !(w.Code == http.StatusOK) {
t.Errorf("OPTIONS handling failed: Code=%d, Header=%v", w.Code, w.Header())
} else if allow := w.Header().Get("Allow"); allow != "POST, OPTIONS" {
t.Error("unexpected Allow header value: " + allow)
}
r, _ = http.NewRequest("OPTIONS", "/doesnotexist", nil)
w = httptest.NewRecorder()
router.ServeHTTP(w, r)
if !(w.Code == http.StatusNotFound) {
t.Errorf("OPTIONS handling failed: Code=%d, Header=%v", w.Code, w.Header())
}
// add another method
router.GET("/path", handlerFunc)
// test again
// * (server)
r, _ = http.NewRequest("OPTIONS", "*", nil)
w = httptest.NewRecorder()
router.ServeHTTP(w, r)
if !(w.Code == http.StatusOK) {
t.Errorf("OPTIONS handling failed: Code=%d, Header=%v", w.Code, w.Header())
} else if allow := w.Header().Get("Allow"); allow != "POST, GET, OPTIONS" && allow != "GET, POST, OPTIONS" {
t.Error("unexpected Allow header value: " + allow)
}
// path
r, _ = http.NewRequest("OPTIONS", "/path", nil)
w = httptest.NewRecorder()
router.ServeHTTP(w, r)
if !(w.Code == http.StatusOK) {
t.Errorf("OPTIONS handling failed: Code=%d, Header=%v", w.Code, w.Header())
} else if allow := w.Header().Get("Allow"); allow != "POST, GET, OPTIONS" && allow != "GET, POST, OPTIONS" {
t.Error("unexpected Allow header value: " + allow)
}
// custom handler
var custom bool
router.OPTIONS("/path", func(w http.ResponseWriter, r *http.Request, _ Params) {
custom = true
})
// test again
// * (server)
r, _ = http.NewRequest("OPTIONS", "*", nil)
w = httptest.NewRecorder()
router.ServeHTTP(w, r)
if !(w.Code == http.StatusOK) {
t.Errorf("OPTIONS handling failed: Code=%d, Header=%v", w.Code, w.Header())
} else if allow := w.Header().Get("Allow"); allow != "POST, GET, OPTIONS" && allow != "GET, POST, OPTIONS" {
t.Error("unexpected Allow header value: " + allow)
}
if custom {
t.Error("custom handler called on *")
}
// path
r, _ = http.NewRequest("OPTIONS", "/path", nil)
w = httptest.NewRecorder()
router.ServeHTTP(w, r)
if !(w.Code == http.StatusOK) {
t.Errorf("OPTIONS handling failed: Code=%d, Header=%v", w.Code, w.Header())
}
if !custom {
t.Error("custom handler not called")
}
}
func TestRouterNotAllowed(t *testing.T) { func TestRouterNotAllowed(t *testing.T) {
handlerFunc := func(_ http.ResponseWriter, _ *http.Request, _ Params) {} handlerFunc := func(_ http.ResponseWriter, _ *http.Request, _ Params) {}
router := New() router := New()
router.POST("/path", handlerFunc) router.POST("/path", handlerFunc)
// Test not allowed // test not allowed
r, _ := http.NewRequest("GET", "/path", nil) r, _ := http.NewRequest("GET", "/path", nil)
w := httptest.NewRecorder() w := httptest.NewRecorder()
router.ServeHTTP(w, r) router.ServeHTTP(w, r)
if !(w.Code == http.StatusMethodNotAllowed) { if !(w.Code == http.StatusMethodNotAllowed) {
t.Errorf("NotAllowed handling failed: Code=%d, Header=%v", w.Code, w.Header()) t.Errorf("NotAllowed handling failed: Code=%d, Header=%v", w.Code, w.Header())
} else if allow := w.Header().Get("Allow"); allow != "POST, OPTIONS" {
t.Error("unexpected Allow header value: " + allow)
} }
// add another method
router.DELETE("/path", handlerFunc)
router.OPTIONS("/path", handlerFunc) // must be ignored
// test again
r, _ = http.NewRequest("GET", "/path", nil)
w = httptest.NewRecorder()
router.ServeHTTP(w, r)
if !(w.Code == http.StatusMethodNotAllowed) {
t.Errorf("NotAllowed handling failed: Code=%d, Header=%v", w.Code, w.Header())
} else if allow := w.Header().Get("Allow"); allow != "POST, DELETE, OPTIONS" && allow != "DELETE, POST, OPTIONS" {
t.Error("unexpected Allow header value: " + allow)
}
// test custom handler
w = httptest.NewRecorder() w = httptest.NewRecorder()
responseText := "custom method" responseText := "custom method"
router.MethodNotAllowed = func(w http.ResponseWriter, req *http.Request) { router.MethodNotAllowed = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
w.WriteHeader(http.StatusTeapot) w.WriteHeader(http.StatusTeapot)
w.Write([]byte(responseText)) w.Write([]byte(responseText))
} })
router.ServeHTTP(w, r) router.ServeHTTP(w, r)
if got := w.Body.String(); !(got == responseText) { if got := w.Body.String(); !(got == responseText) {
t.Errorf("unexpected response got %q want %q", got, responseText) t.Errorf("unexpected response got %q want %q", got, responseText)
@@ -201,6 +350,9 @@ func TestRouterNotAllowed(t *testing.T) {
if w.Code != http.StatusTeapot { if w.Code != http.StatusTeapot {
t.Errorf("unexpected response code %d want %d", w.Code, http.StatusTeapot) t.Errorf("unexpected response code %d want %d", w.Code, http.StatusTeapot)
} }
if allow := w.Header().Get("Allow"); allow != "POST, DELETE, OPTIONS" && allow != "DELETE, POST, OPTIONS" {
t.Error("unexpected Allow header value: " + allow)
}
} }
func TestRouterNotFound(t *testing.T) { func TestRouterNotFound(t *testing.T) {
@@ -212,35 +364,35 @@ func TestRouterNotFound(t *testing.T) {
router.GET("/", handlerFunc) router.GET("/", handlerFunc)
testRoutes := []struct { testRoutes := []struct {
route string route string
code int code int
header string location string
}{ }{
{"/path/", 301, "map[Location:[/path]]"}, // TSR -/ {"/path/", 301, "/path"}, // TSR -/
{"/dir", 301, "map[Location:[/dir/]]"}, // TSR +/ {"/dir", 301, "/dir/"}, // TSR +/
{"", 301, "map[Location:[/]]"}, // TSR +/ {"", 301, "/"}, // TSR +/
{"/PATH", 301, "map[Location:[/path]]"}, // Fixed Case {"/PATH", 301, "/path"}, // Fixed Case
{"/DIR/", 301, "map[Location:[/dir/]]"}, // Fixed Case {"/DIR/", 301, "/dir/"}, // Fixed Case
{"/PATH/", 301, "map[Location:[/path]]"}, // Fixed Case -/ {"/PATH/", 301, "/path"}, // Fixed Case -/
{"/DIR", 301, "map[Location:[/dir/]]"}, // Fixed Case +/ {"/DIR", 301, "/dir/"}, // Fixed Case +/
{"/../path", 301, "map[Location:[/path]]"}, // CleanPath {"/../path", 301, "/path"}, // CleanPath
{"/nope", 404, ""}, // NotFound {"/nope", 404, ""}, // NotFound
} }
for _, tr := range testRoutes { for _, tr := range testRoutes {
r, _ := http.NewRequest("GET", tr.route, nil) r, _ := http.NewRequest("GET", tr.route, nil)
w := httptest.NewRecorder() w := httptest.NewRecorder()
router.ServeHTTP(w, r) router.ServeHTTP(w, r)
if !(w.Code == tr.code && (w.Code == 404 || fmt.Sprint(w.Header()) == tr.header)) { if !(w.Code == tr.code && (w.Code == 404 || fmt.Sprint(w.Header().Get("Location")) == tr.location)) {
t.Errorf("NotFound handling route %s failed: Code=%d, Header=%v", tr.route, w.Code, w.Header()) t.Errorf("NotFound handling route %s failed: Code=%d, Header=%v", tr.route, w.Code, w.Header().Get("Location"))
} }
} }
// Test custom not found handler // Test custom not found handler
var notFound bool var notFound bool
router.NotFound = func(rw http.ResponseWriter, r *http.Request) { router.NotFound = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
rw.WriteHeader(404) rw.WriteHeader(404)
notFound = true notFound = true
} })
r, _ := http.NewRequest("GET", "/nope", nil) r, _ := http.NewRequest("GET", "/nope", nil)
w := httptest.NewRecorder() w := httptest.NewRecorder()
router.ServeHTTP(w, r) router.ServeHTTP(w, r)
+141 -40
View File
@@ -7,6 +7,7 @@ package httprouter
import ( import (
"strings" "strings"
"unicode" "unicode"
"unicode/utf8"
) )
func min(a, b int) int { func min(a, b int) int {
@@ -33,9 +34,10 @@ func countParams(path string) uint8 {
type nodeType uint8 type nodeType uint8
const ( const (
static nodeType = 0 static nodeType = iota // default
param nodeType = 1 root
catchAll nodeType = 2 param
catchAll
) )
type node struct { type node struct {
@@ -58,9 +60,7 @@ func (n *node) incrementChildPrio(pos int) int {
newPos := pos newPos := pos
for newPos > 0 && n.children[newPos-1].priority < prio { for newPos > 0 && n.children[newPos-1].priority < prio {
// swap node positions // swap node positions
tmpN := n.children[newPos-1] n.children[newPos-1], n.children[newPos] = n.children[newPos], n.children[newPos-1]
n.children[newPos-1] = n.children[newPos]
n.children[newPos] = tmpN
newPos-- newPos--
} }
@@ -105,6 +105,7 @@ func (n *node) addRoute(path string, handle Handle) {
child := node{ child := node{
path: n.path[i:], path: n.path[i:],
wildChild: n.wildChild, wildChild: n.wildChild,
nType: static,
indices: n.indices, indices: n.indices,
children: n.children, children: n.children,
handle: n.handle, handle: n.handle,
@@ -141,16 +142,25 @@ func (n *node) addRoute(path string, handle Handle) {
numParams-- numParams--
// Check if the wildcard matches // Check if the wildcard matches
if len(path) >= len(n.path) && n.path == path[:len(n.path)] { if len(path) >= len(n.path) && n.path == path[:len(n.path)] &&
// check for longer wildcard, e.g. :name and :names // Check for longer wildcard, e.g. :name and :names
if len(n.path) >= len(path) || path[len(n.path)] == '/' { (len(n.path) >= len(path) || path[len(n.path)] == '/') {
continue walk continue walk
} else {
// Wildcard conflict
var pathSeg string
if n.nType == catchAll {
pathSeg = path
} else {
pathSeg = strings.SplitN(path, "/", 2)[0]
} }
prefix := fullPath[:strings.Index(fullPath, pathSeg)] + n.path
panic("'" + pathSeg +
"' in new path '" + fullPath +
"' conflicts with existing wildcard '" + n.path +
"' in existing prefix '" + prefix +
"'")
} }
panic("path segment '" + path +
"' conflicts with existing wildcard '" + n.path +
"' in path '" + fullPath + "'")
} }
c := path[0] c := path[0]
@@ -187,7 +197,7 @@ func (n *node) addRoute(path string, handle Handle) {
} else if i == len(path) { // Make node a (in-path) leaf } else if i == len(path) { // Make node a (in-path) leaf
if n.handle != nil { if n.handle != nil {
panic("a handle is already registered for path ''" + fullPath + "'") panic("a handle is already registered for path '" + fullPath + "'")
} }
n.handle = handle n.handle = handle
} }
@@ -195,6 +205,7 @@ func (n *node) addRoute(path string, handle Handle) {
} }
} else { // Empty tree } else { // Empty tree
n.insertChild(numParams, path, fullPath, handle) n.insertChild(numParams, path, fullPath, handle)
n.nType = root
} }
} }
@@ -317,7 +328,7 @@ func (n *node) insertChild(numParams uint8, path, fullPath string, handle Handle
// made if a handle exists with an extra (without the) trailing slash for the // made if a handle exists with an extra (without the) trailing slash for the
// given path. // given path.
func (n *node) getValue(path string) (handle Handle, p Params, tsr bool) { func (n *node) getValue(path string) (handle Handle, p Params, tsr bool) {
walk: // Outer loop for walking the tree walk: // outer loop for walking the tree
for { for {
if len(path) > len(n.path) { if len(path) > len(n.path) {
if path[:len(n.path)] == n.path { if path[:len(n.path)] == n.path {
@@ -411,6 +422,11 @@ walk: // Outer loop for walking the tree
return return
} }
if path == "/" && n.wildChild && n.nType != root {
tsr = true
return
}
// No handle found. Check if a handle for this path + a // No handle found. Check if a handle for this path + a
// trailing slash exists for trailing slash recommendation // trailing slash exists for trailing slash recommendation
for i := 0; i < len(n.indices); i++ { for i := 0; i < len(n.indices); i++ {
@@ -439,34 +455,117 @@ walk: // Outer loop for walking the tree
// It returns the case-corrected path and a bool indicating whether the lookup // It returns the case-corrected path and a bool indicating whether the lookup
// was successful. // was successful.
func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) (ciPath []byte, found bool) { func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) (ciPath []byte, found bool) {
ciPath = make([]byte, 0, len(path)+1) // preallocate enough memory return n.findCaseInsensitivePathRec(
path,
strings.ToLower(path),
make([]byte, 0, len(path)+1), // preallocate enough memory for new path
[4]byte{}, // empty rune buffer
fixTrailingSlash,
)
}
// Outer loop for walking the tree // shift bytes in array by n bytes left
for len(path) >= len(n.path) && strings.ToLower(path[:len(n.path)]) == strings.ToLower(n.path) { func shiftNRuneBytes(rb [4]byte, n int) [4]byte {
path = path[len(n.path):] switch n {
case 0:
return rb
case 1:
return [4]byte{rb[1], rb[2], rb[3], 0}
case 2:
return [4]byte{rb[2], rb[3]}
case 3:
return [4]byte{rb[3]}
default:
return [4]byte{}
}
}
// recursive case-insensitive lookup function used by n.findCaseInsensitivePath
func (n *node) findCaseInsensitivePathRec(path, loPath string, ciPath []byte, rb [4]byte, fixTrailingSlash bool) ([]byte, bool) {
loNPath := strings.ToLower(n.path)
walk: // outer loop for walking the tree
for len(loPath) >= len(loNPath) && (len(loNPath) == 0 || loPath[1:len(loNPath)] == loNPath[1:]) {
// add common path to result
ciPath = append(ciPath, n.path...) ciPath = append(ciPath, n.path...)
if len(path) > 0 { if path = path[len(n.path):]; len(path) > 0 {
loOld := loPath
loPath = loPath[len(loNPath):]
// If this node does not have a wildcard (param or catchAll) child, // If this node does not have a wildcard (param or catchAll) child,
// we can just look up the next child node and continue to walk down // we can just look up the next child node and continue to walk down
// the tree // the tree
if !n.wildChild { if !n.wildChild {
r := unicode.ToLower(rune(path[0])) // skip rune bytes already processed
for i, index := range n.indices { rb = shiftNRuneBytes(rb, len(loNPath))
// must use recursive approach since both index and
// ToLower(index) could exist. We must check both. if rb[0] != 0 {
if r == unicode.ToLower(index) { // old rune not finished
out, found := n.children[i].findCaseInsensitivePath(path, fixTrailingSlash) for i := 0; i < len(n.indices); i++ {
if found { if n.indices[i] == rb[0] {
return append(ciPath, out...), true // continue with child node
n = n.children[i]
loNPath = strings.ToLower(n.path)
continue walk
}
}
} else {
// process a new rune
var rv rune
// find rune start
// runes are up to 4 byte long,
// -4 would definitely be another rune
var off int
for max := min(len(loNPath), 3); off < max; off++ {
if i := len(loNPath) - off; utf8.RuneStart(loOld[i]) {
// read rune from cached lowercase path
rv, _ = utf8.DecodeRuneInString(loOld[i:])
break
}
}
// calculate lowercase bytes of current rune
utf8.EncodeRune(rb[:], rv)
// skipp already processed bytes
rb = shiftNRuneBytes(rb, off)
for i := 0; i < len(n.indices); i++ {
// lowercase matches
if n.indices[i] == rb[0] {
// must use a recursive approach since both the
// uppercase byte and the lowercase byte might exist
// as an index
if out, found := n.children[i].findCaseInsensitivePathRec(
path, loPath, ciPath, rb, fixTrailingSlash,
); found {
return out, true
}
break
}
}
// same for uppercase rune, if it differs
if up := unicode.ToUpper(rv); up != rv {
utf8.EncodeRune(rb[:], up)
rb = shiftNRuneBytes(rb, off)
for i := 0; i < len(n.indices); i++ {
// uppercase matches
if n.indices[i] == rb[0] {
// continue with child node
n = n.children[i]
loNPath = strings.ToLower(n.path)
continue walk
}
} }
} }
} }
// Nothing found. We can recommend to redirect to the same URL // Nothing found. We can recommend to redirect to the same URL
// without a trailing slash if a leaf exists for that path // without a trailing slash if a leaf exists for that path
found = (fixTrailingSlash && path == "/" && n.handle != nil) return ciPath, (fixTrailingSlash && path == "/" && n.handle != nil)
return
} }
n = n.children[0] n = n.children[0]
@@ -484,8 +583,11 @@ func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) (ciPa
// we need to go deeper! // we need to go deeper!
if k < len(path) { if k < len(path) {
if len(n.children) > 0 { if len(n.children) > 0 {
path = path[k:] // continue with child node
n = n.children[0] n = n.children[0]
loNPath = strings.ToLower(n.path)
loPath = loPath[k:]
path = path[k:]
continue continue
} }
@@ -493,7 +595,7 @@ func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) (ciPa
if fixTrailingSlash && len(path) == k+1 { if fixTrailingSlash && len(path) == k+1 {
return ciPath, true return ciPath, true
} }
return return ciPath, false
} }
if n.handle != nil { if n.handle != nil {
@@ -506,7 +608,7 @@ func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) (ciPa
return append(ciPath, '/'), true return append(ciPath, '/'), true
} }
} }
return return ciPath, false
case catchAll: case catchAll:
return append(ciPath, path...), true return append(ciPath, path...), true
@@ -531,11 +633,11 @@ func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) (ciPa
(n.nType == catchAll && n.children[0].handle != nil) { (n.nType == catchAll && n.children[0].handle != nil) {
return append(ciPath, '/'), true return append(ciPath, '/'), true
} }
return return ciPath, false
} }
} }
} }
return return ciPath, false
} }
} }
@@ -545,11 +647,10 @@ func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) (ciPa
if path == "/" { if path == "/" {
return ciPath, true return ciPath, true
} }
if len(path)+1 == len(n.path) && n.path[len(path)] == '/' && if len(loPath)+1 == len(loNPath) && loNPath[len(loPath)] == '/' &&
strings.ToLower(path) == strings.ToLower(n.path[:len(path)]) && loPath[1:] == loNPath[1:len(loPath)] && n.handle != nil {
n.handle != nil {
return append(ciPath, n.path...), true return append(ciPath, n.path...), true
} }
} }
return return ciPath, false
} }
+90 -2
View File
@@ -8,6 +8,7 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"reflect" "reflect"
"regexp"
"strings" "strings"
"testing" "testing"
) )
@@ -22,7 +23,7 @@ func printChildren(n *node, prefix string) {
} }
} }
// Used as a workaround since we can't compare functions or their adresses // Used as a workaround since we can't compare functions or their addresses
var fakeHandlerValue string var fakeHandlerValue string
func fakeHandler(val string) Handle { func fakeHandler(val string) Handle {
@@ -89,7 +90,7 @@ func checkMaxParams(t *testing.T, n *node) uint8 {
maxParams = params maxParams = params
} }
} }
if n.nType != static && !n.wildChild { if n.nType > root && !n.wildChild {
maxParams++ maxParams++
} }
@@ -394,6 +395,9 @@ func TestTreeTrailingSlashRedirect(t *testing.T) {
"/1/:id/2", "/1/:id/2",
"/aa", "/aa",
"/a/", "/a/",
"/admin",
"/admin/:category",
"/admin/:category/:page",
"/doc", "/doc",
"/doc/go_faq.html", "/doc/go_faq.html",
"/doc/go1.html", "/doc/go1.html",
@@ -423,6 +427,9 @@ func TestTreeTrailingSlashRedirect(t *testing.T) {
"/0/go/", "/0/go/",
"/1/go", "/1/go",
"/a", "/a",
"/admin/",
"/admin/config/",
"/admin/config/permissions/",
"/doc/", "/doc/",
} }
for _, route := range tsrRoutes { for _, route := range tsrRoutes {
@@ -452,6 +459,24 @@ func TestTreeTrailingSlashRedirect(t *testing.T) {
} }
} }
func TestTreeRootTrailingSlashRedirect(t *testing.T) {
tree := &node{}
recv := catchPanic(func() {
tree.addRoute("/:test", fakeHandler("/:test"))
})
if recv != nil {
t.Fatalf("panic inserting test route: %v", recv)
}
handler, _, tsr := tree.getValue("/")
if handler != nil {
t.Fatalf("non-nil handler")
} else if tsr {
t.Errorf("expected no TSR recommendation")
}
}
func TestTreeFindCaseInsensitivePath(t *testing.T) { func TestTreeFindCaseInsensitivePath(t *testing.T) {
tree := &node{} tree := &node{}
@@ -478,6 +503,16 @@ func TestTreeFindCaseInsensitivePath(t *testing.T) {
"/doc/go/away", "/doc/go/away",
"/no/a", "/no/a",
"/no/b", "/no/b",
"/Π",
"/u/apfêl/",
"/u/äpfêl/",
"/u/öpfêl",
"/v/Äpfêl/",
"/v/Öpfêl",
"/w/♬", // 3 byte
"/w/♭/", // 3 byte, last byte differs
"/w/𠜎", // 4 byte
"/w/𠜏/", // 4 byte
} }
for _, route := range routes { for _, route := range routes {
@@ -556,6 +591,20 @@ func TestTreeFindCaseInsensitivePath(t *testing.T) {
{"/DOC/", "/doc", true, true}, {"/DOC/", "/doc", true, true},
{"/NO", "", false, true}, {"/NO", "", false, true},
{"/DOC/GO", "", false, true}, {"/DOC/GO", "", false, true},
{"/π", "/Π", true, false},
{"/π/", "/Π", true, true},
{"/u/ÄPFÊL/", "/u/äpfêl/", true, false},
{"/u/ÄPFÊL", "/u/äpfêl/", true, true},
{"/u/ÖPFÊL/", "/u/öpfêl", true, true},
{"/u/ÖPFÊL", "/u/öpfêl", true, false},
{"/v/äpfêL/", "/v/Äpfêl/", true, false},
{"/v/äpfêL", "/v/Äpfêl/", true, true},
{"/v/öpfêL/", "/v/Öpfêl", true, true},
{"/v/öpfêL", "/v/Öpfêl", true, false},
{"/w/♬/", "/w/♬", true, true},
{"/w/♭", "/w/♭/", true, true},
{"/w/𠜎/", "/w/𠜎", true, true},
{"/w/𠜏", "/w/𠜏/", true, true},
} }
// With fixTrailingSlash = true // With fixTrailingSlash = true
for _, test := range tests { for _, test := range tests {
@@ -609,3 +658,42 @@ func TestTreeInvalidNodeType(t *testing.T) {
t.Fatalf("Expected panic '"+panicMsg+"', got '%v'", recv) t.Fatalf("Expected panic '"+panicMsg+"', got '%v'", recv)
} }
} }
func TestTreeWildcardConflictEx(t *testing.T) {
conflicts := [...]struct {
route string
segPath string
existPath string
existSegPath string
}{
{"/who/are/foo", "/foo", `/who/are/\*you`, `/\*you`},
{"/who/are/foo/", "/foo/", `/who/are/\*you`, `/\*you`},
{"/who/are/foo/bar", "/foo/bar", `/who/are/\*you`, `/\*you`},
{"/conxxx", "xxx", `/con:tact`, `:tact`},
{"/conooo/xxx", "ooo", `/con:tact`, `:tact`},
}
for _, conflict := range conflicts {
// I have to re-create a 'tree', because the 'tree' will be
// in an inconsistent state when the loop recovers from the
// panic which threw by 'addRoute' function.
tree := &node{}
routes := [...]string{
"/con:tact",
"/who/are/*you",
"/who/foo/hello",
}
for _, route := range routes {
tree.addRoute(route, fakeHandler(route))
}
recv := catchPanic(func() {
tree.addRoute(conflict.route, fakeHandler(conflict.route))
})
if !regexp.MustCompile(fmt.Sprintf("'%s' in new path .* conflicts with existing wildcard '%s' in existing prefix '%s'", conflict.segPath, conflict.existSegPath, conflict.existPath)).MatchString(fmt.Sprint(recv)) {
t.Fatalf("invalid wildcard conflict error (%v)", recv)
}
}
}
+1
View File
@@ -14,6 +14,7 @@ It offers slightly better compression at lower compression settings, and up to 3
# changelog # changelog
* Aug 1, 2018: Added [huff0 README](https://github.com/klauspost/compress/tree/master/huff0#huff0-entropy-compression).
* Jul 8, 2018: Added [Performance Update 2018](#performance-update-2018) below. * Jul 8, 2018: Added [Performance Update 2018](#performance-update-2018) below.
* Jun 23, 2018: Merged [Go 1.11 inflate optimizations](https://go-review.googlesource.com/c/go/+/102235). Go 1.9 is now required. Backwards compatible version tagged with [v1.3.0](https://github.com/klauspost/compress/releases/tag/v1.3.0). * Jun 23, 2018: Merged [Go 1.11 inflate optimizations](https://go-review.googlesource.com/c/go/+/102235). Go 1.9 is now required. Backwards compatible version tagged with [v1.3.0](https://github.com/klauspost/compress/releases/tag/v1.3.0).
* Apr 2, 2018: Added [huff0](https://godoc.org/github.com/klauspost/compress/huff0) en/decoder. Experimental for now, API may change. * Apr 2, 2018: Added [huff0](https://godoc.org/github.com/klauspost/compress/huff0) en/decoder. Experimental for now, API may change.
+1
View File
@@ -1,5 +1,6 @@
//+build !noasm //+build !noasm
//+build !appengine //+build !appengine
//+build !gccgo
// Copyright 2015, Klaus Post, see LICENSE for details. // Copyright 2015, Klaus Post, see LICENSE for details.
+1
View File
@@ -1,5 +1,6 @@
//+build !noasm //+build !noasm
//+build !appengine //+build !appengine
//+build !gccgo
// Copyright 2015, Klaus Post, see LICENSE for details. // Copyright 2015, Klaus Post, see LICENSE for details.
+1 -1
View File
@@ -1,4 +1,4 @@
//+build !amd64 noasm appengine //+build !amd64 noasm appengine gccgo
// Copyright 2015, Klaus Post, see LICENSE for details. // Copyright 2015, Klaus Post, see LICENSE for details.
+5
View File
@@ -14,6 +14,7 @@ package fse
import ( import (
"errors" "errors"
"fmt" "fmt"
"math/bits"
) )
const ( const (
@@ -136,3 +137,7 @@ func (s *Scratch) prepare(in []byte) (*Scratch, error) {
func tableStep(tableSize uint32) uint32 { func tableStep(tableSize uint32) uint32 {
return (tableSize >> 1) + (tableSize >> 3) + 3 return (tableSize >> 1) + (tableSize >> 3) + 3
} }
func highBits(val uint32) (n uint32) {
return uint32(bits.Len32(val) - 1)
}
-9
View File
@@ -1,9 +0,0 @@
// +build go1.9
package fse
import "math/bits"
func highBits(val uint32) (n uint32) {
return uint32(bits.Len32(val) - 1)
}
-18
View File
@@ -1,18 +0,0 @@
// +build !go1.9
package fse
var deBruijnClz = [...]uint8{0, 9, 1, 10, 13, 21, 2, 29,
11, 14, 16, 18, 22, 25, 3, 30,
8, 12, 20, 28, 15, 17, 24, 7,
19, 27, 23, 6, 26, 5, 4, 31}
// highBits returns the highest set bit
func highBits(v uint32) (n uint32) {
v |= v >> 1
v |= v >> 2
v |= v >> 4
v |= v >> 8
v |= v >> 16
return uint32(deBruijnClz[(v*0x07C4ACDD)>>27])
}
+87
View File
@@ -0,0 +1,87 @@
# Huff0 entropy compression
This package provides Huff0 encoding and decoding as used in zstd.
[Huff0](https://github.com/Cyan4973/FiniteStateEntropy#new-generation-entropy-coders),
a Huffman codec designed for modern CPU, featuring OoO (Out of Order) operations on multiple ALU
(Arithmetic Logic Unit), achieving extremely fast compression and decompression speeds.
This can be used for compressing input with a lot of similar input values to the smallest number of bytes.
This does not perform any multi-byte [dictionary coding](https://en.wikipedia.org/wiki/Dictionary_coder) as LZ coders,
but it can be used as a secondary step to compressors (like Snappy) that does not do entropy encoding.
* [Godoc documentation](https://godoc.org/github.com/klauspost/compress/huff0)
THIS PACKAGE IS NOT CONSIDERED STABLE AND API OR ENCODING MAY CHANGE IN THE FUTURE.
## News
* Mar 2018: First implementation released. Consider this beta software for now.
# Usage
This package provides a low level interface that allows to compress single independent blocks.
Each block is separate, and there is no built in integrity checks.
This means that the caller should keep track of block sizes and also do checksums if needed.
Compressing a block is done via the [`Compress1X`](https://godoc.org/github.com/klauspost/compress/huff0#Compress1X) and
[`Compress4X`](https://godoc.org/github.com/klauspost/compress/huff0#Compress4X) functions.
You must provide input and will receive the output and maybe an error.
These error values can be returned:
| Error | Description |
|---------------------|-----------------------------------------------------------------------------|
| `<nil>` | Everything ok, output is returned |
| `ErrIncompressible` | Returned when input is judged to be too hard to compress |
| `ErrUseRLE` | Returned from the compressor when the input is a single byte value repeated |
| `ErrTooBig` | Returned if the input block exceeds the maximum allowed size (128 Kib) |
| `(error)` | An internal error occurred. |
As can be seen above some of there are errors that will be returned even under normal operation so it is important to handle these.
To reduce allocations you can provide a [`Scratch`](https://godoc.org/github.com/klauspost/compress/huff0#Scratch) object
that can be re-used for successive calls. Both compression and decompression accepts a `Scratch` object, and the same
object can be used for both.
Be aware, that when re-using a `Scratch` object that the *output* buffer is also re-used, so if you are still using this
you must set the `Out` field in the scratch to nil. The same buffer is used for compression and decompression output.
The `Scratch` object will retain state that allows to re-use previous tables for encoding and decoding.
## Tables and re-use
Huff0 allows for reusing tables from the previous block to save space if that is expected to give better/faster results.
The Scratch object allows you to set a [`ReusePolicy`](https://godoc.org/github.com/klauspost/compress/huff0#ReusePolicy)
that controls this behaviour. See the documentation for details. This can be altered between each block.
Do however note that this information is *not* stored in the output block and it is up to the users of the package to
record whether [`ReadTable`](https://godoc.org/github.com/klauspost/compress/huff0#ReadTable) should be called,
based on the boolean reported back from the CompressXX call.
If you want to store the table separate from the data, you can access them as `OutData` and `OutTable` on the
[`Scratch`](https://godoc.org/github.com/klauspost/compress/huff0#Scratch) object.
## Decompressing
The first part of decoding is to initialize the decoding table through [`ReadTable`](https://godoc.org/github.com/klauspost/compress/huff0#ReadTable).
This will initialize the decoding tables.
You can supply the complete block to `ReadTable` and it will return the data part of the block
which can be given to the decompressor.
Decompressing is done by calling the [`Decompress1X`](https://godoc.org/github.com/klauspost/compress/huff0#Scratch.Decompress1X)
or [`Decompress4X`](https://godoc.org/github.com/klauspost/compress/huff0#Scratch.Decompress4X) function.
You must provide the output from the compression stage, at exactly the size you got back. If you receive an error back
your input was likely corrupted.
It is important to note that a successful decoding does *not* mean your output matches your original input.
There are no integrity checks, so relying on errors from the decompressor does not assure your data is valid.
# Contributing
Contributions are always welcome. Be aware that adding public functions will require good justification and breaking
changes will likely not be accepted. If in doubt open an issue before writing the PR.
+11 -1
View File
@@ -67,21 +67,25 @@ func (b *bitWriter) flush() {
v := b.nBits >> 3 v := b.nBits >> 3
switch v { switch v {
case 0: case 0:
return
case 1: case 1:
b.out = append(b.out, b.out = append(b.out,
byte(b.bitContainer), byte(b.bitContainer),
) )
b.bitContainer >>= 1 << 3
case 2: case 2:
b.out = append(b.out, b.out = append(b.out,
byte(b.bitContainer), byte(b.bitContainer),
byte(b.bitContainer>>8), byte(b.bitContainer>>8),
) )
b.bitContainer >>= 2 << 3
case 3: case 3:
b.out = append(b.out, b.out = append(b.out,
byte(b.bitContainer), byte(b.bitContainer),
byte(b.bitContainer>>8), byte(b.bitContainer>>8),
byte(b.bitContainer>>16), byte(b.bitContainer>>16),
) )
b.bitContainer >>= 3 << 3
case 4: case 4:
b.out = append(b.out, b.out = append(b.out,
byte(b.bitContainer), byte(b.bitContainer),
@@ -89,6 +93,7 @@ func (b *bitWriter) flush() {
byte(b.bitContainer>>16), byte(b.bitContainer>>16),
byte(b.bitContainer>>24), byte(b.bitContainer>>24),
) )
b.bitContainer >>= 4 << 3
case 5: case 5:
b.out = append(b.out, b.out = append(b.out,
byte(b.bitContainer), byte(b.bitContainer),
@@ -97,6 +102,7 @@ func (b *bitWriter) flush() {
byte(b.bitContainer>>24), byte(b.bitContainer>>24),
byte(b.bitContainer>>32), byte(b.bitContainer>>32),
) )
b.bitContainer >>= 5 << 3
case 6: case 6:
b.out = append(b.out, b.out = append(b.out,
byte(b.bitContainer), byte(b.bitContainer),
@@ -106,6 +112,7 @@ func (b *bitWriter) flush() {
byte(b.bitContainer>>32), byte(b.bitContainer>>32),
byte(b.bitContainer>>40), byte(b.bitContainer>>40),
) )
b.bitContainer >>= 6 << 3
case 7: case 7:
b.out = append(b.out, b.out = append(b.out,
byte(b.bitContainer), byte(b.bitContainer),
@@ -116,6 +123,7 @@ func (b *bitWriter) flush() {
byte(b.bitContainer>>40), byte(b.bitContainer>>40),
byte(b.bitContainer>>48), byte(b.bitContainer>>48),
) )
b.bitContainer >>= 7 << 3
case 8: case 8:
b.out = append(b.out, b.out = append(b.out,
byte(b.bitContainer), byte(b.bitContainer),
@@ -127,10 +135,12 @@ func (b *bitWriter) flush() {
byte(b.bitContainer>>48), byte(b.bitContainer>>48),
byte(b.bitContainer>>56), byte(b.bitContainer>>56),
) )
b.bitContainer = 0
b.nBits = 0
return
default: default:
panic(fmt.Errorf("bits (%d) > 64", b.nBits)) panic(fmt.Errorf("bits (%d) > 64", b.nBits))
} }
b.bitContainer >>= v << 3
b.nBits &= 7 b.nBits &= 7
} }
+12 -4
View File
@@ -6,6 +6,10 @@ import (
"sync" "sync"
) )
// Compress1X will compress the input.
// The output can be decoded using Decompress1X.
// Supply a Scratch object. The scratch object contains state about re-use,
// So when sharing across independent encodes, be sure to set the re-use policy.
func Compress1X(in []byte, s *Scratch) (out []byte, reUsed bool, err error) { func Compress1X(in []byte, s *Scratch) (out []byte, reUsed bool, err error) {
s, err = s.prepare(in) s, err = s.prepare(in)
if err != nil { if err != nil {
@@ -14,6 +18,11 @@ func Compress1X(in []byte, s *Scratch) (out []byte, reUsed bool, err error) {
return compress(in, s, s.compress1X) return compress(in, s, s.compress1X)
} }
// Compress4X will compress the input. The input is split into 4 independent blocks
// and compressed similar to Compress1X.
// The output can be decoded using Decompress4X.
// Supply a Scratch object. The scratch object contains state about re-use,
// So when sharing across independent encodes, be sure to set the re-use policy.
func Compress4X(in []byte, s *Scratch) (out []byte, reUsed bool, err error) { func Compress4X(in []byte, s *Scratch) (out []byte, reUsed bool, err error) {
s, err = s.prepare(in) s, err = s.prepare(in)
if err != nil { if err != nil {
@@ -80,8 +89,7 @@ func compress(in []byte, s *Scratch, compressor func(src []byte) ([]byte, error)
return nil, false, err return nil, false, err
} }
if !s.canUseTable(s.cTable) { if false && !s.canUseTable(s.cTable) {
// TODO: Not needed.
panic("invalid table generated") panic("invalid table generated")
} }
@@ -512,7 +520,7 @@ func (s *Scratch) setMaxHeight(lastNonNull int) uint8 {
// renorm totalCost // renorm totalCost
totalCost >>= largestBits - maxNbBits /* note : totalCost is necessarily a multiple of baseCost */ totalCost >>= largestBits - maxNbBits /* note : totalCost is necessarily a multiple of baseCost */
/* repay normalized cost */ // repay normalized cost
{ {
const noSymbol = 0xF0F0F0F0 const noSymbol = 0xF0F0F0F0
var rankLast [tableLogMax + 2]uint32 var rankLast [tableLogMax + 2]uint32
@@ -528,7 +536,7 @@ func (s *Scratch) setMaxHeight(lastNonNull int) uint8 {
if huffNode[pos].nbBits >= currentNbBits { if huffNode[pos].nbBits >= currentNbBits {
continue continue
} }
currentNbBits = huffNode[pos].nbBits /* < maxNbBits */ currentNbBits = huffNode[pos].nbBits // < maxNbBits
rankLast[maxNbBits-currentNbBits] = uint32(pos) rankLast[maxNbBits-currentNbBits] = uint32(pos)
} }
} }
+7 -5
View File
@@ -29,7 +29,7 @@ type dEntryDouble struct {
// ReadTable will read a table from the input. // ReadTable will read a table from the input.
// The size of the input may be larger than the table definition. // The size of the input may be larger than the table definition.
// Any content remaining after the table definition will be returned. // Any content remaining after the table definition will be returned.
// if no Scratch is provided a new one is allocated. // If no Scratch is provided a new one is allocated.
// The returned Scratch can be used for decoding input using this table. // The returned Scratch can be used for decoding input using this table.
func ReadTable(in []byte, s *Scratch) (s2 *Scratch, remain []byte, err error) { func ReadTable(in []byte, s *Scratch) (s2 *Scratch, remain []byte, err error) {
s, err = s.prepare(in) s, err = s.prepare(in)
@@ -150,8 +150,9 @@ func ReadTable(in []byte, s *Scratch) (s2 *Scratch, remain []byte, err error) {
} }
// Decompress1X will decompress a 1X encoded stream. // Decompress1X will decompress a 1X encoded stream.
// The supplied input must match the end of a block exactly. // The length of the supplied input must match the end of a block exactly.
// Before this is called, the table must be initialized with ReadTable. // Before this is called, the table must be initialized with ReadTable unless
// the encoder re-used the table.
func (s *Scratch) Decompress1X(in []byte) (out []byte, err error) { func (s *Scratch) Decompress1X(in []byte) (out []byte, err error) {
if len(s.dt.single) == 0 { if len(s.dt.single) == 0 {
return nil, errors.New("no table loaded") return nil, errors.New("no table loaded")
@@ -206,8 +207,9 @@ func (s *Scratch) Decompress1X(in []byte) (out []byte, err error) {
} }
// Decompress4X will decompress a 4X encoded stream. // Decompress4X will decompress a 4X encoded stream.
// Before this is called, the table must be initialized with ReadTable. // Before this is called, the table must be initialized with ReadTable unless
// The supplied input must match the end of a block exactly. // the encoder re-used the table.
// The length of the supplied input must match the end of a block exactly.
// The destination size of the uncompressed data must be known and provided. // The destination size of the uncompressed data must be known and provided.
func (s *Scratch) Decompress4X(in []byte, dstSize int) (out []byte, err error) { func (s *Scratch) Decompress4X(in []byte, dstSize int) (out []byte, err error) {
if len(s.dt.single) == 0 { if len(s.dt.single) == 0 {
-9
View File
@@ -1,9 +0,0 @@
// +build go1.9
package huff0
import "math/bits"
func highBit32(val uint32) (n uint32) {
return uint32(bits.Len32(val) - 1)
}
-18
View File
@@ -1,18 +0,0 @@
// +build !go1.9
package huff0
var deBruijnClz = [...]uint8{0, 9, 1, 10, 13, 21, 2, 29,
11, 14, 16, 18, 22, 25, 3, 30,
8, 12, 20, 28, 15, 17, 24, 7,
19, 27, 23, 6, 26, 5, 4, 31}
// highBit32 returns the highest set bit
func highBit32(v uint32) (n uint32) {
v |= v >> 1
v |= v >> 2
v |= v >> 4
v |= v >> 8
v |= v >> 16
return uint32(deBruijnClz[(v*0x07C4ACDD)>>27])
}
+8
View File
@@ -1,9 +1,13 @@
// Package huff0 provides fast huffman encoding as used in zstd.
//
// See README.md at https://github.com/klauspost/compress/tree/master/huff0 for details.
package huff0 package huff0
import ( import (
"errors" "errors"
"fmt" "fmt"
"math" "math"
"math/bits"
"github.com/klauspost/compress/fse" "github.com/klauspost/compress/fse"
) )
@@ -230,3 +234,7 @@ func (s *Scratch) minSize(total int) int {
} }
return int(nbBits) >> 3 return int(nbBits) >> 3
} }
func highBit32(val uint32) (n uint32) {
return uint32(bits.Len32(val) - 1)
}
+2 -3
View File
@@ -6,10 +6,9 @@ os:
- linux - linux
- osx - osx
go: go:
- 1.5.x
- 1.6.x
- 1.7.x
- 1.8.x - 1.8.x
- 1.9.x
- 1.10.x
- master - master
script: script:
+35
View File
@@ -0,0 +1,35 @@
Developer Certificate of Origin
Version 1.1
Copyright (C) 2015- Klaus Post & Contributors.
Email: klauspost@gmail.com
Everyone is permitted to copy and distribute verbatim copies of this
license document, but changing it is not allowed.
Developer's Certificate of Origin 1.1
By making a contribution to this project, I certify that:
(a) The contribution was created in whole or in part by me and I
have the right to submit it under the open source license
indicated in the file; or
(b) The contribution is based upon previous work that, to the best
of my knowledge, is covered under an appropriate open source
license and I have the right under that license to submit that
work with modifications, whether created in whole or in part
by me, under the same open source license (unless I am
permitted to submit under a different license), as indicated
in the file; or
(c) The contribution was provided directly to me by some other
person who certified (a), (b) or (c) and I have not modified
it.
(d) I understand and agree that this project and the contribution
are public and that a record of the contribution (including all
personal information I submit with it, including my sign-off) is
maintained indefinitely and may be redistributed consistent with
this project or the open source license(s) involved.
+12 -2
View File
@@ -76,6 +76,8 @@ const (
RDTSCP // RDTSCP Instruction RDTSCP // RDTSCP Instruction
CX16 // CMPXCHG16B Instruction CX16 // CMPXCHG16B Instruction
SGX // Software Guard Extensions SGX // Software Guard Extensions
IBPB // Indirect Branch Restricted Speculation (IBRS) and Indirect Branch Predictor Barrier (IBPB)
STIBP // Single Thread Indirect Branch Predictors
// Performance indicators // Performance indicators
SSE2SLOW // SSE2 is supported, but usually not faster SSE2SLOW // SSE2 is supported, but usually not faster
@@ -131,6 +133,8 @@ var flagNames = map[Flags]string{
RDTSCP: "RDTSCP", // RDTSCP Instruction RDTSCP: "RDTSCP", // RDTSCP Instruction
CX16: "CX16", // CMPXCHG16B Instruction CX16: "CX16", // CMPXCHG16B Instruction
SGX: "SGX", // Software Guard Extensions SGX: "SGX", // Software Guard Extensions
IBPB: "IBPB", // Indirect Branch Restricted Speculation and Indirect Branch Predictor Barrier
STIBP: "STIBP", // Single Thread Indirect Branch Predictors
// Performance indicators // Performance indicators
SSE2SLOW: "SSE2SLOW", // SSE2 supported, but usually not faster SSE2SLOW: "SSE2SLOW", // SSE2 supported, but usually not faster
@@ -450,7 +454,7 @@ func (c CPUInfo) CX16() bool {
// TSX is split into HLE (Hardware Lock Elision) and RTM (Restricted Transactional Memory) detection. // TSX is split into HLE (Hardware Lock Elision) and RTM (Restricted Transactional Memory) detection.
// So TSX simply checks that. // So TSX simply checks that.
func (c CPUInfo) TSX() bool { func (c CPUInfo) TSX() bool {
return c.Features&(MPX|RTM) == MPX|RTM return c.Features&(HLE|RTM) == HLE|RTM
} }
// Atom indicates an Atom processor // Atom indicates an Atom processor
@@ -854,7 +858,7 @@ func support() Flags {
// Check AVX2, AVX2 requires OS support, but BMI1/2 don't. // Check AVX2, AVX2 requires OS support, but BMI1/2 don't.
if mfi >= 7 { if mfi >= 7 {
_, ebx, ecx, _ := cpuidex(7, 0) _, ebx, ecx, edx := cpuidex(7, 0)
if (rval&AVX) != 0 && (ebx&0x00000020) != 0 { if (rval&AVX) != 0 && (ebx&0x00000020) != 0 {
rval |= AVX2 rval |= AVX2
} }
@@ -888,6 +892,12 @@ func support() Flags {
if ebx&(1<<29) != 0 { if ebx&(1<<29) != 0 {
rval |= SHA rval |= SHA
} }
if edx&(1<<26) != 0 {
rval |= IBPB
}
if edx&(1<<27) != 0 {
rval |= STIBP
}
// Only detect AVX-512 features if XGETBV is supported // Only detect AVX-512 features if XGETBV is supported
if c&((1<<26)|(1<<27)) == (1<<26)|(1<<27) { if c&((1<<26)|(1<<27)) == (1<<26)|(1<<27) {
+11 -1
View File
@@ -70,6 +70,8 @@ const (
rdtscp // RDTSCP Instruction rdtscp // RDTSCP Instruction
cx16 // CMPXCHG16B Instruction cx16 // CMPXCHG16B Instruction
sgx // Software Guard Extensions sgx // Software Guard Extensions
ibpb // Indirect Branch Restricted Speculation (IBRS) and Indirect Branch Predictor Barrier (IBPB)
stibp // Single Thread Indirect Branch Predictors
// Performance indicators // Performance indicators
sse2slow // SSE2 is supported, but usually not faster sse2slow // SSE2 is supported, but usually not faster
@@ -125,6 +127,8 @@ var flagNames = map[flags]string{
rdtscp: "RDTSCP", // RDTSCP Instruction rdtscp: "RDTSCP", // RDTSCP Instruction
cx16: "CX16", // CMPXCHG16B Instruction cx16: "CX16", // CMPXCHG16B Instruction
sgx: "SGX", // Software Guard Extensions sgx: "SGX", // Software Guard Extensions
ibpb: "IBPB", // Indirect Branch Restricted Speculation and Indirect Branch Predictor Barrier
stibp: "STIBP", // Single Thread Indirect Branch Predictors
// Performance indicators // Performance indicators
sse2slow: "SSE2SLOW", // SSE2 supported, but usually not faster sse2slow: "SSE2SLOW", // SSE2 supported, but usually not faster
@@ -848,7 +852,7 @@ func support() flags {
// Check AVX2, AVX2 requires OS support, but BMI1/2 don't. // Check AVX2, AVX2 requires OS support, but BMI1/2 don't.
if mfi >= 7 { if mfi >= 7 {
_, ebx, ecx, _ := cpuidex(7, 0) _, ebx, ecx, edx := cpuidex(7, 0)
if (rval&avx) != 0 && (ebx&0x00000020) != 0 { if (rval&avx) != 0 && (ebx&0x00000020) != 0 {
rval |= avx2 rval |= avx2
} }
@@ -882,6 +886,12 @@ func support() flags {
if ebx&(1<<29) != 0 { if ebx&(1<<29) != 0 {
rval |= sha rval |= sha
} }
if edx&(1<<26) != 0 {
rval |= ibpb
}
if edx&(1<<27) != 0 {
rval |= stibp
}
// Only detect AVX-512 features if XGETBV is supported // Only detect AVX-512 features if XGETBV is supported
if c&((1<<26)|(1<<27)) == (1<<26)|(1<<27) { if c&((1<<26)|(1<<27)) == (1<<26)|(1<<27) {
Binary file not shown.
+9
View File
@@ -0,0 +1,9 @@
(The MIT License)
Copyright (c) 2017 marvin + konsorten GmbH (open-source@konsorten.de)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+40
View File
@@ -0,0 +1,40 @@
# Windows Terminal Sequences
This library allow for enabling Windows terminal color support for Go.
See [Console Virtual Terminal Sequences](https://docs.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences) for details.
## Usage
```go
import (
"syscall"
sequences "github.com/konsorten/go-windows-terminal-sequences"
)
func main() {
sequences.EnableVirtualTerminalProcessing(syscall.Stdout, true)
}
```
## Authors
The tool is sponsored by the [marvin + konsorten GmbH](http://www.konsorten.de).
We thank all the authors who provided code to this library:
* Felix Kollmann
## License
(The MIT License)
Copyright (c) 2018 marvin + konsorten GmbH (open-source@konsorten.de)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+1
View File
@@ -0,0 +1 @@
module github.com/konsorten/go-windows-terminal-sequences
+36
View File
@@ -0,0 +1,36 @@
// +build windows
package sequences
import (
"syscall"
"unsafe"
)
var (
kernel32Dll *syscall.LazyDLL = syscall.NewLazyDLL("Kernel32.dll")
setConsoleMode *syscall.LazyProc = kernel32Dll.NewProc("SetConsoleMode")
)
func EnableVirtualTerminalProcessing(stream syscall.Handle, enable bool) error {
const ENABLE_VIRTUAL_TERMINAL_PROCESSING uint32 = 0x4
var mode uint32
err := syscall.GetConsoleMode(syscall.Stdout, &mode)
if err != nil {
return err
}
if enable {
mode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING
} else {
mode &^= ENABLE_VIRTUAL_TERMINAL_PROCESSING
}
ret, _, err := setConsoleMode.Call(uintptr(unsafe.Pointer(stream)), uintptr(mode))
if ret == 0 {
return err
}
return nil
}
@@ -0,0 +1,48 @@
// +build windows
package sequences
import (
"fmt"
"os"
"syscall"
"testing"
)
func TestStdoutSequencesOn(t *testing.T) {
err := EnableVirtualTerminalProcessing(syscall.Stdout, true)
if err != nil {
t.Fatalf("Failed to enable VTP: %v", err)
}
defer EnableVirtualTerminalProcessing(syscall.Stdout, false)
fmt.Fprintf(os.Stdout, "\x1b[34mHello \x1b[35mWorld\x1b[0m!\n")
}
func TestStdoutSequencesOff(t *testing.T) {
err := EnableVirtualTerminalProcessing(syscall.Stdout, false)
if err != nil {
t.Fatalf("Failed to enable VTP: %v", err)
}
fmt.Fprintf(os.Stdout, "\x1b[34mHello \x1b[35mWorld\x1b[0m!\n")
}
func TestStderrSequencesOn(t *testing.T) {
err := EnableVirtualTerminalProcessing(syscall.Stderr, true)
if err != nil {
t.Fatalf("Failed to enable VTP: %v", err)
}
defer EnableVirtualTerminalProcessing(syscall.Stderr, false)
fmt.Fprintf(os.Stderr, "\x1b[34mHello \x1b[35mWorld\x1b[0m!\n")
}
func TestStderrSequencesOff(t *testing.T) {
err := EnableVirtualTerminalProcessing(syscall.Stderr, false)
if err != nil {
t.Fatalf("Failed to enable VTP: %v", err)
}
fmt.Fprintf(os.Stderr, "\x1b[34mHello \x1b[35mWorld\x1b[0m!\n")
}
+1 -1
View File
@@ -80,7 +80,7 @@ megacheck_install() {
} }
golint_install() { golint_install() {
go get github.com/golang/lint/golint go get golang.org/x/lint/golint
} }
$1 $1
+2 -2
View File
@@ -1,9 +1,9 @@
language: go language: go
go: go:
- 1.8.x
- 1.9.x - 1.9.x
- 1.10.x - 1.10.x
- 1.11.x
- master - master
sudo: true sudo: true
@@ -44,7 +44,7 @@ script:
- > - >
goimports -d -e $(find -name '*.go') | awk '{ print } END { exit NR == 0 ? 0 : 1 }' goimports -d -e $(find -name '*.go') | awk '{ print } END { exit NR == 0 ? 0 : 1 }'
- go vet ./... - go vet ./...
- megacheck -go 1.8 ./... - megacheck -go 1.9 ./...
- golint ./... - golint ./...
- PQTEST_BINARY_PARAMETERS=no go test -race -v ./... - PQTEST_BINARY_PARAMETERS=no go test -race -v ./...
- PQTEST_BINARY_PARAMETERS=yes go test -race -v ./... - PQTEST_BINARY_PARAMETERS=yes go test -race -v ./...
+1 -1
View File
@@ -10,7 +10,7 @@
## Docs ## Docs
For detailed documentation and basic usage examples, please see the package For detailed documentation and basic usage examples, please see the package
documentation at <http://godoc.org/github.com/lib/pq>. documentation at <https://godoc.org/github.com/lib/pq>.
## Tests ## Tests
-2
View File
@@ -1,5 +1,3 @@
// +build go1.1
package pq package pq
import ( import (
-2
View File
@@ -1,5 +1,3 @@
// +build go1.8
package pq package pq
import ( import (
+1 -1
View File
@@ -239,7 +239,7 @@ for more information). Note that the channel name will be truncated to 63
bytes by the PostgreSQL server. bytes by the PostgreSQL server.
You can find a complete, working example of Listener usage at You can find a complete, working example of Listener usage at
http://godoc.org/github.com/lib/pq/example/listen. https://godoc.org/github.com/lib/pq/example/listen.
*/ */
package pq package pq
+1
View File
@@ -0,0 +1 @@
module github.com/lib/pq

Some files were not shown because too many files have changed in this diff Show More