Repository: bnkamalesh/webgo
Branch: master
Commit: 8ee6c6e23f3b
Files: 52
Total size: 150.0 KB
Directory structure:
gitextract_26pnjm80/
├── .github/
│ ├── FUNDING.yml
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.md
│ │ └── feature_request.md
│ └── workflows/
│ └── go.yml
├── .gitignore
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── _config.yml
├── cmd/
│ ├── README.md
│ ├── certs/
│ │ ├── CA.key
│ │ ├── CA.pem
│ │ ├── CA.srl
│ │ ├── localhost.crt
│ │ ├── localhost.csr
│ │ ├── localhost.decrypted.key
│ │ ├── localhost.ext
│ │ └── localhost.key
│ ├── handlers.go
│ ├── main.go
│ └── static/
│ ├── css/
│ │ ├── main.css
│ │ └── normalize.css
│ ├── index.html
│ └── js/
│ ├── main.js
│ └── sse.js
├── config.go
├── config_test.go
├── errors.go
├── errors_test.go
├── extensions/
│ └── sse/
│ ├── README.md
│ ├── client.go
│ ├── message.go
│ └── sse.go
├── go.mod
├── go.sum
├── middleware/
│ ├── accesslog/
│ │ ├── accesslog.go
│ │ └── accesslog_test.go
│ └── cors/
│ ├── cors.go
│ └── cors_test.go
├── responses.go
├── responses_test.go
├── route.go
├── route_test.go
├── router.go
├── router_test.go
├── tests/
│ ├── config.json
│ └── ssl/
│ ├── server.crt
│ ├── server.csr
│ └── server.key
├── webgo.go
└── webgo_test.go
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/FUNDING.yml
================================================
# These are supported funding model platforms
github: [bnkamalesh] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
polar: # Replace with a single Polar username
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
thanks_dev: # Replace with a single thanks.dev username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.
================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
================================================
FILE: .github/workflows/go.yml
================================================
# This workflow will build a golang project
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go
name: Go
on:
push:
branches: ["master"]
pull_request:
branches: ["master"]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: "1.23"
- name: Build
run: go build -v ./...
- name: Tests
run: |
go install github.com/mattn/goveralls@latest
go test -covermode atomic -coverprofile=covprofile $(go list ./... | grep -v /cmd | grep -v /extensions/)
- name: Send coverage
uses: shogo82148/actions-goveralls@v1
with:
path-to-profile: covprofile
- name: golangci-lint
uses: golangci/golangci-lint-action@v6
with:
version: v1.60
================================================
FILE: .gitignore
================================================
# Created by https://www.gitignore.io/api/go,osx,linux,windows
### Go ###
# Binaries for programs and plugins
*.exe
*.dll
*.so
*.dylib
# Test binary, build with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736
.glide/
# Golang project vendor packages which should be ignored
vendor/
### Linux ###
*~
# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*
# KDE directory preferences
.directory
# Linux trash folder which might appear on any partition or disk
.Trash-*
# .nfs files are created when an open file is removed but is still being accessed
.nfs*
### OSX ###
*.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
### Windows ###
# Windows thumbnail cache files
Thumbs.db
ehthumbs.db
ehthumbs_vista.db
# Folder config file
Desktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msm
*.msp
# Windows shortcuts
*.lnk
# End of https://www.gitignore.io/api/go,osx,linux,windows
.vscode
================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at bn_kamalesh@yahoo.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/
================================================
FILE: CONTRIBUTING.md
================================================
Contributions are welcome from everyone. Please adhere to the [code of conduct](https://github.com/naughtygopher/webgo/blob/master/CODE_OF_CONDUCT.md) of the project, and be respectful to all.
Please follow the guidelines provided for contribution
1. Updates to the project are only accepted via Pull Requests (PR)
2. Pull requests will be reviewed & tested
3. Every PR should be accompanied by its test wherever applicable
4. While creating an issue
1. Mention the steps to reproduce the issue
2. Mention the environment in which it was run
3. Include your 1st level of troubleshooting results
5. Provide meaningful commit messages
### Versioning & PR messages
WebGo tries to use [semantic versioning](https://semver.org/) and starting recently, have decided to adhere to the following syntax in PR description. List down the changes as bulleted list, as follows:
```markdown
[major] any backward incompatible or breaking change
[minor] any new feature
[patch] enhancements of existing features, refactor, bug fix etc.
[-] for changes which does not require a version number update
```
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2024 Naughty Gopher
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.
================================================
FILE: README.md
================================================
<p align="center"><img src="https://user-images.githubusercontent.com/1092882/60883564-20142380-a268-11e9-988a-d98fb639adc6.png" alt="webgo gopher" width="256px"/></p>
[](https://github.com/naughtygopher/webgo/actions)
[](https://pkg.go.dev/github.com/naughtygopher/webgo)
[](https://goreportcard.com/report/github.com/naughtygopher/webgo)
[](https://coveralls.io/github/naughtygopher/webgo?branch=master)
[](https://github.com/avelino/awesome-go#web-frameworks)
[](https://github.com/creativecreature/sturdyc/blob/master/LICENSE)
# WebGo v7.0.4
WebGo is a minimalistic router for [Go](https://golang.org) to build web applications (server side) with no 3rd party dependencies. WebGo will always be Go standard library compliant; with the HTTP handlers having the same signature as [http.HandlerFunc](https://golang.org/pkg/net/http/#HandlerFunc).
### Contents
1. [Router](https://github.com/naughtygopher/webgo#router)
2. [Handler chaining](https://github.com/naughtygopher/webgo#handler-chaining)
3. [Middleware](https://github.com/naughtygopher/webgo#middleware)
4. [Error handling](https://github.com/naughtygopher/webgo#error-handling)
5. [Helper functions](https://github.com/naughtygopher/webgo#helper-functions)
6. [HTTPS ready](https://github.com/naughtygopher/webgo#https-ready)
7. [Graceful shutdown](https://github.com/naughtygopher/webgo#graceful-shutdown)
8. [Logging](https://github.com/naughtygopher/webgo#logging)
9. [Server-Sent Events](https://github.com/naughtygopher/webgo#server-sent-events)
10. [Usage](https://github.com/naughtygopher/webgo#usage)
## Router
Webgo has a simplistic, linear path matching router and supports defining [URI](https://developer.mozilla.org/en-US/docs/Glossary/URI)s with the following patterns
1. `/api/users` - URI with no dynamic values
2. `/api/users/:userID`
- URI with a named parameter, `userID`
- If TrailingSlash is set to true, it will accept the URI ending with a '/', refer to [sample](https://github.com/naughtygopher/webgo#sample)
3. `/api/users/:misc*`
- Named URI parameter `misc`, with a wildcard suffix '\*'
- This matches everything after `/api/users`. e.g. `/api/users/a/b/c/d`
When there are multiple handlers matching the same URI, only the first occurring handler will handle the request.
Refer to the [sample](https://github.com/naughtygopher/webgo#sample) to see how routes are configured. You can access named parameters of the URI using the `Context` function.
Note: webgo Context is **not** available inside the special handlers (not found & method not implemented)
```golang
func helloWorld(w http.ResponseWriter, r *http.Request) {
// WebGo context
wctx := webgo.Context(r)
// URI paramaters, map[string]string
params := wctx.Params()
// route, the webgo.Route which is executing this request
route := wctx.Route
webgo.R200(
w,
fmt.Sprintf(
"Route name: '%s', params: '%s'",
route.Name,
params,
),
)
}
```
## Handler chaining
Handler chaining lets you execute multiple handlers for a given route. Execution of a chain can be configured to run even after a handler has written a response to the HTTP request, if you set `FallThroughPostResponse` to `true` (refer [sample](https://github.com/naughtygopher/webgo/blob/master/cmd/main.go#L70)).
## Middleware
WebGo [middlware](https://godoc.org/github.com/naughtygopher/webgo#Middleware) lets you wrap all the routes with a middleware unlike handler chaining. The router exposes a method [Use](https://godoc.org/github.com/naughtygopher/webgo#Router.Use) && [UseOnSpecialHandlers](https://godoc.org/github.com/naughtygopher/webgo#Router.UseOnSpecialHandlers) to add a Middleware to the router.
NotFound && NotImplemented are considered `Special` handlers. `webgo.Context(r)` within special handlers will return `nil`.
Any number of middleware can be added to the router, the order of execution of middleware would be [LIFO](<https://en.wikipedia.org/wiki/Stack_(abstract_data_type)>) (Last In First Out). i.e. in case of the following code
```golang
func main() {
router.Use(accesslog.AccessLog, cors.CORS(nil))
router.Use(<more middleware>)
}
```
**_CorsWrap_** would be executed first, followed by **_AccessLog_**.
## Error handling
Webgo context has 2 methods to [set](https://github.com/naughtygopher/webgo/blob/master/webgo.go#L60) & [get](https://github.com/naughtygopher/webgo/blob/master/webgo.go#L66) erro within a request context. It enables Webgo to implement a single middleware where you can handle error returned within an HTTP handler. [set error](https://github.com/naughtygopher/webgo/blob/master/cmd/main.go#L45), [get error](https://github.com/naughtygopher/webgo/blob/master/cmd/main.go#L51).
## Helper functions
WebGo provides a few helper functions. When using `Send` or `SendResponse` (other Rxxx responder functions), the response is wrapped in WebGo's [response struct](https://github.com/naughtygopher/webgo/blob/master/responses.go#L17) and is serialized as JSON.
```json
{
"data": "<any valid JSON payload>",
"status": "<HTTP status code, of type integer>"
}
```
When using `SendError`, the response is wrapped in WebGo's [error response struct](https://github.com/naughtygopher/webgo/blob/master/responses.go#L23) and is serialzied as JSON.
```json
{
"errors": "<any valid JSON payload>",
"status": "<HTTP status code, of type integer>"
}
```
## HTTPS ready
HTTPS server can be started easily, by providing the key & cert file. You can also have both HTTP & HTTPS servers running side by side.
Start HTTPS server
```golang
cfg := &webgo.Config{
Port: "80",
HTTPSPort: "443",
CertFile: "/path/to/certfile",
KeyFile: "/path/to/keyfile",
}
router := webgo.NewRouter(cfg, routes()...)
router.StartHTTPS()
```
Starting both HTTP & HTTPS server
```golang
cfg := &webgo.Config{
Port: "80",
HTTPSPort: "443",
CertFile: "/path/to/certfile",
KeyFile: "/path/to/keyfile",
}
router := webgo.NewRouter(cfg, routes()...)
go router.StartHTTPS()
router.Start()
```
## Graceful shutdown
Graceful shutdown lets you shutdown the server without affecting any live connections/clients connected to the server. Any new connection request after initiating a shutdown would be ignored.
Sample code to show how to use shutdown
```golang
func main() {
osSig := make(chan os.Signal, 5)
cfg := &webgo.Config{
Host: "",
Port: "8080",
ReadTimeout: 15 * time.Second,
WriteTimeout: 60 * time.Second,
ShutdownTimeout: 15 * time.Second,
}
router := webgo.NewRouter(cfg, routes()...)
go func() {
<-osSig
// Initiate HTTP server shutdown
err := router.Shutdown()
if err != nil {
fmt.Println(err)
os.Exit(1)
} else {
fmt.Println("shutdown complete")
os.Exit(0)
}
// If you have HTTPS server running, you can use the following code
// err := router.ShutdownHTTPS()
// if err != nil {
// fmt.Println(err)
// os.Exit(1)
// } else {
// fmt.Println("shutdown complete")
// os.Exit(0)
// }
}()
go func(){
time.Sleep(time.Second*15)
signal.Notify(osSig, os.Interrupt, syscall.SIGTERM)
}()
router.Start()
}
```
## Logging
WebGo exposes a singleton & global scoped logger variable [LOGHANDLER](https://godoc.org/github.com/naughtygopher/webgo#Logger) with which you can plug in your custom logger by implementing the [Logger](https://godoc.org/github.com/naughtygopher/webgo#Logger) interface.
### Configuring the default Logger
The default logger uses Go standard library's `log.Logger` with `os.Stdout` (for debug and info logs) & `os.Stderr` (for warning, error, fatal) as default io.Writers. You can set the io.Writer as well as disable specific types of logs using the `GlobalLoggerConfig(stdout, stderr, cfgs...)` function.
## Server-Sent Events
[MDN has a very good documentation of what SSE (Server-Sent Events)](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events) are. The sample app provided shows how to use the SSE extension of webgo.
## Usage
A fully functional sample is provided [here](https://github.com/naughtygopher/webgo/blob/master/cmd/main.go).
### Benchmark
1. [the-benchmarker](https://github.com/the-benchmarker/web-frameworks)
2. [go-web-framework-benchmark](https://github.com/smallnest/go-web-framework-benchmark)
### Contributing
Refer [here](https://github.com/naughtygopher/webgo/blob/master/CONTRIBUTING.md) to find out details about making a contribution
### Credits
Thanks to all the [contributors](https://github.com/naughtygopher/webgo/graphs/contributors)
## The gopher
The gopher used here was created using [Gopherize.me](https://gopherize.me/). WebGo stays out of developers' way, so sitback and enjoy a cup of coffee.
================================================
FILE: _config.yml
================================================
theme: jekyll-theme-cayman
================================================
FILE: cmd/README.md
================================================
# Webgo Sample
### Server Sent Events

This picture shows the sample SSE implementation provided with this application. In the sample app, the server is
sending timestamp every second, to all the clients.
**Important**: _[SSE](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events)
is a live connection between server & client. So a short WriteTimeout duration in webgo.Config will
keep dropping the connection. If you have any middleware which is setting deadlines or timeouts on the
request.Context, will also effect these connections._
## How to run
If you have Go installed on your computer, open the terminal and:
```bash
$ cd $GOPATH/src
$ git clone https://github.com/naughtygopher/webgo.git
$ cd webgo/cmd
$ go run *.go
Info 2023/02/05 08:51:26 HTTP server, listening on :8080
Info 2023/02/05 08:51:26 HTTPS server, listening on :9595
```
Or if you have [Docker](https://www.docker.com/), open the terminal and:
```bash
$ git clone https://github.com/naughtygopher/webgo.git
$ cd webgo
$ docker run \
-p 8080:8080 \
-p 9595:9595 \
-v ${PWD}:/go/src/github.com/naughtygopher/webgo/ \
-w /go/src/github.com/naughtygopher/webgo/cmd \
--rm -ti golang:latest go run *.go
Info 2023/02/05 08:51:26 HTTP server, listening on :8080
Info 2023/02/05 08:51:26 HTTPS server, listening on :9595
```
You can try the following API calls with the sample app. It also uses all the features provided by webgo
1. `http://localhost:8080/`
- Loads an HTML page
2. `http://localhost:8080/matchall/`
- Route with wildcard parameter configured
- All URIs which begin with `/matchall` will be matched because it has a wildcard variable
- e.g.
- http://localhost:8080/matchall/hello
- http://localhost:8080/matchall/hello/world
- http://localhost:8080/matchall/hello/world/user
3. `http://localhost:8080/api/<param>`
- Route with a named 'param' configured
- It will match all requests which match `/api/<single parameter>`
- e.g.
- http://localhost:8080/api/hello
- http://localhost:8080/api/world
4. `http://localhost:8080/error-setter`
- Route which sets an error and sets response status 500
5. `http://localhost:8080/v7.0.0/api/<param>`
- Route with a named 'param' configured
- It will match all requests which match `/v7.0.0/api/<single parameter>`
- e.g.
- http://localhost:8080/v7.0.0/api/hello
- http://localhost:8080/v7.0.0/api/world
================================================
FILE: cmd/certs/CA.key
================================================
-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: DES-EDE3-CBC,3172865781AB630D
b65fTCSGMadtvR5GnVfTsssu9qrQgm5XNCXRJ+PLpuXuWdywORTOEM4/FtRA62AN
f41Dl/OO7nUNTTF4J+0fNiLMmnkiYvGSa5lcjRqBikk6waZjYkSUoo4TdGh502F/
jFzbGzqXlX0qt+pj0HY/fhKxpr45bOkBr9S3ucMbgyuREX1HShxF759Mc+sJFx7w
Ghs+byTL3cZABHRn/0xpC9S/PhEnsW3dPPJItCkEUsLumwTFCuaGskpP3chuVhZh
36p74An+Tg9wlgcaCaUSUHWs22rnLBHAjv9JzVuoLJWdUwIYrcQTJz2+8mVlun24
Qns7dhRafOHlIeOCxI/fmKlXE/S9S7tuXaMsHtqGK/22MAXJx/AOymjfgXfTLnF1
XmLK0FTyp9BB1pfW9P+D6JEp7bj1fRHZeCOkesJOjyDEb+v+oYMaha6IVfQCMKQf
P1+okMwBPGQIAr5d4Ov82mwpDyO1+rHAFn3b3zuro4rfHiHPDTo9Oa7EtMBIWZgj
Ln0KLeaRkPht6wSUubCSl8Ypg27xbHwbQbWcQsr/OwLgWMJ40/1QKaqYIDV4LytZ
mSzwwo4kQASKI1jwFWff+4yVqd3SuyW5uGcPNnkbneyKZzFHd/rAWnsT9cIIOX0v
US5LEn9r+qjYk+ZVCSgqmrwUHyPfbx/BeARJTDBzo2jJC+ZiR7UkzD54r5pzs7R4
9N3hVVmocS9BNPj45ioLw/Fhbu62N34NkRdDAisPhlQ+yM7klKBciQsEsKj1c6iw
QwJNScvgW6x0+47J+tp21KLuxdfVP+Bq9wSb2B4kR3l93/ZAXR1S0cg0cuez1frd
8g1kpQMzxhWVMzB6NHG7wAIo7p947mueP3Ggh1rgA6WLTltvH11ywadglG23HB7R
zuxcQqoPm7Tzcift316DE5Q+qipHDA2UmiWZ83ZVQCshiJxILsPkGbh7k+mRwRyh
e08TLEJMtCWiqvCmxHbebx7y8+oX035QIUVIOxHGier2CyZtgpaZyeEa4DHZCwuH
ZrTMfigGDSXnonCrVtsC9zSYGQav/tRVxahKsM/TA+O1gOWNTncEe2ESKWaBzJpQ
wUo7u/e/hbyPxMd2ezmeYVWwRSy9J/uZOsH0DsUlGsUzTWj+qrbmrdDYGVI2sgA1
xfTfv7vdLFpESDRV4eRWscblusHffjCFA9oC9Y9qj2M8X+HLa/EELMb5CCWGZm3E
HMZKwg6cah5rc82FFk0nDTzNpw57rTorPOGDe3oUif2NR+A4gxyepVrNDBFxDzbd
PT2YMPr2IsHgxnRwFyoAPGG3rKCO+rFNJKcfrxvdeTj7mZ0Yzo93f9ycgNhqDKnR
+n8vCWtWnRuDu8hk3d5YjhaZux/SmOuF9G6VX0jnJA534aLhEo0mzddGKM0bnOGN
J1aMc65s4rPqUOQiase/3w7fNDu6szF/tcUMEWPRCDymZpZ9yznK122ajQVrKyOq
HDcftg1rrKli8I/AljJMgC9ACZ9YfOfZ3qymcY7X7ZJyMucKi822ajIggi630aJQ
sEqwxl5coM/N+Rhcp2/NiYyq3MXQBibKhq00OBLq76QQeNsJbRhUDw==
-----END RSA PRIVATE KEY-----
================================================
FILE: cmd/certs/CA.pem
================================================
-----BEGIN CERTIFICATE-----
MIIDijCCAnICCQCImGKiTiq7ITANBgkqhkiG9w0BAQsFADCBhjELMAkGA1UEBhMC
SU4xCzAJBgNVBAgMAktBMRIwEAYDVQQHDAlCZW5nYWx1cnUxFTATBgNVBAoMDERl
bGl2ZXJ5SGVybzETMBEGA1UECwwKUS1Db21tZXJjZTESMBAGA1UEAwwJbG9jYWxo
b3N0MRYwFAYJKoZIhvcNAQkBFgdrQGIuY29tMB4XDTIyMTIwOTE2MDQxMFoXDTMy
MTIwNjE2MDQxMFowgYYxCzAJBgNVBAYTAklOMQswCQYDVQQIDAJLQTESMBAGA1UE
BwwJQmVuZ2FsdXJ1MRUwEwYDVQQKDAxEZWxpdmVyeUhlcm8xEzARBgNVBAsMClEt
Q29tbWVyY2UxEjAQBgNVBAMMCWxvY2FsaG9zdDEWMBQGCSqGSIb3DQEJARYHa0Bi
LmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAM83cerEFzgRxu/l
NrFOGOCn1bwexXoP1xawzVaj/vmTL4+bN114e4KnJrmon9k9NHUxhJJGfNxM7cSG
7lYlIwIDdMt1FNm1iWjPycvcxWZppEnjbwlOGYn0miekr+SSh18AbCZ+kcZFx6Cp
O4wEb29kXCxInGEdgj5M23EpQdi5qXCmfmrIl/ueiNnJPQezFir8UizRKG5xHnZK
BaGcT0E4lOJLLKGqpvN5v0cO1Vwu2nxFmXlcV5dWsPOjxvPPSCYuu0EHhJ2jucQW
MNtc6cucG70rOJwkQi6JMbS5XU9pboux2O/H0WUwFSYjI40opWjKpXE5eo6GIgl/
eHCiNJ8CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAH9a3WOMgGvDn80O8hvhZzXO8
6XPBDqjW7Zk/l1GiZYpNuDvAuqiBIDxKQZdRtnbRBTCLGO6yyHt974WStJud1/sN
Loam78+GMEMJq0tUNUNXuOVNLo/Zz/4tN2cDosnB8k+Atm+c3m5TSHaOayOy+PJL
OiDi7RP5IPpiEYtdGvE2eoYfqjSnY00kIV5ea57PIc3gkFO9FXP+M6UXzkxo2xC7
fvVhYEVjQ7uvdWLGKYMvF/PRV2OKRnAdFasga3k8PyC/ToxjN/87ypeUy1VZzEm7
3zsbislIKL36CCOYmGaUgTPfxXWN/MvUgb87lWfrqPbSM7ooSQGHFe1eP5Wd2g==
-----END CERTIFICATE-----
================================================
FILE: cmd/certs/CA.srl
================================================
FDC7B23B140FA79F
================================================
FILE: cmd/certs/localhost.crt
================================================
-----BEGIN CERTIFICATE-----
MIIEgjCCA2qgAwIBAgIJAP3HsjsUD6efMA0GCSqGSIb3DQEBCwUAMIGGMQswCQYD
VQQGEwJJTjELMAkGA1UECAwCS0ExEjAQBgNVBAcMCUJlbmdhbHVydTEVMBMGA1UE
CgwMRGVsaXZlcnlIZXJvMRMwEQYDVQQLDApRLUNvbW1lcmNlMRIwEAYDVQQDDAls
b2NhbGhvc3QxFjAUBgkqhkiG9w0BCQEWB2tAYi5jb20wHhcNMjIxMjA5MTYwOTQ2
WhcNMzIxMjA2MTYwOTQ2WjCBlzELMAkGA1UEBhMCREUxDzANBgNVBAgMBkJlcmxp
bjEWMBQGA1UEBwwNUmVpbmlja2VuZG9yZjEWMBQGA1UECgwNRGVsaXZlcnkgSGVy
bzETMBEGA1UECwwKUS1Db21tZXJjZTEZMBcGA1UEAwwQZGVsaXZlcnloZXJvLmNv
bTEXMBUGCSqGSIb3DQEJARYIa0Bibi5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IB
DwAwggEKAoIBAQDRT6l39GZ3vSAi1eLt8oauseH4uNwijzcaqHot45f3087eqSej
hfqmTfhga+MSDtbKIo73O4wq12klCbTtil4UpRT4dVJwKQLXLriFCiq40Wzcyhqy
E0qGoZG1TCoy3PLUCwkxXlixAdEimhuZPIVPDQIY0fs1c8GxdFfhxMQ88WEqs0Rp
rygYp+hD18Hk0VYhPmqZXb0m3BG7/eTYHrYDVAdk9f2OYMR925idwk94iHvTjOqC
bOpVOzF4FM+jkT7r4hfa2UuCF4sYNhAP2DEZWHnoYjb7cxRDKYshMqCH5WhlbB+v
rAllLs+4GEu1yOs0VYqt2TzXNr/6KK1G77SRAgMBAAGjgd8wgdwwgaUGA1UdIwSB
nTCBmqGBjKSBiTCBhjELMAkGA1UEBhMCSU4xCzAJBgNVBAgMAktBMRIwEAYDVQQH
DAlCZW5nYWx1cnUxFTATBgNVBAoMDERlbGl2ZXJ5SGVybzETMBEGA1UECwwKUS1D
b21tZXJjZTESMBAGA1UEAwwJbG9jYWxob3N0MRYwFAYJKoZIhvcNAQkBFgdrQGIu
Y29tggkAiJhiok4quyEwCQYDVR0TBAIwADALBgNVHQ8EBAMCBPAwGgYDVR0RBBMw
EYIJbG9jYWxob3N0hwR/AAABMA0GCSqGSIb3DQEBCwUAA4IBAQA2P0I8ujEPS2GB
7cMUAE3cB4fxEwI2t89JGcmeK0BUyVBlvdwZGEIVs2Tn0FFbs0VpcgeY3YkV2ogs
ekrUHQzmnRX9EtTMucGM6gX4JeDJWWthehVIB6Jp1iDLtAAbyCGph5nrdArkA0tR
ANkyrXKTcMAx3giBzSZrpxguF+fnASZ+p99c57FnRXjMv5NkQnSCgRQkmaHtUIKJ
dDAlEIyPrpfe2bbw7BtUt2UiW9KPz/CG9TDrpWzh5jRyoJRzXUhlOPgFPCBvH1AC
DGl2ciAGfScEY+HZp+YPdTzln5TSc4w/REuWDqBIydJwytUQX+EcTA0936TQq3ec
lYKaXexW
-----END CERTIFICATE-----
================================================
FILE: cmd/certs/localhost.csr
================================================
-----BEGIN CERTIFICATE REQUEST-----
MIIC3TCCAcUCAQAwgZcxCzAJBgNVBAYTAkRFMQ8wDQYDVQQIDAZCZXJsaW4xFjAU
BgNVBAcMDVJlaW5pY2tlbmRvcmYxFjAUBgNVBAoMDURlbGl2ZXJ5IEhlcm8xEzAR
BgNVBAsMClEtQ29tbWVyY2UxGTAXBgNVBAMMEGRlbGl2ZXJ5aGVyby5jb20xFzAV
BgkqhkiG9w0BCQEWCGtAYm4uY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
CgKCAQEA0U+pd/Rmd70gItXi7fKGrrHh+LjcIo83Gqh6LeOX99PO3qkno4X6pk34
YGvjEg7WyiKO9zuMKtdpJQm07YpeFKUU+HVScCkC1y64hQoquNFs3MoashNKhqGR
tUwqMtzy1AsJMV5YsQHRIpobmTyFTw0CGNH7NXPBsXRX4cTEPPFhKrNEaa8oGKfo
Q9fB5NFWIT5qmV29JtwRu/3k2B62A1QHZPX9jmDEfduYncJPeIh704zqgmzqVTsx
eBTPo5E+6+IX2tlLgheLGDYQD9gxGVh56GI2+3MUQymLITKgh+VoZWwfr6wJZS7P
uBhLtcjrNFWKrdk81za/+iitRu+0kQIDAQABoAAwDQYJKoZIhvcNAQELBQADggEB
ADGo51Y/2/uJhndzitmBLM9yvwXmsD7JQvT3y+8xSne0p+jniwHhzqLww6SLLtIQ
LRUzbXQlnJnPzj2fhdPYM9238Cxyd/w59/cg/RXnkjMnoaiH/9FZpqwIwnMFugkp
+BcqszZat70OjdhZPkI/WzImNHdtSzUFhI3OACXqhdSM2wGkzHQCWxMzRmKsE8XF
iMmuvFVBExXLoG/PqoRS5W3Op1SZdYvKybhmrgM+XeHlvTv8VOFgUBmBEolhZtvU
9eT3ommzMbyZSbf6eXlTj+OrBTlN7n8et42TyDaf03kZhhiSpiq+lNz7+eHcF64Z
8u4AOXi22aPkwy2fffYKr4E=
-----END CERTIFICATE REQUEST-----
================================================
FILE: cmd/certs/localhost.decrypted.key
================================================
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEA0U+pd/Rmd70gItXi7fKGrrHh+LjcIo83Gqh6LeOX99PO3qkn
o4X6pk34YGvjEg7WyiKO9zuMKtdpJQm07YpeFKUU+HVScCkC1y64hQoquNFs3Moa
shNKhqGRtUwqMtzy1AsJMV5YsQHRIpobmTyFTw0CGNH7NXPBsXRX4cTEPPFhKrNE
aa8oGKfoQ9fB5NFWIT5qmV29JtwRu/3k2B62A1QHZPX9jmDEfduYncJPeIh704zq
gmzqVTsxeBTPo5E+6+IX2tlLgheLGDYQD9gxGVh56GI2+3MUQymLITKgh+VoZWwf
r6wJZS7PuBhLtcjrNFWKrdk81za/+iitRu+0kQIDAQABAoIBAQCwHW5Df0HkiB6N
ERiTC9ilDwlKxQh8j7JW3OGI0RJiNTbABOZUYfwHiF1vi/eQjynNBIz0m4cR2RQg
VO2GXUFR76EYeWb29prsQeSCFI7j2VrW37rckPzJERNPz5lGGMC9B9ghUPghX5z/
l1mXcuPcIt7b0XqkfBTC4li7n1taxluvKpxtFoN7XuV7QyhpQtR74M9CAmmtxAjY
3HSY3fC7kSbT+8mii29stm3/zJSdnuSdlfrHRv+cXsrWfN5isjpvDb7HCcvm7OHB
QyUC89zy/WM8xReKEStkRX+/+zW2//y7aXMzI/YmiPdX+XoIY+MdxqvMjzbATZF+
haBeM5ttAoGBAP5GhhmuvKSURJTfoEroIxtXnfpvE4T/+TQmR1Wc9m/Ecc6iVuHF
RgHpkKhOFl5P457Y1oRpYkmECxZ4KuvHwIqe9rRofGByxbxilvA69p8RkV8oI/gc
qP5+VN2Sx77tTTKVX7S6SFrROSmB9YAE7/83Vo1oD9absl47SeR9+EBbAoGBANK7
EhIYE12q5nQBDhjjAt9Xa6N+ROjonjW1cWq/v6FWgoBXiQtsh1MSK2KZPi7F05kP
9oYM//4oMy/mFsKbS8HTy+Y7HDYvh7Gz3gAxUWrabIFrJlJMXMj9VIgYDZsnQiBD
3j2sUOpr+5B4laHD5oM8rYjR10F1AN7oAuTdDzKDAoGAfmkHH9t70wIW+kAWi0bO
tTggxLDV7mfnNyLUkd5fsX7i6UxRjxoozKiWDuYLPsXOrli0hM1zXIL1lC0XgXIj
6YZPta7ALp7AaQBGc5WMp9XvBHSLNTziUurxO9pNzUBiAYS7OLjnYabkGRuPth4+
Rg33zILwZMuwqCIngR2S/kMCgYB9ssCQsnO6x5o3T/nMtnycJFU8bLFGDJtyhgxl
FIOGBUhKrew9ODtwPcJLSgVhePdCsdbnFxIL1IbT53dkFaYWs/NIHbIyUB+szBF8
I+7gwfE/MV7mcE5YRWQK2e4jwkMbY+BJAWQysL6Z6pO2rlftqGAK4MB5dwVR8Sro
wUOzaQKBgQDZ3ohj61drnkkdRRRX5115ne3AMctx1rn7Ss6AByDn21wYUrOP0qAy
i4kmah0kibqz8+N6PM7G7gA5uXHk0bwFfgU/TPQdz6DfzSbTmO7lEEJ7ejI9ml0C
1Y16Rya8Ny3fLjCFWKYReFhXUWNzuWPHsQezUiwnMeHRka0c9QFVSA==
-----END RSA PRIVATE KEY-----
================================================
FILE: cmd/certs/localhost.ext
================================================
authorityKeyIdentifier = keyid,issuer
basicConstraints = CA:FALSE
keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
subjectAltName = @alt_names
[alt_names]
DNS.1 = localhost
IP.1 = 127.0.0.1
================================================
FILE: cmd/certs/localhost.key
================================================
-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: DES-EDE3-CBC,9D6364828FCEE2E0
2RhK7oE6kVWBNDFGjRUuxPO5XI3qarcJTswXNk7YFqmyQpgt9cFXUdRI36Ppbpcs
Sw6h2gmBaqsS7sbL3wWZyZL+PcVVCy7TRTBI/mU1Cvj1iRe9ms3+b7uwnuSGh0DF
G40X+VACjwmP+hAKQivcPtog+c5laAx/QQ456kNQMl9yYCdbJCTCBNKVS8ycIB54
0moAxCv8lgO0mllh4vvMl5RbfOqPVr0Ky8EWJJqs9CWCy7eKLe8BIzy7nDGcVhx+
BZ24r8h59OIUWCFmJ/FXhDTLXrPr2xpF0COTKuPrTtU7LMaCdYljyilfkCo4l+Pk
ctawRwKLUd4/IE26P90WaoV85y9tne1145pcKQVQa0NdfI6rRA2jRqzv6iFrVYBM
AUeybrX+HnaHABwJYNunXBCk3YUWk5NuMPsTNGlPqMPQyVC/D196YSU16sDx2zlT
Z886GqLfr5SqwInvlb7zQn72+1beoh2Du7NQ6oXuVZsc5w1sGiv7B7axOEznJ8KT
QxTst9pCLj2zpm45fRW8cKscC9g/fXMYQrRwcSeeuWvXtI9csljpWKpolvepKFmx
bUrYUIqFyfCZsLBHaXf3vsbPF/h3q0ZGDadVTWioiA9jpi+y5rcb8mFDl9qiRBsC
RDafd0aoJPXiWotO58Fo4VXRLWqEUhyHF9oVN5SD5CWSWos/YfkRXAVmElEK+dbM
TpumlQvt6gbXculag7c/OtHJtVfLuhqdW4yGXvwY1z07U+EdYLrMomKffYaxjTi5
2c1bNA7t2cF4mQk++DH9jvj1Xfuxdh5p84g1tvSYrVvJWt4JHzbzknylteGMSJ5w
dkuDXdBS9qrwjvc6nLNAn+qXduzAyzf2GR5obe/mFwZSvm33o6W/DFWkhQDdHjSC
o7QuyNvHYYMCau7CKcpgoBycBrkktQ6gFAGR1HsT4Xdsk60XkLi8ctUiLPdFL6zc
tRcmZt+ddzJA2qaNCxNopj2J8Qkav208zQ9f7P6DZNe3pWI09SrUisnAb0cuYASm
XfAcBP9vBfKx9pvxfpVgp6DYFaVllgXDM+Z1lmKzZMOAb0RFcyWsYtILIgLnTDeG
tMeHygAevAAT1N1uKlQMvXvOtmehOGVfBAaJVt1U4xTWjCxBGfWed+bs9NkL2zSU
0/kEacWEbq0c23Wsiv8IHKxqZLPyeiyHvZgDWLt4v1Y+t9l+q+HZIQxXmFhb96qt
+tOv7pY6dig2fLRAApp9Q2kKfimQBtO7sxafK3qBmT75Y86gBdmSgjREviec9Bb2
VeZeqf/TPhiBVbRpYAe4RixF5cal3pxC9N9GmEMk+Qfr/MEonkUgX8w4tlP3hkXx
HS+5GpltrIag5GEaKbFgX/FYcHbDQNMXmIqV6ieavN+04MbHuC4Cm1joHVTEQJUn
II0mfCAgxFOYKqr2Sex86zxhury5O1oYS0ETCTCLVgab1dIin4+cElrGW42Sw3Bm
tGChrevGYi2hBQkBOHfnw2lFLtJmQ0R0pke+CICVWLriTV5N183f5zosdjJyYpxa
JL7p/ePLjLRCvMpY185/mbvc8h1AtIsm6N67r8xK250oZBid9LZXa6G449RnPku7
-----END RSA PRIVATE KEY-----
================================================
FILE: cmd/handlers.go
================================================
package main
import (
"context"
"errors"
"fmt"
"log"
"net/http"
"os"
"strings"
"github.com/naughtygopher/webgo/v7"
"github.com/naughtygopher/webgo/v7/extensions/sse"
)
// StaticFilesHandler is used to serve static files
func StaticFilesHandler(rw http.ResponseWriter, r *http.Request) {
wctx := webgo.Context(r)
// '..' is replaced to prevent directory traversal which could go out of static directory
path := strings.ReplaceAll(wctx.Params()["w"], "..", "-")
path = strings.ReplaceAll(path, "~", "-")
rw.Header().Set("Last-Modified", lastModified)
http.ServeFile(rw, r, fmt.Sprintf("./static/%s", path))
}
func OriginalResponseWriterHandler(w http.ResponseWriter, r *http.Request) {
rw := webgo.OriginalResponseWriter(w)
if rw == nil {
webgo.Send(w, "text/html", "got nil", http.StatusPreconditionFailed)
return
}
webgo.Send(w, "text/html", "success", http.StatusOK)
}
func HomeHandler(w http.ResponseWriter, r *http.Request) {
fs, err := os.OpenFile("./static/index.html", os.O_RDONLY, 0600)
if err != nil {
webgo.SendError(w, err.Error(), http.StatusInternalServerError)
return
}
info, err := fs.Stat()
if err != nil {
webgo.SendError(w, err.Error(), http.StatusInternalServerError)
return
}
out := make([]byte, info.Size())
_, err = fs.Read(out)
if err != nil {
webgo.SendError(w, err.Error(), http.StatusInternalServerError)
return
}
pushHomepage(r, w)
_, _ = w.Write(out)
}
func pushCSS(pusher http.Pusher, r *http.Request, path string) {
cssOpts := &http.PushOptions{
Header: http.Header{
"Accept-Encoding": r.Header["Accept-Encoding"],
"Content-Type": []string{"text/css; charset=UTF-8"},
},
}
err := pusher.Push(path, cssOpts)
if err != nil {
webgo.LOGHANDLER.Error(err)
}
}
func pushJS(pusher http.Pusher, r *http.Request, path string) {
cssOpts := &http.PushOptions{
Header: http.Header{
"Accept-Encoding": r.Header["Accept-Encoding"],
"Content-Type": []string{"application/javascript"},
},
}
err := pusher.Push(path, cssOpts)
if err != nil {
webgo.LOGHANDLER.Error(err)
}
}
func pushHomepage(r *http.Request, w http.ResponseWriter) {
pusher, ok := w.(http.Pusher)
if !ok {
return
}
cp, _ := r.Cookie("pusher")
if cp != nil {
return
}
cookie := &http.Cookie{
Name: "pusher",
Value: "css,js",
MaxAge: 300,
}
http.SetCookie(w, cookie)
pushCSS(pusher, r, "/static/css/main.css")
pushCSS(pusher, r, "/static/css/normalize.css")
pushJS(pusher, r, "/static/js/main.js")
pushJS(pusher, r, "/static/js/sse.js")
}
func SSEHandler(sse *sse.SSE) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
params := webgo.Context(r).Params()
r.Header.Set(sse.ClientIDHeader, params["clientID"])
err := sse.Handler(w, r)
if err != nil && !errors.Is(err, context.Canceled) {
log.Println("errorLogger:", err.Error())
return
}
}
}
func ErrorSetterHandler(w http.ResponseWriter, r *http.Request) {
err := errors.New("oh no, server error")
webgo.SetError(r, err)
webgo.R500(w, err.Error())
}
func ParamHandler(w http.ResponseWriter, r *http.Request) {
// WebGo context
wctx := webgo.Context(r)
// URI parameters, map[string]string
params := wctx.Params()
// route, the webgo.Route which is executing this request
route := wctx.Route
webgo.R200(
w,
map[string]interface{}{
"route_name": route.Name,
"route_pattern": route.Pattern,
"params": params,
"chained": r.Header.Get("chained"),
},
)
}
func InvalidJSONHandler(w http.ResponseWriter, r *http.Request) {
webgo.R200(w, make(chan int))
}
================================================
FILE: cmd/main.go
================================================
package main
import (
"context"
"fmt"
"log"
"net/http"
"os"
"strings"
"time"
"github.com/naughtygopher/webgo/v7"
"github.com/naughtygopher/webgo/v7/extensions/sse"
"github.com/naughtygopher/webgo/v7/middleware/accesslog"
"github.com/naughtygopher/webgo/v7/middleware/cors"
)
var (
lastModified = time.Now().Format(http.TimeFormat)
)
func chain(w http.ResponseWriter, r *http.Request) {
r.Header.Set("chained", "true")
}
// errLogger is a middleware which will log all errors returned/set by a handler
func errLogger(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
next(w, r)
err := webgo.GetError(r)
if err != nil {
// log only server errors
if webgo.ResponseStatus(w) > 499 {
log.Println("errorLogger:", err.Error())
}
}
}
func routegroupMiddleware(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
w.Header().Add("routegroup", "true")
next(w, r)
}
func getRoutes(sse *sse.SSE) []*webgo.Route {
return []*webgo.Route{
{
Name: "root",
Method: http.MethodGet,
Pattern: "/",
Handlers: []http.HandlerFunc{HomeHandler},
TrailingSlash: true,
},
{
Name: "matchall",
Method: http.MethodGet,
Pattern: "/matchall/:wildcard*",
Handlers: []http.HandlerFunc{ParamHandler},
TrailingSlash: true,
},
{
Name: "api",
Method: http.MethodGet,
Pattern: "/api/:param",
Handlers: []http.HandlerFunc{chain, ParamHandler},
TrailingSlash: true,
FallThroughPostResponse: true,
},
{
Name: "invalidjson",
Method: http.MethodGet,
Pattern: "/invalidjson",
Handlers: []http.HandlerFunc{InvalidJSONHandler},
TrailingSlash: true,
},
{
Name: "error-setter",
Method: http.MethodGet,
Pattern: "/error-setter",
Handlers: []http.HandlerFunc{ErrorSetterHandler},
TrailingSlash: true,
},
{
Name: "original-responsewriter",
Method: http.MethodGet,
Pattern: "/original-responsewriter",
Handlers: []http.HandlerFunc{OriginalResponseWriterHandler},
TrailingSlash: true,
},
{
Name: "static",
Method: http.MethodGet,
Pattern: "/static/:w*",
Handlers: []http.HandlerFunc{StaticFilesHandler},
TrailingSlash: true,
},
{
Name: "sse",
Method: http.MethodGet,
Pattern: "/sse/:clientID",
Handlers: []http.HandlerFunc{SSEHandler(sse)},
TrailingSlash: true,
},
}
}
func setup() (*webgo.Router, *sse.SSE) {
port := strings.TrimSpace(os.Getenv("HTTP_PORT"))
if port == "" {
port = "8080"
}
cfg := &webgo.Config{
Host: "",
Port: port,
HTTPSPort: "9595",
ReadTimeout: 15 * time.Second,
WriteTimeout: 1 * time.Hour,
CertFile: "./certs/localhost.crt",
KeyFile: "./certs/localhost.decrypted.key",
}
webgo.GlobalLoggerConfig(
nil, nil,
webgo.LogCfgDisableDebug,
)
routeGroup := webgo.NewRouteGroup("/v7.0.0", false)
routeGroup.Add(webgo.Route{
Name: "router-group-prefix-v7.0.0_api",
Method: http.MethodGet,
Pattern: "/api/:param",
Handlers: []http.HandlerFunc{chain, ParamHandler},
})
routeGroup.Use(routegroupMiddleware)
sseService := sse.New()
sseService.OnRemoveClient = func(ctx context.Context, clientID string, count int) {
log.Printf("\nClient %q removed, active client(s): %d\n", clientID, count)
}
sseService.OnCreateClient = func(ctx context.Context, client *sse.Client, count int) {
log.Printf("\nClient %q added, active client(s): %d\n", client.ID, count)
}
routes := getRoutes(sseService)
routes = append(routes, routeGroup.Routes()...)
router := webgo.NewRouter(cfg, routes...)
router.UseOnSpecialHandlers(accesslog.AccessLog)
router.Use(
errLogger,
cors.CORS(nil),
accesslog.AccessLog,
)
return router, sseService
}
func main() {
router, sseService := setup()
clients := []*sse.Client{}
sseService.OnCreateClient = func(ctx context.Context, client *sse.Client, count int) {
clients = append(clients, client)
}
// broadcast server time to all SSE listeners
go func() {
retry := time.Millisecond * 500
for {
now := time.Now().Format(time.RFC1123Z)
sseService.Broadcast(sse.Message{
Data: now + fmt.Sprintf(" (%d)", sseService.ActiveClients()),
Retry: retry,
})
time.Sleep(time.Second)
}
}()
go router.StartHTTPS()
router.Start()
}
================================================
FILE: cmd/static/css/main.css
================================================
* {
transition: color 0.25s, margin 0.25s, padding 0.25s, width 0.25s, height 0.25s, background-color 0.25s;
}
html, body {
font-family: sans-serif;
font-size: 16px;
line-height: 1.5em;
font-weight: 400;
background: #efefef;
color: #444;
}
p {
margin: 0 0 1em;
}
a {
color: #999;
}
a:hover {
color: #222;
}
section.main {
background: #fff;
width: 90%;
max-width: 370px;
margin: 10vw auto;
padding: 0 2em;
border-radius: 4px;
overflow: hidden;
}
table {
width: 100%;
font-size: 12px;
line-height: 1.5em;
border: 1px solid #eee;
border-collapse: collapse;
}
tr, td {
border: 1px solid #eee;
}
td {
padding: 0.25rem;
text-align: right;
}
td:nth-child(1) {
text-align: left;
background-color: rgba(0,0,0,0.02);
}
================================================
FILE: cmd/static/css/normalize.css
================================================
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
/* Document
========================================================================== */
/**
* 1. Correct the line height in all browsers.
* 2. Prevent adjustments of font size after orientation changes in iOS.
*/
html {
line-height: 1.15; /* 1 */
-webkit-text-size-adjust: 100%; /* 2 */
}
/* Sections
========================================================================== */
/**
* Remove the margin in all browsers.
*/
body {
margin: 0;
}
/**
* Render the `main` element consistently in IE.
*/
main {
display: block;
}
/**
* Correct the font size and margin on `h1` elements within `section` and
* `article` contexts in Chrome, Firefox, and Safari.
*/
h1 {
font-size: 2em;
margin: 0.67em 0;
}
/* Grouping content
========================================================================== */
/**
* 1. Add the correct box sizing in Firefox.
* 2. Show the overflow in Edge and IE.
*/
hr {
box-sizing: content-box; /* 1 */
height: 0; /* 1 */
overflow: visible; /* 2 */
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
pre {
font-family: monospace, monospace; /* 1 */
font-size: 1em; /* 2 */
}
/* Text-level semantics
========================================================================== */
/**
* Remove the gray background on active links in IE 10.
*/
a {
background-color: transparent;
}
/**
* 1. Remove the bottom border in Chrome 57-
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
*/
abbr[title] {
border-bottom: none; /* 1 */
text-decoration: underline; /* 2 */
text-decoration: underline dotted; /* 2 */
}
/**
* Add the correct font weight in Chrome, Edge, and Safari.
*/
b,
strong {
font-weight: bolder;
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
code,
kbd,
samp {
font-family: monospace, monospace; /* 1 */
font-size: 1em; /* 2 */
}
/**
* Add the correct font size in all browsers.
*/
small {
font-size: 80%;
}
/**
* Prevent `sub` and `sup` elements from affecting the line height in
* all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
/* Embedded content
========================================================================== */
/**
* Remove the border on images inside links in IE 10.
*/
img {
border-style: none;
}
/* Forms
========================================================================== */
/**
* 1. Change the font styles in all browsers.
* 2. Remove the margin in Firefox and Safari.
*/
button,
input,
optgroup,
select,
textarea {
font-family: inherit; /* 1 */
font-size: 100%; /* 1 */
line-height: 1.15; /* 1 */
margin: 0; /* 2 */
}
/**
* Show the overflow in IE.
* 1. Show the overflow in Edge.
*/
button,
input { /* 1 */
overflow: visible;
}
/**
* Remove the inheritance of text transform in Edge, Firefox, and IE.
* 1. Remove the inheritance of text transform in Firefox.
*/
button,
select { /* 1 */
text-transform: none;
}
/**
* Correct the inability to style clickable types in iOS and Safari.
*/
button,
[type="button"],
[type="reset"],
[type="submit"] {
-webkit-appearance: button;
}
/**
* Remove the inner border and padding in Firefox.
*/
button::-moz-focus-inner,
[type="button"]::-moz-focus-inner,
[type="reset"]::-moz-focus-inner,
[type="submit"]::-moz-focus-inner {
border-style: none;
padding: 0;
}
/**
* Restore the focus styles unset by the previous rule.
*/
button:-moz-focusring,
[type="button"]:-moz-focusring,
[type="reset"]:-moz-focusring,
[type="submit"]:-moz-focusring {
outline: 1px dotted ButtonText;
}
/**
* Correct the padding in Firefox.
*/
fieldset {
padding: 0.35em 0.75em 0.625em;
}
/**
* 1. Correct the text wrapping in Edge and IE.
* 2. Correct the color inheritance from `fieldset` elements in IE.
* 3. Remove the padding so developers are not caught out when they zero out
* `fieldset` elements in all browsers.
*/
legend {
box-sizing: border-box; /* 1 */
color: inherit; /* 2 */
display: table; /* 1 */
max-width: 100%; /* 1 */
padding: 0; /* 3 */
white-space: normal; /* 1 */
}
/**
* Add the correct vertical alignment in Chrome, Firefox, and Opera.
*/
progress {
vertical-align: baseline;
}
/**
* Remove the default vertical scrollbar in IE 10+.
*/
textarea {
overflow: auto;
}
/**
* 1. Add the correct box sizing in IE 10.
* 2. Remove the padding in IE 10.
*/
[type="checkbox"],
[type="radio"] {
box-sizing: border-box; /* 1 */
padding: 0; /* 2 */
}
/**
* Correct the cursor style of increment and decrement buttons in Chrome.
*/
[type="number"]::-webkit-inner-spin-button,
[type="number"]::-webkit-outer-spin-button {
height: auto;
}
/**
* 1. Correct the odd appearance in Chrome and Safari.
* 2. Correct the outline style in Safari.
*/
[type="search"] {
-webkit-appearance: textfield; /* 1 */
outline-offset: -2px; /* 2 */
}
/**
* Remove the inner padding in Chrome and Safari on macOS.
*/
[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
/**
* 1. Correct the inability to style clickable types in iOS and Safari.
* 2. Change font properties to `inherit` in Safari.
*/
::-webkit-file-upload-button {
-webkit-appearance: button; /* 1 */
font: inherit; /* 2 */
}
/* Interactive
========================================================================== */
/*
* Add the correct display in Edge, IE 10+, and Firefox.
*/
details {
display: block;
}
/*
* Add the correct display in all browsers.
*/
summary {
display: list-item;
}
/* Misc
========================================================================== */
/**
* Add the correct display in IE 10+.
*/
template {
display: none;
}
/**
* Add the correct display in IE 10.
*/
[hidden] {
display: none;
}
================================================
FILE: cmd/static/index.html
================================================
<!doctype html>
<html class="no-js" lang="">
<head>
<meta charset="utf-8">
<title>Webgo - Sample</title>
<meta name="description" content="This is a sample app using Webgo">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta property="og:title" content="Webgo Sample">
<meta property="og:type" content="website">
<link rel="stylesheet" href="/static/css/normalize.css">
<link rel="stylesheet" href="/static/css/main.css">
<meta name="theme-color" content="#fafafa">
</head>
<body>
<section class="main">
<p align="center"><img
src="https://user-images.githubusercontent.com/1092882/60883564-20142380-a268-11e9-988a-d98fb639adc6.png"
alt="webgo gopher" style="width: 100%; max-width: 256px;" /></p>
<p class="tags" align="justify">
<a href="https://codecov.io/gh/bnkamalesh/webgo"><img
src="https://img.shields.io/codecov/c/github/bnkamalesh/webgo.svg" alt="coverage"></a>
<a href="https://goreportcard.com/report/github.com/naughtygopher/webgo"><img
src="https://goreportcard.com/badge/github.com/naughtygopher/webgo" alt=""></a>
<a href="https://codeclimate.com/github/bnkamalesh/webgo/maintainability"><img
src="https://api.codeclimate.com/v1/badges/85b3a55c3fa6b4c5338d/maintainability" alt=""></a>
<a href="http://godoc.org/github.com/naughtygopher/webgo"><img
src="https://godoc.org/github.com/nathany/looper?status.svg" alt=""></a>
<a href="https://github.com/avelino/awesome-go#web-frameworks"><img
src="https://awesome.re/mentioned-badge.svg" alt=""></a>
</p>
<h1 align="center">WebGo</h1>
<p class="sse-container">
<table>
<tr>
<td><a
href="https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events">SSE</a> Client ID</td>
<td><span id="sse-client-id"></span></td>
</tr>
<tr>
<td><a
href="https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events">SSE</a> data
</td>
<td><span id="sse"></span></td>
</tr>
<tr>
<td>Active clients</td>
<td><span id="sse-clients"></span></td>
</tr>
</table>
</p>
</section>
<script src="/static/js/main.js"></script>
</body>
</html>
================================================
FILE: cmd/static/js/main.js
================================================
const webgo = async () => {
const clientID = Math.random()
.toString(36)
.replace(/[^a-z]+/g, "")
.substring(0, 16);
const sseDOM = document.getElementById("sse");
const sseClientsDOM = document.getElementById("sse-clients");
const sseClientIDDOM = document.getElementById("sse-client-id");
const formatBackoff = (backoff, precision = 2) => {
let boff = `${backoff}ms`;
if (backoff >= 1000) {
boff = `${parseFloat(backoff / 1000).toFixed(precision)}s`;
}
return boff;
};
const config = {
url: `/sse/${clientID}`,
onMessage: (data) => {
const parts = data?.split?.("(");
if (!parts || !parts.length) {
return;
}
const date = new Date(parts[0]);
const activeClients = parts[1].replace(")", "");
sseDOM.innerText = date.toLocaleString();
sseClientsDOM.innerText = activeClients;
sseClientIDDOM.innerText = clientID;
},
onError: (err, { backoff }) => {
sseClientsDOM.innerText = "N/A";
let interval = null;
interval = window.setInterval(() => {
sseDOM.innerHTML = `SSE failed, attempting reconnect in <strong>${formatBackoff(
backoff,
0
)}</strong>`;
backoff -= 1000;
if (backoff < 0) {
sseDOM.innerHTML = `SSE failed, attempting reconnect in <strong>0s</strong>`;
window.clearInterval(interval);
}
}, 1000);
console.log(err);
},
initialBackoff: 1000,
backoffStep: 1000,
};
const sseworker = new Worker("/static/js/sse.js");
sseworker.onerror = (e) => {
sseworker.terminate();
};
sseworker.onmessage = (e) => {
if (e?.data?.error) {
config.onError("SSE failed", e?.data);
} else {
config.onMessage(e?.data);
}
};
sseworker.postMessage({
url: config.url,
initialBackoff: config.initialBackoff,
backoffStep: config.backoffStep,
});
};
webgo();
================================================
FILE: cmd/static/js/sse.js
================================================
const sse = (url, config = {}) => {
const {
onMessage,
onError,
initialBackoff = 10, // milliseconds
maxBackoff = 15 * 1000, // 15 seconds
backoffStep = 50, // milliseconds
} = config;
let backoff = initialBackoff,
sseRetryTimeout = null;
const start = () => {
const source = new EventSource(url);
const configState = { initialBackoff, maxBackoff, backoffStep, backoff };
source.onopen = () => {
clearTimeout(sseRetryTimeout);
// reset backoff to initial, so further failures will again start with initial backoff
// instead of previous duration
backoff = initialBackoff;
configState.backoff = backoff;
};
source.onmessage = (event) => {
onMessage && onMessage(event, configState);
};
source.onerror = (err) => {
source.close();
if (!backoffStep) {
onError && onError(err, configState);
return;
}
clearTimeout(sseRetryTimeout);
// reattempt connecting with *linear* backoff
sseRetryTimeout = self.setTimeout(() => {
start(url, onMessage);
if (backoff < maxBackoff) {
backoff += backoffStep;
if (backoff > maxBackoff) {
backoff = maxBackoff;
}
}
}, backoff);
onError && onError(err, configState);
};
};
return start;
};
onmessage = (e) => {
sse(e?.data?.url, {
onMessage: (event) => {
postMessage(event?.data);
},
onError: (err, attrs) => {
postMessage({ error: "SSE failed", ...attrs });
},
})();
};
================================================
FILE: config.go
================================================
package webgo
import (
"encoding/json"
"os"
"strconv"
"time"
)
// Config is used for reading app's configuration from json file
type Config struct {
// Host is the host on which the server is listening
Host string `json:"host,omitempty"`
// Port is the port number where the server has to listen for the HTTP requests
Port string `json:"port,omitempty"`
// CertFile is the TLS/SSL certificate file path, required for HTTPS
CertFile string `json:"certFile,omitempty"`
// KeyFile is the filepath of private key of the certificate
KeyFile string `json:"keyFile,omitempty"`
// HTTPSPort is the port number where the server has to listen for the HTTP requests
HTTPSPort string `json:"httpsPort,omitempty"`
// ReadTimeout is the maximum duration for which the server would read a request
ReadTimeout time.Duration `json:"readTimeout,omitempty"`
// WriteTimeout is the maximum duration for which the server would try to respond
WriteTimeout time.Duration `json:"writeTimeout,omitempty"`
// InsecureSkipVerify is the HTTP certificate verification
InsecureSkipVerify bool `json:"insecureSkipVerify,omitempty"`
// ShutdownTimeout is the duration in which graceful shutdown is completed
ShutdownTimeout time.Duration
// ReverseMiddleware if true, will reverse the order of execution middleware
// from the order of it was added. e.g. router.Use(m1,m2), m2 will execute first
// if ReverseMiddleware is true
ReverseMiddleware bool
}
// Load config file from the provided filepath and validate
func (cfg *Config) Load(filepath string) {
file, err := os.ReadFile(filepath)
if err != nil {
LOGHANDLER.Fatal(err)
}
err = json.Unmarshal(file, cfg)
if err != nil {
LOGHANDLER.Fatal(err)
}
err = cfg.Validate()
if err != nil {
LOGHANDLER.Fatal(ErrInvalidPort)
}
}
// Validate the config parsed into the Config struct
func (cfg *Config) Validate() error {
i, err := strconv.Atoi(cfg.Port)
if err != nil {
return ErrInvalidPort
}
if i <= 0 || i > 65535 {
return ErrInvalidPort
}
return nil
}
================================================
FILE: config_test.go
================================================
package webgo
import (
"bytes"
"testing"
"time"
)
func TestConfig_LoadInvalid(t *testing.T) {
t.Parallel()
tl := &testLogger{
out: bytes.Buffer{},
}
LOGHANDLER = tl
cfg := &Config{}
cfg.Load("")
str := tl.out.String()
want := "open : no such file or directoryunexpected end of JSON inputport number not provided or is invalid (should be between 0 - 65535)"
got := str
if got != want {
t.Errorf(
"Expected '%s', got '%s'",
want,
got,
)
}
tl.out.Reset()
}
func TestConfig_LoadValid(t *testing.T) {
t.Parallel()
cfg := Config{}
cfg.Load("tests/config.json")
cfg.Port = "a"
if cfg.Validate() != ErrInvalidPort {
t.Error("Port validation failed")
}
cfg.Port = "65536"
if cfg.Validate() != ErrInvalidPort {
t.Error("Port validation failed")
}
}
func TestConfig_Validate(t *testing.T) {
t.Parallel()
type fields struct {
Host string
Port string
CertFile string
KeyFile string
HTTPSPort string
ReadTimeout time.Duration
WriteTimeout time.Duration
InsecureSkipVerify bool
ShutdownTimeout time.Duration
}
tests := []struct {
name string
fields fields
wantErr bool
}{
{
name: "invalid port",
fields: fields{
Port: "-12",
},
wantErr: true,
},
{
name: "valid port",
fields: fields{
Port: "9000",
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := &Config{
Host: tt.fields.Host,
Port: tt.fields.Port,
CertFile: tt.fields.CertFile,
KeyFile: tt.fields.KeyFile,
HTTPSPort: tt.fields.HTTPSPort,
ReadTimeout: tt.fields.ReadTimeout,
WriteTimeout: tt.fields.WriteTimeout,
InsecureSkipVerify: tt.fields.InsecureSkipVerify,
ShutdownTimeout: tt.fields.ShutdownTimeout,
}
if err := cfg.Validate(); (err != nil) != tt.wantErr {
t.Errorf("Config.Validate() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
================================================
FILE: errors.go
================================================
package webgo
import (
"errors"
"io"
"log"
"os"
)
var (
// ErrInvalidPort is the error returned when the port number provided in the config file is invalid
ErrInvalidPort = errors.New("port number not provided or is invalid (should be between 0 - 65535)")
lh *logHandler
)
type logCfg string
const (
// LogCfgDisableDebug is used to disable debug logs
LogCfgDisableDebug = logCfg("disable-debug")
// LogCfgDisableInfo is used to disable info logs
LogCfgDisableInfo = logCfg("disable-info")
// LogCfgDisableWarn is used to disable warning logs
LogCfgDisableWarn = logCfg("disable-warn")
// LogCfgDisableError is used to disable error logs
LogCfgDisableError = logCfg("disable-err")
// LogCfgDisableFatal is used to disable fatal logs
LogCfgDisableFatal = logCfg("disable-fatal")
)
// Logger defines all the logging methods to be implemented
type Logger interface {
Debug(data ...interface{})
Info(data ...interface{})
Warn(data ...interface{})
Error(data ...interface{})
Fatal(data ...interface{})
}
// logHandler has all the log writer handlers
type logHandler struct {
debug *log.Logger
info *log.Logger
warn *log.Logger
err *log.Logger
fatal *log.Logger
}
// Debug prints log of severity 5
func (lh *logHandler) Debug(data ...interface{}) {
if lh.debug == nil {
return
}
lh.debug.Println(data...)
}
// Info prints logs of severity 4
func (lh *logHandler) Info(data ...interface{}) {
if lh.info == nil {
return
}
lh.info.Println(data...)
}
// Warn prints log of severity 3
func (lh *logHandler) Warn(data ...interface{}) {
if lh.warn == nil {
return
}
lh.warn.Println(data...)
}
// Error prints log of severity 2
func (lh *logHandler) Error(data ...interface{}) {
if lh.err == nil {
return
}
lh.err.Println(data...)
}
// Fatal prints log of severity 1
func (lh *logHandler) Fatal(data ...interface{}) {
if lh.fatal == nil {
return
}
lh.fatal.Fatalln(data...)
}
// LOGHANDLER is a global variable which webgo uses to log messages
var LOGHANDLER Logger
func init() {
GlobalLoggerConfig(nil, nil)
}
func loggerWithCfg(stdout io.Writer, stderr io.Writer, cfgs ...logCfg) *logHandler {
lh = &logHandler{
debug: log.New(stdout, "Debug ", log.LstdFlags),
info: log.New(stdout, "Info ", log.LstdFlags),
warn: log.New(stderr, "Warning ", log.LstdFlags),
err: log.New(stderr, "Error ", log.LstdFlags),
fatal: log.New(stderr, "Fatal ", log.LstdFlags|log.Llongfile),
}
for _, c := range cfgs {
switch c {
case LogCfgDisableDebug:
{
lh.debug = nil
}
case LogCfgDisableInfo:
{
lh.info = nil
}
case LogCfgDisableWarn:
{
lh.warn = nil
}
case LogCfgDisableError:
{
lh.err = nil
}
case LogCfgDisableFatal:
{
lh.fatal = nil
}
}
}
return lh
}
// GlobalLoggerConfig is used to configure the global/default logger of webgo
// IMPORTANT: This is not concurrent safe
func GlobalLoggerConfig(stdout io.Writer, stderr io.Writer, cfgs ...logCfg) {
if stdout == nil {
stdout = os.Stdout
}
if stderr == nil {
stderr = os.Stderr
}
LOGHANDLER = loggerWithCfg(stdout, stderr, cfgs...)
}
================================================
FILE: errors_test.go
================================================
package webgo
import (
"testing"
)
func Test_loggerWithCfg(t *testing.T) {
t.Parallel()
cfgs := []logCfg{
LogCfgDisableDebug,
LogCfgDisableInfo,
LogCfgDisableWarn,
LogCfgDisableError,
LogCfgDisableFatal,
}
l := loggerWithCfg(nil, nil, cfgs...)
if l.debug != nil {
t.Errorf("expected debug to be nil, got %v", l.debug)
}
if l.err != nil {
t.Errorf("expected err to be nil, got %v", l.err)
}
if l.fatal != nil {
t.Errorf("expected fatal to be nil, got %v", l.fatal)
}
if l.info != nil {
t.Errorf("expected info to be nil, got %v", l.info)
}
if l.warn != nil {
t.Errorf("expected warn to be nil, got %v", l.warn)
}
}
================================================
FILE: extensions/sse/README.md
================================================
# Server-Sent Events
This extension provides support for [Server-Sent](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events) Events for any net/http compliant http server.
It provides the following hooks for customizing the workflows:
1. `OnCreateClient func(ctx context.Context, client *Client, count int)`
2. `OnRemoveClient func(ctx context.Context, clientID string, count int)`
3. `OnSend func(ctx context.Context, client *Client, err error)`
4. `BeforeSend func(ctx context.Context, client *Client)`
```golang
import (
"github.com/naughtygopher/webgo/extensions/sse"
)
func main() {
sseService := sse.New()
// broadcast to all active clients
sseService.Broadcast(Message{
Data: "Hello world",
Retry: time.MilliSecond,
})
// You can replace the ClientManager with your custom implementation, and override the default one
// sseService.Clients = <your custom client manager>
// send message to an individual client
clientID := "cli123"
cli := sseService.Client(clientID)
if cli != nil {
cli.Message <- &Message{Data: fmt.Sprintf("Hello %s",clientID), Retry: time.MilliSecond }
}
}
```
## Client Manager
Client manager is an interface which is required for SSE to function, since this is an interface it's easier for you to replace if required. The default implementation is a simple one using mutex. If you have a custom implementation which is faster/better, you can easily swap out the default one.
```golang
type ClientManager interface {
// New should return a new client, and the total number of active clients after adding this new one
New(ctx context.Context, w http.ResponseWriter, clientID string) (*Client, int)
// Range should iterate through all the active clients
Range(func(*Client))
// Remove should remove the active client given a clientID, and close the connection
Remove(clientID string) int
// Active returns the number of active clients
Active() int
// Clients returns a list of all active clients
Clients() []*Client
// Client returns *Client if clientID is active
Client(clientID string) *Client
}
```
================================================
FILE: extensions/sse/client.go
================================================
package sse
import (
"context"
"net/http"
)
type ClientManager interface {
// New should return a new client, and the total number of active clients after adding this new one
New(ctx context.Context, w http.ResponseWriter, clientID string) (*Client, int)
// Range should iterate through all the active clients
Range(func(*Client))
// Remove should remove the active client given a clientID, and close the connection
Remove(clientID string) int
// Active returns the number of active clients
Active() int
// Clients returns a list of all active clients
Clients() []*Client
// Client returns *Client if clientID is active
Client(clientID string) *Client
}
type Client struct {
ID string
Msg chan *Message
ResponseWriter http.ResponseWriter
Ctx context.Context
}
type eventType int
const (
eTypeNewClient eventType = iota
eTypeClientList
eTypeRemoveClient
eTypeActiveClientCount
eTypeClient
)
func (et eventType) String() string {
switch et {
case eTypeNewClient:
return "new_client"
case eTypeClientList:
return "client_list"
case eTypeRemoveClient:
return "remove_client"
case eTypeActiveClientCount:
return "active_client_count"
}
return "unknown"
}
type event struct {
Type eventType
ClientID string
Client *Client
Response chan *eventResponse
}
type eventResponse struct {
Clients []*Client
RemainingClients int
Client *Client
}
type Clients struct {
clients map[string]*Client
MsgBuffer int
events chan<- event
}
func (cs *Clients) listener(events <-chan event) {
for ev := range events {
switch ev.Type {
case eTypeNewClient:
cs.clients[ev.Client.ID] = ev.Client
case eTypeClientList:
copied := make([]*Client, 0, len(cs.clients))
for clientID := range cs.clients {
copied = append(copied, cs.clients[clientID])
}
ev.Response <- &eventResponse{
Clients: copied,
}
case eTypeRemoveClient:
cli := cs.clients[ev.ClientID]
if cli == nil {
ev.Response <- nil
continue
}
// Ctx.Done() is needed to close its streaming handler
cli.Ctx.Done()
delete(cs.clients, ev.ClientID)
ev.Response <- nil
case eTypeClient:
ev.Response <- &eventResponse{
Client: cs.clients[ev.ClientID],
}
}
}
}
func (cs *Clients) New(ctx context.Context, w http.ResponseWriter, clientID string) (*Client, int) {
mchan := make(chan *Message, cs.MsgBuffer)
cli := &Client{
ID: clientID,
Msg: mchan,
ResponseWriter: w,
Ctx: ctx,
}
cs.events <- event{
Type: eTypeNewClient,
Client: cli,
}
return cli, len(cs.clients)
}
func (cs *Clients) Range(f func(cli *Client)) {
rch := make(chan *eventResponse)
cs.events <- event{
Type: eTypeClientList,
Response: rch,
}
response := <-rch
for i := range response.Clients {
f(response.Clients[i])
}
}
func (cs *Clients) Remove(clientID string) int {
rch := make(chan *eventResponse)
cs.events <- event{
Type: eTypeRemoveClient,
ClientID: clientID,
Response: rch,
}
<-rch
return len(cs.clients)
}
func (cs *Clients) Active() int {
return len(cs.clients)
}
// MessageChannels returns a slice of message channels of all clients
// which you can then use to send message concurrently
func (cs *Clients) Clients() []*Client {
rch := make(chan *eventResponse)
cs.events <- event{
Type: eTypeClientList,
Response: rch,
}
response := <-rch
return response.Clients
}
func (cs *Clients) Client(clientID string) *Client {
rch := make(chan *eventResponse)
cs.events <- event{
Type: eTypeClientList,
Response: rch,
}
cli := <-rch
return cli.Client
}
func NewClientManager() ClientManager {
const buffer = 10
events := make(chan event, buffer)
cli := &Clients{
clients: make(map[string]*Client),
events: events,
MsgBuffer: buffer,
}
go cli.listener(events)
return cli
}
================================================
FILE: extensions/sse/message.go
================================================
package sse
import (
"bytes"
"net/http"
"strconv"
"time"
)
// Message represents a valid SSE message
// ref: https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events
type Message struct {
// Event is a string identifying the type of event described. If this is specified, an event will be dispatched on the browser to the listener for the specified event name; the website source code should use addEventListener() to listen for named events. The onmessage handler is called if no event name is specified for a message.
Event string
// Data field for the message. When the EventSource receives multiple consecutive lines that begin with data:, it concatenates them, inserting a newline character between each one. Trailing newlines are removed.
Data string
// ID to set the EventSource object's last event ID value.
ID string
// Retry is the reconnection time. If the connection to the server is lost, the browser will wait for the specified time before attempting to reconnect. This must be an integer, specifying the reconnection time in milliseconds. If a non-integer value is specified, the field is ignored.
Retry time.Duration
}
func (m *Message) Bytes() []byte {
// The event stream is a simple stream of text data which must be encoded using UTF-8.
// Messages in the event stream are separated by a pair of newline characters.
// A colon as the first character of a line is in essence a comment, and is ignored.
buff := bytes.NewBufferString("")
if m.Event != "" {
buff.WriteString("event:" + m.Event + "\n")
}
if m.ID != "" {
buff.WriteString("id:" + m.ID + "\n")
}
if m.Data != "" {
buff.WriteString("data:" + m.Data + "\n")
}
if m.Retry != 0 {
buff.WriteString("retry:" + strconv.Itoa(int(m.Retry.Milliseconds())) + "\n")
}
buff.WriteString("\n")
return buff.Bytes()
}
func DefaultUnsupportedMessageHandler(w http.ResponseWriter, r *http.Request) error {
w.WriteHeader(http.StatusNotImplemented)
_, err := w.Write([]byte("Streaming not supported"))
return err
}
================================================
FILE: extensions/sse/sse.go
================================================
// Package sse implements Server-Sent Events(SSE)
// This extension is compliant with any net/http implementation, and is not limited to WebGo.
package sse
import (
"context"
"net/http"
)
var closeMessage = (&Message{
Data: `{"error":"close"}`,
}).Bytes()
type SSE struct {
// ClientIDHeader is the HTTP request header in which the client ID is set. Default is `sse-clientid`
ClientIDHeader string
// UnsupportedMessage is used to send the error response to client if the
// server doesn't support SSE
UnsupportedMessage func(http.ResponseWriter, *http.Request) error
// OnCreateClient is a hook, for when a client is added to the active clients. count is the number
// of active clients after adding the latest client
OnCreateClient func(ctx context.Context, client *Client, count int)
// OnRemoveClient is a hook, for when a client is removed from the active clients. count is the number
// of active clients after removing a client
OnRemoveClient func(ctx context.Context, clientID string, count int)
// OnSend is a hook, which is called *after* a message is sent to a client
OnSend func(ctx context.Context, client *Client, err error)
// BeforeSend is a hook, which is called before starting to listen for messages to send
BeforeSend func(ctx context.Context, client *Client)
Clients ClientManager
}
// Handler returns an error rather than being directly used as an http.HandlerFunc,
// to let the user handle error. e.g. if the error has to be logged
func (sse *SSE) Handler(w http.ResponseWriter, r *http.Request) error {
flusher, hasFlusher := w.(http.Flusher)
if !hasFlusher {
return sse.UnsupportedMessage(w, r)
}
header := w.Header()
header.Set("Content-Type", "text/event-stream")
header.Set("Connection", "keep-alive")
header.Set("X-Accel-Buffering", "no")
w.WriteHeader(http.StatusOK)
ctx := r.Context()
clientID := r.Header.Get(sse.ClientIDHeader)
client := sse.NewClient(ctx, w, clientID)
defer func() {
w.WriteHeader(http.StatusNoContent)
sse.RemoveClient(ctx, clientID)
}()
sse.BeforeSend(ctx, client)
for {
select {
case payload, ok := <-client.Msg:
if !ok {
return nil
}
_, err := w.Write(payload.Bytes())
sse.OnSend(ctx, client, err)
if err != nil {
return err
}
case <-ctx.Done():
{
_, err := w.Write(closeMessage)
sse.OnSend(ctx, client, err)
return err
}
}
flusher.Flush()
}
}
// HandlerFunc is a convenience function which can be directly used with net/http implementations.
// Important: You cannot handle any error returned by the Handler
func (sse *SSE) HandlerFunc(w http.ResponseWriter, r *http.Request) {
_ = sse.Handler(w, r)
}
// Broadcast sends the message to all active clients
func (sse *SSE) Broadcast(msg Message) {
sse.Clients.Range(func(cli *Client) {
cli.Msg <- &msg
})
}
func (sse *SSE) NewClient(ctx context.Context, w http.ResponseWriter, clientID string) *Client {
cli, count := sse.Clients.New(ctx, w, clientID)
sse.OnCreateClient(ctx, cli, count)
return cli
}
func (sse *SSE) ActiveClients() int {
return sse.Clients.Active()
}
func (sse *SSE) RemoveClient(ctx context.Context, clientID string) {
sse.OnRemoveClient(
ctx,
clientID,
sse.Clients.Remove(clientID),
)
}
func (sse *SSE) Client(id string) *Client {
return sse.Clients.Client(id)
}
func DefaultCreateHook(ctx context.Context, client *Client, count int) {}
func DefaultRemoveHook(ctx context.Context, clientID string, count int) {}
func DefaultOnSend(ctx context.Context, client *Client, err error) {}
func DefaultBeforeSend(ctx context.Context, client *Client) {}
func New() *SSE {
s := &SSE{
ClientIDHeader: "sse-clientid",
Clients: NewClientManager(),
UnsupportedMessage: DefaultUnsupportedMessageHandler,
OnRemoveClient: DefaultRemoveHook,
OnCreateClient: DefaultCreateHook,
OnSend: DefaultOnSend,
BeforeSend: DefaultBeforeSend,
}
return s
}
================================================
FILE: go.mod
================================================
module github.com/naughtygopher/webgo/v7
go 1.22
================================================
FILE: go.sum
================================================
================================================
FILE: middleware/accesslog/accesslog.go
================================================
/*
Package accesslogs provides a simple straight forward access log middleware. The logs are of the
following format:
<timestamp> <HTTP request method> <full URL including query string parameters> <duration of execution> <HTTP response status code>
*/
package accesslog
import (
"fmt"
"net/http"
"time"
"github.com/naughtygopher/webgo/v7"
)
// AccessLog is a middleware which prints access log to stdout
func AccessLog(rw http.ResponseWriter, req *http.Request, next http.HandlerFunc) {
start := time.Now()
next(rw, req)
end := time.Now()
webgo.LOGHANDLER.Info(
fmt.Sprintf(
"%s %s %s %d",
req.Method,
req.URL.String(),
end.Sub(start).String(),
webgo.ResponseStatus(rw),
),
)
}
================================================
FILE: middleware/accesslog/accesslog_test.go
================================================
/*
Package accesslogs provides a simple straight forward access log middleware. The logs are of the
following format:
<timestamp> <HTTP request method> <full URL including query string parameters> <duration of execution> <HTTP response status code>
*/
package accesslog
import (
"bytes"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/naughtygopher/webgo/v7"
)
func TestAccessLog(t *testing.T) {
stdout := bytes.NewBuffer([]byte(``))
stderr := bytes.NewBuffer([]byte(``))
webgo.GlobalLoggerConfig(stdout, stderr)
port := "9696"
router, err := setup(port)
if err != nil {
t.Error(err.Error())
return
}
router.Use(AccessLog)
router.SetupMiddleware()
url := fmt.Sprintf("http://localhost:%s/hello", port)
w := httptest.NewRecorder()
req := httptest.NewRequest(
http.MethodGet,
url,
nil,
)
router.ServeHTTP(w, req)
parts := strings.Split(stdout.String(), " ")
if len(parts) != 7 {
t.Errorf(
"Expected log to have %d parts, got %d",
7,
len(parts),
)
return
}
if parts[0] != "Info" {
t.Errorf("expected log type 'Info', got '%s'", parts[0])
}
if parts[3] != http.MethodGet {
t.Errorf("expected HTTP method %s, got %s", http.MethodGet, parts[3])
}
if parts[4] != url {
t.Errorf("expected HTTP full URL '%s', got '%s'", url, parts[4])
}
if parts[6][0:3] != "200" {
t.Errorf("expected HTTP status code '%d', got '%s'", http.StatusOK, parts[6][0:3])
}
}
func handler(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte(`hello`))
}
func setup(port string) (*webgo.Router, error) {
cfg := &webgo.Config{
Port: "9696",
ReadTimeout: time.Second * 1,
WriteTimeout: time.Second * 1,
ShutdownTimeout: time.Second * 10,
CertFile: "tests/ssl/server.crt",
KeyFile: "tests/ssl/server.key",
}
router := webgo.NewRouter(cfg, &webgo.Route{
Name: "hello",
Pattern: "/hello",
Method: http.MethodGet,
Handlers: []http.HandlerFunc{handler},
})
return router, nil
}
================================================
FILE: middleware/cors/cors.go
================================================
/*
Package cors sets the appropriate CORS(https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS)
response headers, and lets you customize. Following customizations are allowed:
- provide a list of allowed domains
- provide a list of headers
- set the max-age of CORS headers
The list of allowed methods are
*/
package cors
import (
"fmt"
"net/http"
"regexp"
"sort"
"strings"
"github.com/naughtygopher/webgo/v7"
)
const (
headerOrigin = "Access-Control-Allow-Origin"
headerMethods = "Access-Control-Allow-Methods"
headerCreds = "Access-Control-Allow-Credentials"
headerAllowHeaders = "Access-Control-Allow-Headers"
headerReqHeaders = "Access-Control-Request-Headers"
headerAccessControlAge = "Access-Control-Max-Age"
allowHeaders = "Accept,Content-Type,Content-Length,Accept-Encoding,Access-Control-Request-Headers,"
)
var (
defaultAllowMethods = "HEAD,GET,POST,PUT,PATCH,DELETE,OPTIONS"
)
func allowedDomains() []string {
// The domains mentioned here are default
domains := []string{"*"}
return domains
}
func getReqOrigin(r *http.Request) string {
return r.Header.Get("Origin")
}
func allowedOriginsRegex(allowedOrigins ...string) []regexp.Regexp {
if len(allowedOrigins) == 0 {
allowedOrigins = []string{"*"}
} else {
// If "*" is one of the allowed domains, i.e. all domains, then rest of the values are ignored
for _, val := range allowedOrigins {
val = strings.TrimSpace(val)
if val == "*" {
allowedOrigins = []string{"*"}
break
}
}
}
allowedOriginRegex := make([]regexp.Regexp, 0, len(allowedOrigins))
for _, ao := range allowedOrigins {
parts := strings.Split(ao, ":")
str := strings.TrimSpace(parts[0])
if str == "" {
continue
}
if str == "*" {
allowedOriginRegex = append(
allowedOriginRegex,
*(regexp.MustCompile(".+")),
)
break
}
regStr := fmt.Sprintf(`^(http)?(https)?(:\/\/)?(.+\.)?%s(:[0-9]+)?$`, str)
allowedOriginRegex = append(
allowedOriginRegex,
// Allow any port number of the specified domain
*(regexp.MustCompile(regStr)),
)
}
return allowedOriginRegex
}
func allowedMethods(routes []*webgo.Route) string {
if len(routes) == 0 {
return defaultAllowMethods
}
methods := make([]string, 0, len(routes))
for _, r := range routes {
found := false
for _, m := range methods {
if m == r.Method {
found = true
break
}
}
if found {
continue
}
methods = append(methods, r.Method)
}
sort.Strings(methods)
return strings.Join(methods, ",")
}
// Config holds all the configurations which is available for customizing this middleware
type Config struct {
TimeoutSecs int
Routes []*webgo.Route
AllowedOrigins []string
AllowedHeaders []string
}
func allowedHeaders(headers []string) string {
if len(headers) == 0 {
return allowHeaders
}
allowedHeaders := strings.Join(headers, ",")
if allowedHeaders[len(allowedHeaders)-1] != ',' {
allowedHeaders += ","
}
return allowedHeaders
}
func allowOrigin(reqOrigin string, allowedOriginRegex []regexp.Regexp) bool {
for _, o := range allowedOriginRegex {
// Set appropriate response headers required for CORS
if o.MatchString(reqOrigin) || reqOrigin == "" {
return true
}
}
return false
}
// Middleware can be used as well, it lets the user use this middleware without webgo
func Middleware(allowedOriginRegex []regexp.Regexp, corsTimeout, allowedMethods, allowedHeaders string) webgo.Middleware {
return func(rw http.ResponseWriter, req *http.Request, next http.HandlerFunc) {
reqOrigin := getReqOrigin(req)
allowed := allowOrigin(reqOrigin, allowedOriginRegex)
if !allowed {
// If CORS failed, no respective headers are set. But the execution is allowed to continue
// Earlier this middleware blocked access altogether, which was considered an added
// security measure despite it being outside the scope of this middelware. Though, such
// restrictions create unnecessary complexities during inter-app communication.
next(rw, req)
return
}
// Set appropriate response headers required for CORS
rw.Header().Set(headerOrigin, reqOrigin)
rw.Header().Set(headerAccessControlAge, corsTimeout)
rw.Header().Set(headerCreds, "true")
rw.Header().Set(headerMethods, allowedMethods)
rw.Header().Set(headerAllowHeaders, allowedHeaders+req.Header.Get(headerReqHeaders))
if req.Method == http.MethodOptions {
webgo.SendHeader(rw, http.StatusOK)
return
}
next(rw, req)
}
}
// AddOptionsHandlers appends OPTIONS handler for all the routes
// The response body would be empty for all the new handlers added
func AddOptionsHandlers(routes []*webgo.Route) []*webgo.Route {
dummyHandler := func(w http.ResponseWriter, r *http.Request) {}
if len(routes) == 0 {
return []*webgo.Route{
{
Name: "cors",
Pattern: "/:w*",
Method: http.MethodOptions,
TrailingSlash: true,
Handlers: []http.HandlerFunc{dummyHandler},
},
}
}
list := make([]*webgo.Route, 0, len(routes))
list = append(list, routes...)
for _, r := range routes {
list = append(list, &webgo.Route{
Name: fmt.Sprintf("%s-CORS", r.Name),
Method: http.MethodOptions,
Pattern: r.Pattern,
TrailingSlash: true,
Handlers: []http.HandlerFunc{dummyHandler},
})
}
return list
}
// CORS is a single CORS middleware which can be applied to the whole app at once
func CORS(cfg *Config) webgo.Middleware {
if cfg == nil {
cfg = new(Config)
// 30 minutes
cfg.TimeoutSecs = 30 * 60
}
allowedOrigins := cfg.AllowedOrigins
if len(allowedOrigins) == 0 {
allowedOrigins = allowedDomains()
}
allowedOriginRegex := allowedOriginsRegex(allowedOrigins...)
allowedmethods := allowedMethods(cfg.Routes)
allowedHeaders := allowedHeaders(cfg.AllowedHeaders)
corsTimeout := fmt.Sprintf("%d", cfg.TimeoutSecs)
return Middleware(
allowedOriginRegex,
corsTimeout,
allowedmethods,
allowedHeaders,
)
}
================================================
FILE: middleware/cors/cors_test.go
================================================
/*
Package cors sets the appropriate CORS(https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS)
response headers, and lets you customize. Following customizations are allowed:
- provide a list of allowed domains
- provide a list of headers
- set the max-age of CORS headers
The list of allowed methods are
*/
package cors
import (
"fmt"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/naughtygopher/webgo/v7"
)
func TestCORSEmptyconfig(t *testing.T) {
port := "9696"
routes := getRoutes()
routes = append(routes, AddOptionsHandlers(nil)...)
router, err := setup(port, routes)
if err != nil {
t.Error(err.Error())
return
}
router.Use(CORS(&Config{TimeoutSecs: 50}))
router.SetupMiddleware()
url := fmt.Sprintf("http://localhost:%s/hello", port)
w := httptest.NewRecorder()
req := httptest.NewRequest(
http.MethodGet,
url,
nil,
)
router.ServeHTTP(w, req)
body, _ := io.ReadAll(w.Body)
str := string(body)
if str != "hello" {
t.Errorf(
"Expected body '%s', got '%s'",
"hello",
str,
)
}
if w.Header().Get(headerMethods) != defaultAllowMethods {
t.Errorf(
"Expected header %s to be '%s', got '%s'",
headerMethods,
defaultAllowMethods,
w.Header().Get(headerMethods),
)
}
if w.Header().Get(headerCreds) != "true" {
t.Errorf(
"Expected header %s to be 'true', got '%s'",
headerCreds,
w.Header().Get(headerCreds),
)
}
if w.Header().Get(headerAccessControlAge) != "50" {
t.Errorf(
"Expected '%s' to be '50', got '%s'",
headerAccessControlAge,
w.Header().Get(headerAccessControlAge),
)
}
if w.Header().Get(headerAllowHeaders) != allowHeaders {
t.Errorf(
"Expected '%s' to be '%s', got '%s'",
headerAllowHeaders,
allowHeaders,
w.Header().Get(headerAllowHeaders),
)
}
// check OPTIONS method
w = httptest.NewRecorder()
req = httptest.NewRequest(
http.MethodOptions,
url,
nil,
)
router.ServeHTTP(w, req)
body, _ = io.ReadAll(w.Body)
str = string(body)
if str != "" {
t.Errorf(
"Expected empty body, got '%s'",
str,
)
}
}
func TestCORSWithConfig(t *testing.T) {
port := "9696"
routes := AddOptionsHandlers(getRoutes())
router, err := setup(port, routes)
if err != nil {
t.Error(err.Error())
return
}
cfg := &Config{
Routes: routes,
AllowedOrigins: []string{"example.com", fmt.Sprintf("localhost:%s", port)},
AllowedHeaders: []string{"x-custom"},
}
router.Use(CORS(cfg))
baseAPI := fmt.Sprintf("http://localhost:%s", port)
url := fmt.Sprintf("%s/hello", baseAPI)
w := httptest.NewRecorder()
req := httptest.NewRequest(
http.MethodGet,
url,
nil,
)
router.SetupMiddleware()
router.ServeHTTP(w, req)
if w.Header().Get(headerMethods) != "GET,OPTIONS" {
t.Errorf(
"Expected value for %s header is 'GET', got '%s'",
headerMethods,
w.Header().Get(headerMethods),
)
}
want := strings.Join(cfg.AllowedHeaders, ",") + ","
if w.Header().Get(headerAllowHeaders) != want {
t.Errorf(
"Expected value for %s header is '%s', got '%s'",
headerAllowHeaders,
want,
w.Header().Get(headerAllowHeaders),
)
}
// test OPTIONS request
w = httptest.NewRecorder()
req = httptest.NewRequest(
http.MethodOptions,
url,
nil,
)
req.Header.Set("Origin", "helloworld.com")
router.ServeHTTP(w, req)
body, _ := io.ReadAll(w.Body)
str := string(body)
if str != "" {
t.Errorf(
"Expected empty body, got '%s'",
str,
)
}
// since origin is set as "helloworld.com", which is not in the allowed list of origins
// CORS headers should NOT be set
if w.Header().Get(headerOrigin) != "" {
t.Errorf(
"Expected empty value for header '%s', got '%s'",
headerOrigin,
w.Header().Get(headerOrigin),
)
}
if w.Header().Get(headerAccessControlAge) != "" {
t.Errorf(
"Expected empty value for header '%s', got '%s'",
headerAccessControlAge,
w.Header().Get(headerAccessControlAge),
)
}
if w.Header().Get(headerCreds) != "" {
t.Errorf(
"Expected empty value for header '%s', got '%s'",
headerCreds,
w.Header().Get(headerCreds),
)
}
if w.Header().Get(headerMethods) != "" {
t.Errorf(
"Expected empty value for header '%s', got '%s'",
headerMethods,
w.Header().Get(headerMethods),
)
}
if w.Header().Get(headerAllowHeaders) != "" {
t.Errorf(
"Expected empty value for header '%s', got '%s'",
headerAllowHeaders,
w.Header().Get(headerAllowHeaders),
)
}
}
func handler(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte(`hello`))
}
func getRoutes() []*webgo.Route {
return []*webgo.Route{
{
Name: "hello",
Pattern: "/hello",
Method: http.MethodGet,
Handlers: []http.HandlerFunc{handler},
},
}
}
func setup(port string, routes []*webgo.Route) (*webgo.Router, error) {
cfg := &webgo.Config{
Port: "9696",
ReadTimeout: time.Second * 1,
WriteTimeout: time.Second * 1,
ShutdownTimeout: time.Second * 10,
CertFile: "tests/ssl/server.crt",
KeyFile: "tests/ssl/server.key",
}
router := webgo.NewRouter(cfg, routes...)
return router, nil
}
================================================
FILE: responses.go
================================================
package webgo
import (
"encoding/json"
"fmt"
"html/template"
"net/http"
)
var (
jsonErrPayload = []byte{}
)
// ErrorData used to render the error page
type ErrorData struct {
ErrCode int
ErrDescription string
}
// dOutput is the standard/valid output wrapped in `{data: <payload>, status: <http response status>}`
type dOutput struct {
Data interface{} `json:"data"`
Status int `json:"status"`
}
// errOutput is the error output wrapped in `{errors:<errors>, status: <http response status>}`
type errOutput struct {
Errors interface{} `json:"errors"`
Status int `json:"status"`
}
const (
// HeaderContentType is the key for mentioning the response header content type
HeaderContentType = "Content-Type"
// JSONContentType is the MIME type when the response is JSON
JSONContentType = "application/json"
// HTMLContentType is the MIME type when the response is HTML
HTMLContentType = "text/html; charset=UTF-8"
// ErrInternalServer to send when there's an internal server error
ErrInternalServer = "Internal server error"
)
// SendHeader is used to send only a response header, i.e no response body
func SendHeader(w http.ResponseWriter, rCode int) {
w.WriteHeader(rCode)
}
func crwAsserter(w http.ResponseWriter, rCode int) http.ResponseWriter {
if crw, ok := w.(*customResponseWriter); ok {
crw.statusCode = rCode
return crw
}
return newCRW(w, rCode)
}
// Send sends a completely custom response without wrapping in the
// `{data: <data>, status: <int>` struct
func Send(w http.ResponseWriter, contentType string, data interface{}, rCode int) {
w = crwAsserter(w, rCode)
w.Header().Set(HeaderContentType, contentType)
_, err := fmt.Fprint(w, data)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
_, _ = w.Write([]byte(ErrInternalServer))
LOGHANDLER.Error(err)
}
}
// SendResponse is used to respond to any request (JSON response) based on the code, data etc.
func SendResponse(w http.ResponseWriter, data interface{}, rCode int) {
w = crwAsserter(w, rCode)
w.Header().Add(HeaderContentType, JSONContentType)
err := json.NewEncoder(w).Encode(dOutput{Data: data, Status: rCode})
if err == nil {
return
}
// assuming the error was related to JSON encoding, so reattempting to respond
// with a static payload. This could still fail in case of network write or other error(s)
w = crwAsserter(w, http.StatusInternalServerError)
_, _ = w.Write(jsonErrPayload)
LOGHANDLER.Error(err)
}
// SendError is used to respond to any request with an error
func SendError(w http.ResponseWriter, data interface{}, rCode int) {
w = crwAsserter(w, rCode)
w.Header().Add(HeaderContentType, JSONContentType)
err := json.NewEncoder(w).Encode(errOutput{data, rCode})
if err == nil {
return
}
// assuming the error was related to JSON encoding, so reattempting to respond
// with a static payload. This could still fail in case of network write or other error(s)
w = crwAsserter(w, http.StatusInternalServerError)
_, _ = w.Write(jsonErrPayload)
LOGHANDLER.Error(err)
}
// Render is used for rendering templates (HTML)
func Render(w http.ResponseWriter, data interface{}, rCode int, tpl *template.Template) {
w = crwAsserter(w, rCode)
// In case of HTML response, setting appropriate header type for text/HTML response
w.Header().Set(HeaderContentType, HTMLContentType)
// Rendering an HTML template with appropriate data
err := tpl.Execute(w, data)
if err != nil {
Send(w, "text/plain", ErrInternalServer, http.StatusInternalServerError)
LOGHANDLER.Error(err.Error())
}
}
// R200 - Successful/OK response
func R200(w http.ResponseWriter, data interface{}) {
SendResponse(w, data, http.StatusOK)
}
// R201 - New item created
func R201(w http.ResponseWriter, data interface{}) {
SendResponse(w, data, http.StatusCreated)
}
// R204 - empty, no content
func R204(w http.ResponseWriter) {
SendHeader(w, http.StatusNoContent)
}
// R302 - Temporary redirect
func R302(w http.ResponseWriter, data interface{}) {
SendResponse(w, data, http.StatusFound)
}
// R400 - Invalid request, any incorrect/erraneous value in the request body
func R400(w http.ResponseWriter, data interface{}) {
SendError(w, data, http.StatusBadRequest)
}
// R403 - Unauthorized access
func R403(w http.ResponseWriter, data interface{}) {
SendError(w, data, http.StatusForbidden)
}
// R404 - Resource not found
func R404(w http.ResponseWriter, data interface{}) {
SendError(w, data, http.StatusNotFound)
}
// R406 - Unacceptable header. For any error related to values set in header
func R406(w http.ResponseWriter, data interface{}) {
SendError(w, data, http.StatusNotAcceptable)
}
// R451 - Resource taken down because of a legal request
func R451(w http.ResponseWriter, data interface{}) {
SendError(w, data, http.StatusUnavailableForLegalReasons)
}
// R500 - Internal server error
func R500(w http.ResponseWriter, data interface{}) {
SendError(w, data, http.StatusInternalServerError)
}
================================================
FILE: responses_test.go
================================================
package webgo
import (
"encoding/json"
"html/template"
"io"
"net/http"
"net/http/httptest"
"reflect"
"testing"
)
func TestSendHeader(t *testing.T) {
t.Parallel()
w := httptest.NewRecorder()
SendHeader(w, http.StatusNoContent)
if w.Result().StatusCode != http.StatusNoContent {
t.Errorf("Expected code '%d', got '%d'", http.StatusNoContent, w.Result().StatusCode)
}
}
func TestSendError(t *testing.T) {
t.Parallel()
w := httptest.NewRecorder()
payload := map[string]string{"message": "hello world"}
SendError(w, payload, http.StatusBadRequest)
resp := struct {
Errors map[string]string
}{}
body, err := io.ReadAll(w.Body)
if err != nil {
t.Error(err.Error())
return
}
err = json.Unmarshal(body, &resp)
if err != nil {
t.Error(err.Error())
return
}
if !reflect.DeepEqual(payload, resp.Errors) {
t.Errorf(
"Expected '%v', got '%v'. Raw response: '%s'",
payload,
resp.Errors,
string(body),
)
}
if w.Result().StatusCode != http.StatusBadRequest {
t.Errorf(
"Expected response status code %d, got %d. Raw response: '%s'",
http.StatusBadRequest,
w.Result().StatusCode,
string(body),
)
}
// testing invalid response body
w = httptest.NewRecorder()
invResp := struct {
Errors string
}{}
invalidPayload := make(chan int)
SendError(w, invalidPayload, http.StatusBadRequest)
body, err = io.ReadAll(w.Body)
if err != nil {
t.Error(err.Error())
return
}
err = json.Unmarshal(body, &invResp)
if err != nil {
t.Error(err.Error())
return
}
if invResp.Errors != `Internal server error` {
t.Errorf(
"Expected 'Internal server error', got '%v'. Raw response: '%s'",
invResp.Errors,
string(body),
)
}
if w.Result().StatusCode != http.StatusInternalServerError {
t.Errorf(
"Expected response status code %d, got %d. Raw response: '%s'",
http.StatusInternalServerError,
w.Result().StatusCode,
string(body),
)
}
}
func TestSendResponse(t *testing.T) {
t.Parallel()
w := httptest.NewRecorder()
payload := map[string]string{"hello": "world"}
SendResponse(w, payload, http.StatusOK)
body, err := io.ReadAll(w.Body)
if err != nil {
t.Error(err.Error())
return
}
resp := struct {
Data map[string]string
}{}
err = json.Unmarshal(body, &resp)
if err != nil {
t.Error(err.Error())
}
if !reflect.DeepEqual(payload, resp.Data) {
t.Errorf(
"Expected '%v', got '%v'. Raw response: '%s'",
payload,
resp.Data,
string(body),
)
}
if w.Result().StatusCode != http.StatusOK {
t.Errorf(
"Expected response status code %d, got %d. Raw response: '%s'",
http.StatusOK,
w.Result().StatusCode,
string(body),
)
}
// testing invalid response payload
w = httptest.NewRecorder()
SendResponse(w, make(chan int), http.StatusOK)
body, err = io.ReadAll(w.Body)
if err != nil {
t.Error(err.Error())
return
}
invalidresp := struct {
Errors string
}{}
err = json.Unmarshal(body, &invalidresp)
if err != nil {
t.Error(err.Error())
}
if !reflect.DeepEqual(`Internal server error`, invalidresp.Errors) {
t.Errorf(
"Expected '%v', got '%v'. Raw response: '%s'",
payload,
invalidresp.Errors,
string(body),
)
}
if w.Result().StatusCode != http.StatusInternalServerError {
t.Errorf(
"Expected response status code %d, got %d. Raw response: '%s'",
http.StatusInternalServerError,
w.Result().StatusCode,
string(body),
)
}
}
func TestSend(t *testing.T) {
t.Parallel()
w := httptest.NewRecorder()
payload := map[string]string{"hello": "world"}
reqBody, _ := json.Marshal(payload)
Send(w, JSONContentType, string(reqBody), http.StatusOK)
body, err := io.ReadAll(w.Body)
if err != nil {
t.Error(err.Error())
return
}
resp := map[string]string{}
err = json.Unmarshal(body, &resp)
if err != nil {
t.Error(err.Error())
}
if !reflect.DeepEqual(payload, resp) {
t.Errorf(
"Expected '%v', got '%v'. Raw response: '%s'",
payload,
resp,
string(body),
)
}
if w.Result().StatusCode != http.StatusOK {
t.Errorf(
"Expected response status code %d, got %d. Raw response: '%s'",
http.StatusOK,
w.Result().StatusCode,
string(body),
)
}
}
func TestRender(t *testing.T) {
t.Parallel()
w := httptest.NewRecorder()
data := struct {
Hello string
}{
Hello: "world",
}
tpl := template.New("txttemp")
tpl, err := tpl.Parse(`{{.Hello}}`)
if err != nil {
t.Error(err.Error())
return
}
Render(w, data, http.StatusOK, tpl)
body, err := io.ReadAll(w.Body)
if err != nil {
t.Error(err.Error())
return
}
if w.Code != http.StatusOK {
t.Errorf(
"Expected response status code %d, got %d. Raw response: '%s'",
http.StatusOK,
w.Code,
string(body),
)
}
w = httptest.NewRecorder()
invaliddata := 0
tpl = template.New("invalid")
tpl, err = tpl.Parse(`{{.Hello}}`)
if err != nil {
t.Error(err.Error())
return
}
Render(w, invaliddata, http.StatusOK, tpl)
body, err = io.ReadAll(w.Body)
if err != nil {
t.Error(err.Error())
return
}
str := string(body)
want := `Internal server error`
if str != want {
t.Errorf(
"Expected '%s', got '%s'. Raw response: '%s'",
want,
str,
str,
)
}
if w.Code != http.StatusInternalServerError {
t.Errorf(
"Expected response status code %d, got %d. Raw response: '%s'",
http.StatusInternalServerError,
w.Code,
string(body),
)
}
}
func TestResponsehelpers(t *testing.T) {
t.Parallel()
w := httptest.NewRecorder()
want := "hello world"
resp := struct {
Data string
Errors string
Status int
}{}
R200(w, want)
body, err := io.ReadAll(w.Body)
if err != nil {
t.Error(err.Error())
return
}
err = json.Unmarshal(body, &resp)
if err != nil {
t.Error(err.Error())
return
}
if resp.Data != want {
t.Errorf(
"Expected '%s', got '%s'",
want,
resp.Data,
)
}
if w.Code != http.StatusOK {
t.Errorf(
"Expected response status code %d, got %d. Raw response: '%s'",
http.StatusOK,
w.Code,
string(body),
)
}
// R201
w = httptest.NewRecorder()
resp.Data = ""
R201(w, want)
body, err = io.ReadAll(w.Body)
if err != nil {
t.Error(err.Error())
return
}
err = json.Unmarshal(body, &resp)
if err != nil {
t.Error(err.Error())
return
}
if resp.Data != want {
t.Errorf(
"Expected '%s', got '%s'",
want,
resp.Data,
)
}
if w.Code != http.StatusCreated {
t.Errorf(
"Expected response status code %d, got %d. Raw response: '%s'",
http.StatusCreated,
w.Code,
string(body),
)
}
// R204
w = httptest.NewRecorder()
resp.Data = ""
R204(w)
body, err = io.ReadAll(w.Body)
if err != nil {
t.Error(err.Error())
return
}
if string(body) != "" {
t.Errorf(
"Expected empty response, got '%s'",
string(body),
)
}
if w.Code != http.StatusNoContent {
t.Errorf(
"Expected response status code %d, got %d. Raw response: '%s'",
http.StatusNoContent,
w.Code,
string(body),
)
}
// R302
w = httptest.NewRecorder()
resp.Data = ""
R302(w, want)
body, err = io.ReadAll(w.Body)
if err != nil {
t.Error(err.Error())
return
}
err = json.Unmarshal(body, &resp)
if err != nil {
t.Error(err.Error())
return
}
if resp.Data != want {
t.Errorf(
"Expected '%s', got '%s'",
want,
resp.Data,
)
}
if w.Code != http.StatusFound {
t.Errorf(
"Expected response status code %d, got %d. Raw response: '%s'",
http.StatusFound,
w.Code,
string(body),
)
}
// R400
w = httptest.NewRecorder()
resp.Data = ""
resp.Errors = ""
R400(w, want)
body, err = io.ReadAll(w.Body)
if err != nil {
t.Error(err.Error())
return
}
err = json.Unmarshal(body, &resp)
if err != nil {
t.Error(err.Error())
return
}
if resp.Errors != want {
t.Errorf(
"Expected '%s', got '%s'",
want,
resp.Errors,
)
}
if w.Code != http.StatusBadRequest {
t.Errorf(
"Expected response status code %d, got %d. Raw response: '%s'",
http.StatusBadRequest,
w.Code,
string(body),
)
}
// R403
w = httptest.NewRecorder()
resp.Data = ""
resp.Errors = ""
R403(w, want)
body, err = io.ReadAll(w.Body)
if err != nil {
t.Error(err.Error())
return
}
err = json.Unmarshal(body, &resp)
if err != nil {
t.Error(err.Error())
return
}
if resp.Errors != want {
t.Errorf(
"Expected '%s', got '%s'",
want,
resp.Errors,
)
}
if w.Code != http.StatusForbidden {
t.Errorf(
"Expected response status code %d, got %d. Raw response: '%s'",
http.StatusForbidden,
w.Code,
string(body),
)
}
// R404
w = httptest.NewRecorder()
resp.Data = ""
resp.Errors = ""
R404(w, want)
body, err = io.ReadAll(w.Body)
if err != nil {
t.Error(err.Error())
return
}
err = json.Unmarshal(body, &resp)
if err != nil {
t.Error(err.Error())
return
}
if resp.Errors != want {
t.Errorf(
"Expected '%s', got '%s'",
want,
resp.Errors,
)
}
if w.Code != http.StatusNotFound {
t.Errorf(
"Expected response status code %d, got %d. Raw response: '%s'",
http.StatusNotFound,
w.Code,
string(body),
)
}
// R406
w = httptest.NewRecorder()
resp.Data = ""
resp.Errors = ""
R406(w, want)
body, err = io.ReadAll(w.Body)
if err != nil {
t.Error(err.Error())
return
}
err = json.Unmarshal(body, &resp)
if err != nil {
t.Error(err.Error())
return
}
if resp.Errors != want {
t.Errorf(
"Expected '%s', got '%s'",
want,
resp.Errors,
)
}
if w.Code != http.StatusNotAcceptable {
t.Errorf(
"Expected response status code %d, got %d. Raw response: '%s'",
http.StatusNotAcceptable,
w.Code,
string(body),
)
}
// R451
w = httptest.NewRecorder()
resp.Data = ""
resp.Errors = ""
R451(w, want)
body, err = io.ReadAll(w.Body)
if err != nil {
t.Error(err.Error())
return
}
err = json.Unmarshal(body, &resp)
if err != nil {
t.Error(err.Error())
return
}
if resp.Errors != want {
t.Errorf(
"Expected '%s', got '%s'",
want,
resp.Errors,
)
}
if w.Code != http.StatusUnavailableForLegalReasons {
t.Errorf(
"Expected response status code %d, got %d. Raw response: '%s'",
http.StatusUnavailableForLegalReasons,
w.Code,
string(body),
)
}
}
================================================
FILE: route.go
================================================
package webgo
import (
"bytes"
"fmt"
"net/http"
"strings"
)
// Route defines a route for each API
type Route struct {
// Name is unique identifier for the route
Name string
// Method is the HTTP request method/type
Method string
// Pattern is the URI pattern to match
Pattern string
// TrailingSlash if set to true, the URI will be matched with or without
// a trailing slash. IMPORTANT: It does not redirect.
TrailingSlash bool
// FallThroughPostResponse if enabled will execute all the handlers even if a response was already sent to the client
FallThroughPostResponse bool
// Handlers is a slice of http.HandlerFunc which can be middlewares or anything else. Though only 1 of them will be allowed to respond to client.
// subsequent writes from the following handlers will be ignored
Handlers []http.HandlerFunc
hasWildcard bool
fragments []uriFragment
paramsCount int
// skipMiddleware if true, middleware added using `router` will not be applied to this Route.
// This is used only when a Route is set using the RouteGroup, which can have its own set of middleware
skipMiddleware bool
// middlewareList is used at the last stage, i.e. right before starting the server
middlewarelist []Middleware
initialized bool
serve http.HandlerFunc
}
type uriFragment struct {
isVariable bool
hasWildcard bool
// fragment will be the key name, if it's a variable/named URI parameter
fragment string
}
func (r *Route) parseURIWithParams() {
// if there are no URI params, then there's no need to set route parts
if !strings.Contains(r.Pattern, ":") {
return
}
fragments := strings.Split(r.Pattern, "/")
if len(fragments) == 1 {
return
}
rFragments := make([]uriFragment, 0, len(fragments))
for _, fragment := range fragments[1:] {
hasParam := false
hasWildcard := false
if strings.Contains(fragment, ":") {
hasParam = true
r.paramsCount++
}
if strings.Contains(fragment, "*") {
r.hasWildcard = true
hasWildcard = true
}
key := strings.ReplaceAll(fragment, ":", "")
key = strings.ReplaceAll(key, "*", "")
rFragments = append(
rFragments,
uriFragment{
isVariable: hasParam,
hasWildcard: hasWildcard,
fragment: key,
})
}
r.fragments = rFragments
}
func (r *Route) setupMiddleware(reverse bool) {
if reverse {
for i := range r.middlewarelist {
m := r.middlewarelist[i]
srv := r.serve
r.serve = func(rw http.ResponseWriter, req *http.Request) {
m(rw, req, srv)
}
}
} else {
for i := len(r.middlewarelist) - 1; i >= 0; i-- {
m := r.middlewarelist[i]
srv := r.serve
r.serve = func(rw http.ResponseWriter, req *http.Request) {
m(rw, req, srv)
}
}
}
// clear middlewarelist since it's already setup for the route
r.middlewarelist = nil
}
// init does all the initializations required for the route
func (r *Route) init() error {
if r.initialized {
return nil
}
r.initialized = true
r.parseURIWithParams()
r.serve = defaultRouteServe(r)
return nil
}
// matchPath matches the requestURI with the URI pattern of the route
func (r *Route) matchPath(requestURI string) (bool, map[string]string) {
p := bytes.NewBufferString(r.Pattern)
if r.TrailingSlash {
p.WriteString("/")
} else {
if requestURI[len(requestURI)-1] == '/' {
return false, nil
}
}
if r.Pattern == requestURI || p.String() == requestURI {
return true, nil
}
return r.matchWithWildcard(requestURI)
}
func (r *Route) matchWithWildcard(requestURI string) (bool, map[string]string) {
// if r.fragments is empty, it means there are no variables in the URI pattern
// hence no point checking
if len(r.fragments) == 0 {
return false, nil
}
params := make(map[string]string, r.paramsCount)
uriFragments := strings.Split(requestURI, "/")[1:]
fragmentsLastIdx := len(r.fragments) - 1
fragmentIdx := 0
uriParameter := make([]string, 0, len(uriFragments))
for idx, fragment := range uriFragments {
// if part is empty, it means it's end of URI with trailing slash
if fragment == "" {
break
}
if fragmentIdx > fragmentsLastIdx {
return false, nil
}
currentFragment := r.fragments[fragmentIdx]
if !currentFragment.isVariable && currentFragment.fragment != fragment {
return false, nil
}
uriParameter = append(uriParameter, fragment)
if currentFragment.isVariable {
params[currentFragment.fragment] = strings.Join(uriParameter, "/")
}
if !currentFragment.hasWildcard {
uriParameter = make([]string, 0, len(uriFragments)-idx)
fragmentIdx++
continue
}
nextIdx := fragmentIdx + 1
if nextIdx > fragmentsLastIdx {
continue
}
nextPart := r.fragments[nextIdx]
// if the URI has more fragments/params after wildcard,
// the immediately following part after wildcard cannot be a variable or another wildcard.
if !nextPart.isVariable && nextPart.fragment == fragment {
// remove the last added 'part' from parameters, as it's part of the static URI
params[currentFragment.fragment] = strings.Join(uriParameter[:len(uriParameter)-1], "/")
uriParameter = make([]string, 0, len(uriFragments)-idx)
fragmentIdx += 2
}
}
if len(params) != r.paramsCount {
return false, nil
}
return true, params
}
func (r *Route) use(mm ...Middleware) {
if r.middlewarelist == nil {
r.middlewarelist = make([]Middleware, 0, len(mm))
}
r.middlewarelist = append(r.middlewarelist, mm...)
}
func routeServeChainedHandlers(r *Route) http.HandlerFunc {
return func(rw http.ResponseWriter, req *http.Request) {
crw, ok := rw.(*customResponseWriter)
if !ok {
crw = newCRW(rw, http.StatusOK)
}
for _, handler := range r.Handlers {
if crw.written && !r.FallThroughPostResponse {
break
}
handler(crw, req)
}
}
}
func defaultRouteServe(r *Route) http.HandlerFunc {
if len(r.Handlers) > 1 {
return routeServeChainedHandlers(r)
}
// when there is only 1 handler, custom response writer is not required to check if response
// is already written or fallthrough is enabled
return r.Handlers[0]
}
type RouteGroup struct {
routes []*Route
// skipRouterMiddleware if set to true, middleware applied to the router will not be applied
// to this route group.
skipRouterMiddleware bool
// PathPrefix is the URI prefix for all routes in this group
PathPrefix string
}
func (rg *RouteGroup) Add(rr ...Route) {
for idx := range rr {
route := rr[idx]
route.skipMiddleware = rg.skipRouterMiddleware
route.Pattern = fmt.Sprintf("%s%s", rg.PathPrefix, route.Pattern)
rg.routes = append(rg.routes, &route)
}
}
func (rg *RouteGroup) Use(mm ...Middleware) {
for _, route := range rg.routes {
route.use(mm...)
}
}
func (rg *RouteGroup) Routes() []*Route {
return rg.routes
}
func NewRouteGroup(pathPrefix string, skipRouterMiddleware bool, rr ...Route) *RouteGroup {
rg := RouteGroup{
PathPrefix: pathPrefix,
skipRouterMiddleware: skipRouterMiddleware,
}
rg.Add(rr...)
return &rg
}
================================================
FILE: route_test.go
================================================
package webgo
import (
"fmt"
"net/http"
"reflect"
"testing"
)
func TestRouteGroupsPathPrefix(t *testing.T) {
t.Parallel()
routes := []Route{
{
Name: "r1",
Pattern: "/a",
Method: http.MethodGet,
Handlers: []http.HandlerFunc{dummyHandler},
},
{
Name: "r2",
Pattern: "/b/:c",
Method: http.MethodGet,
Handlers: []http.HandlerFunc{dummyHandler},
},
{
Name: "r3",
Pattern: "/:w*",
Method: http.MethodGet,
Handlers: []http.HandlerFunc{dummyHandler},
},
}
const prefix = "/v7.0.0"
expectedSkipMiddleware := true
rg := NewRouteGroup("/v7.0.0", expectedSkipMiddleware, routes...)
list := rg.Routes()
for idx := range list {
route := list[idx]
originalRoute := routes[idx]
expectedPattern := fmt.Sprintf("%s%s", prefix, originalRoute.Pattern)
if route.Pattern != expectedPattern {
t.Errorf("Expected pattern %q, got %q", expectedPattern, route.Pattern)
}
if route.skipMiddleware != expectedSkipMiddleware {
t.Errorf("Expected skip %v, got %v", expectedSkipMiddleware, route.skipMiddleware)
}
}
}
func dummyHandler(w http.ResponseWriter, r *http.Request) {}
func BenchmarkMatchWithWildcard(b *testing.B) {
route := Route{
Name: "widlcard",
Method: http.MethodGet,
TrailingSlash: true,
FallThroughPostResponse: true,
Pattern: "/:w*/static1/:myvar/:w2*",
Handlers: []http.HandlerFunc{dummyHandler},
}
uri := "/hello/world/how/are/you/static1/hello2/world2/how2/are2/you2/static2"
err := route.init()
if err != nil {
b.Error(err)
return
}
for i := 0; i < b.N; i++ {
ok, _ := route.matchPath(uri)
if !ok {
b.Errorf("Expected match, got no match")
break
}
}
}
func TestMatchWithWildcard(t *testing.T) {
route := Route{
Name: "widlcard",
Method: http.MethodGet,
TrailingSlash: true,
FallThroughPostResponse: true,
Pattern: "/:w*/static1/:myvar/:w2*/static2",
Handlers: []http.HandlerFunc{dummyHandler},
}
err := route.init()
if err != nil {
t.Error(err)
return
}
uri := "/hello/world/how/are/you/static1/hello2/world2/how2/are2/you2/static2"
wantParams := map[string]string{
"w": "hello/world/how/are/you",
"myvar": "hello2",
"w2": "world2/how2/are2/you2",
}
matched, params := route.matchPath(uri)
if !matched {
t.Errorf("Expected match, got no match")
return
}
if !reflect.DeepEqual(params, wantParams) {
t.Errorf("Expected params %v, got %v", wantParams, params)
return
}
t.Run("no match", func(t *testing.T) {
route := Route{
Name: "widlcard",
Method: http.MethodGet,
TrailingSlash: true,
FallThroughPostResponse: true,
Pattern: "/:w*/static1/:myvar/:w2*/static2",
Handlers: []http.HandlerFunc{dummyHandler},
}
err := route.init()
if err != nil {
t.Error(err)
return
}
uri := "/hello/world/how/are/you/static2/hello2/world2/how2/are2/you2/static2"
matched, params := route.matchPath(uri)
if matched {
t.Errorf("Expected no match, got match")
return
}
if params != nil {
t.Errorf("Expected params %v, got %v", nil, params)
return
}
})
t.Run("match with more params", func(t *testing.T) {
route := Route{
Name: "widlcard",
Method: http.MethodGet,
TrailingSlash: true,
FallThroughPostResponse: true,
Pattern: "/:w*/static1/:myvar/:w2*/static2/:myvar2/:w3*/static3",
Handlers: []http.HandlerFunc{dummyHandler},
}
err := route.init()
if err != nil {
t.Error(err)
return
}
uri := "/hello/world/how/are/you/static1/hello2/world2/how2/are2/you2/static2/hello3/world3/how3/are3/you3/static3"
wantParams := map[string]string{
"w": "hello/world/how/are/you",
"myvar": "hello2",
"w2": "world2/how2/are2/you2",
"myvar2": "hello3",
"w3": "world3/how3/are3/you3",
}
matched, params := route.matchPath(uri)
if !matched {
t.Errorf("Expected match, got no match")
return
}
if !reflect.DeepEqual(params, wantParams) {
t.Errorf("Expected params %v, got %v", wantParams, params)
return
}
})
t.Run("match - end with wildcard", func(t *testing.T) {
route := Route{
Name: "widlcard",
Method: http.MethodGet,
TrailingSlash: true,
FallThroughPostResponse: true,
Pattern: "/:w*/static1/:myvar/:w2*",
Handlers: []http.HandlerFunc{dummyHandler},
}
err := route.init()
if err != nil {
t.Error(err)
return
}
uri := "/hello/world/how/are/you/static1/hello2/world2/how2/are2/you2/static2"
wantParams := map[string]string{
"w": "hello/world/how/are/you",
"myvar": "hello2",
"w2": "world2/how2/are2/you2/static2",
}
matched, params := route.matchPath(uri)
if !matched {
t.Errorf("Expected match, got no match")
return
}
if !reflect.DeepEqual(params, wantParams) {
t.Errorf("Expected params %v, got %v", wantParams, params)
return
}
})
t.Run("root URI, no match", func(t *testing.T) {
route := Route{
Name: "",
Method: http.MethodGet,
TrailingSlash: true,
FallThroughPostResponse: true,
Pattern: "/-/health",
Handlers: []http.HandlerFunc{dummyHandler},
}
err := route.init()
if err != nil {
t.Error(err)
return
}
matched, _ := route.matchPath("/")
if matched {
t.Errorf("Expected no match, got match")
return
}
})
t.Run("root URI, should match", func(t *testing.T) {
route := Route{
Name: "",
Method: http.MethodGet,
TrailingSlash: true,
FallThroughPostResponse: true,
Pattern: "/",
Handlers: []http.HandlerFunc{dummyHandler},
}
err := route.init()
if err != nil {
t.Error(err)
return
}
matched, _ := route.matchPath("/")
if !matched {
t.Errorf("Expected match, got no match")
return
}
})
}
================================================
FILE: router.go
================================================
package webgo
import (
"bufio"
"context"
"encoding/json"
"errors"
"fmt"
"net"
"net/http"
"sync"
)
// httpResponseWriter has all the functions to be implemented by the custom
// responsewriter used
type httpResponseWriter interface {
http.ResponseWriter
http.Flusher
http.Hijacker
http.Pusher
}
func init() {
var err error
jsonErrPayload, err = json.Marshal(errOutput{
Errors: ErrInternalServer,
Status: http.StatusInternalServerError,
})
if err != nil {
panic(err)
}
// ensure the custom response writer implements all the required functions
crw := &customResponseWriter{}
_ = httpResponseWriter(crw)
}
var (
validHTTPMethods = []string{
http.MethodOptions,
http.MethodHead,
http.MethodGet,
http.MethodPost,
http.MethodPut,
http.MethodPatch,
http.MethodDelete,
}
ctxPool = &sync.Pool{
New: func() interface{} {
return new(ContextPayload)
},
}
crwPool = &sync.Pool{
New: func() interface{} {
return new(customResponseWriter)
},
}
)
// customResponseWriter is a custom HTTP response writer
type customResponseWriter struct {
http.ResponseWriter
statusCode int
written bool
headerWritten bool
}
// WriteHeader is the interface implementation to get HTTP response code and add
// it to the custom response writer
func (crw *customResponseWriter) WriteHeader(code int) {
if crw.headerWritten {
return
}
crw.headerWritten = true
crw.statusCode = code
crw.ResponseWriter.WriteHeader(code)
}
// Write is the interface implementation to respond to the HTTP request,
// but check if a response was already sent.
func (crw *customResponseWriter) Write(body []byte) (int, error) {
crw.WriteHeader(crw.statusCode)
crw.written = true
return crw.ResponseWriter.Write(body)
}
// Flush calls the http.Flusher to clear/flush the buffer
func (crw *customResponseWriter) Flush() {
if rw, ok := crw.ResponseWriter.(http.Flusher); ok {
rw.Flush()
}
}
// Hijack implements the http.Hijacker interface
func (crw *customResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
if hj, ok := crw.ResponseWriter.(http.Hijacker); ok {
return hj.Hijack()
}
return nil, nil, errors.New("unable to create hijacker")
}
func (crw *customResponseWriter) Push(target string, opts *http.PushOptions) error {
if n, ok := crw.ResponseWriter.(http.Pusher); ok {
return n.Push(target, opts)
}
return errors.New("pusher not implemented")
}
func (crw *customResponseWriter) reset() {
crw.statusCode = 0
crw.written = false
crw.headerWritten = false
crw.ResponseWriter = nil
}
// Middleware is the signature of WebGo's middleware
type Middleware func(http.ResponseWriter, *http.Request, http.HandlerFunc)
// discoverRoute returns the correct 'route', for the given request
func discoverRoute(path string, routes []*Route) (*Route, map[string]string) {
for _, route := range routes {
if ok, params := route.matchPath(path); ok {
return route, params
}
}
return nil, nil
}
// Router is the HTTP router
type Router struct {
optHandlers []*Route
headHandlers []*Route
getHandlers []*Route
postHandlers []*Route
putHandlers []*Route
patchHandlers []*Route
deleteHandlers []*Route
allHandlers map[string][]*Route
// NotFound is the generic handler for 404 resource not found response
NotFound http.HandlerFunc
// NotImplemented is the generic handler for 501 method not implemented
NotImplemented http.HandlerFunc
// config has all the app config
config *Config
// httpServer is the server handler for the active HTTP server
httpServer *http.Server
// httpsServer is the server handler for the active HTTPS server
httpsServer *http.Server
}
// methodRoutes returns the list of Routes handling the HTTP method given the request
func (rtr *Router) methodRoutes(method string) (routes []*Route) {
switch method {
case http.MethodOptions:
return rtr.optHandlers
case http.MethodHead:
return rtr.headHandlers
case http.MethodGet:
return rtr.getHandlers
case http.MethodPost:
return rtr.postHandlers
case http.MethodPut:
return rtr.putHandlers
case http.MethodPatch:
return rtr.patchHandlers
case http.MethodDelete:
return rtr.deleteHandlers
}
return nil
}
func (rtr *Router) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
// a custom response writer is used to set appropriate HTTP status code in case of
// encoding errors. i.e. if there's a JSON encoding issue while responding,
// the HTTP status code would say 200, and and the JSON payload {"status": 500}
crw := newCRW(rw, http.StatusOK)
routes := rtr.methodRoutes(r.Method)
if routes == nil {
// serve 501 when HTTP method is not implemented
crw.statusCode = http.StatusNotImplemented
rtr.NotImplemented(crw, r)
releaseCRW(crw)
return
}
path := r.URL.EscapedPath()
route, params := discoverRoute(path, routes)
if route == nil {
// serve 404 when there are no matching routes
crw.statusCode = http.StatusNotFound
rtr.NotFound(crw, r)
releaseCRW(crw)
return
}
ctxPayload := newContext()
ctxPayload.Route = route
ctxPayload.URIParams = params
// webgo context is injected to the HTTP request context
*r = *r.WithContext(
context.WithValue(
r.Context(),
wgoCtxKey,
ctxPayload,
),
)
defer releasePoolResources(crw, ctxPayload)
route.serve(crw, r)
}
// Use adds a middleware layer
func (rtr *Router) Use(mm ...Middleware) {
for _, handlers := range rtr.allHandlers {
for idx := range handlers {
route := handlers[idx]
if route.skipMiddleware {
continue
}
route.use(mm...)
}
}
}
// UseOnSpecialHandlers adds middleware to the 2 special handlers of webgo
func (rtr *Router) UseOnSpecialHandlers(mm ...Middleware) {
// v3.2.1 introduced the feature of adding middleware to both notfound & not implemented
// handlers
/*
- It was added considering an `accesslog` middleware, where all requests should be logged
# This is now being moved to a separate function considering an authentication middleware, where all requests
including 404 & 501 would respond with `not authenticated` if you do not have special handling
within the middleware. It is a cleaner implementation to avoid this and let users add their
middleware separately to NOTFOUND & NOTIMPLEMENTED handlers
*/
for idx := range mm {
m := mm[idx]
nf := rtr.NotFound
rtr.NotFound = func(rw http.ResponseWriter, req *http.Request) {
m(rw, req, nf)
}
ni := rtr.NotImplemented
rtr.NotImplemented = func(rw http.ResponseWriter, req *http.Request) {
m(rw, req, ni)
}
}
}
// Add is a convenience method used to add a new route to an already initialized router
// Important: `.Use` should be used only after all routes are added
func (rtr *Router) Add(routes ...*Route) {
hmap := httpHandlers(routes)
rtr.optHandlers = append(rtr.optHandlers, hmap[http.MethodOptions]...)
rtr.headHandlers = append(rtr.headHandlers, hmap[http.MethodHead]...)
rtr.getHandlers = append(rtr.getHandlers, hmap[http.MethodGet]...)
rtr.postHandlers = append(rtr.postHandlers, hmap[http.MethodPost]...)
rtr.putHandlers = append(rtr.putHandlers, hmap[http.MethodPut]...)
rtr.patchHandlers = append(rtr.patchHandlers, hmap[http.MethodPatch]...)
rtr.deleteHandlers = append(rtr.deleteHandlers, hmap[http.MethodDelete]...)
all := rtr.allHandlers
if all == nil {
all = map[string][]*Route{}
}
for _, key := range supportedHTTPMethods {
newlist, hasKey := hmap[key]
if !hasKey {
continue
}
if all[key] == nil {
all[key] = make([]*Route, 0, len(hmap))
}
all[key] = append(all[key], newlist...)
}
rtr.allHandlers = all
}
func newCRW(rw http.ResponseWriter, rCode int) *customResponseWriter {
crw := crwPool.Get().(*customResponseWriter)
crw.ResponseWriter = rw
crw.statusCode = rCode
return crw
}
func releaseCRW(crw *customResponseWriter) {
crw.reset()
crwPool.Put(crw)
}
func newContext() *ContextPayload {
return ctxPool.Get().(*ContextPayload)
}
func releaseContext(cp *ContextPayload) {
cp.reset()
ctxPool.Put(cp)
}
func releasePoolResources(crw *customResponseWriter, cp *ContextPayload) {
releaseCRW(crw)
releaseContext(cp)
}
// NewRouter initializes & returns a new router instance with all the configurations and routes set
func NewRouter(cfg *Config, routes ...*Route) *Router {
r := &Router{
NotFound: http.NotFound,
NotImplemented: func(rw http.ResponseWriter, req *http.Request) {
Send(rw, "", "501 Not Implemented", http.StatusNotImplemented)
},
config: cfg,
}
r.Add(routes...)
return r
}
// checkDuplicateRoutes checks if any of the routes have duplicate name or URI pattern
func checkDuplicateRoutes(idx int, route *Route, routes []*Route) {
// checking if the URI pattern is duplicated
for i := 0; i < idx; i++ {
rt := routes[i]
if rt.Name == route.Name {
LOGHANDLER.Info(
fmt.Sprintf(
"Duplicate route name('%s') detected",
rt.Name,
),
)
}
if rt.Method != route.Method {
continue
}
// regex pattern match
if ok, _ := rt.matchPath(route.Pattern); !ok {
continue
}
LOGHANDLER.Warn(
fmt.Sprintf(
"Duplicate URI pattern detected.\nPattern: '%s'\nDuplicate pattern: '%s'",
rt.Pattern,
route.Pattern,
),
)
LOGHANDLER.Warn("Only the first route to match the URI pattern would handle the request")
}
}
// httpHandlers returns all the handlers in a map, for each HTTP method
func httpHandlers(routes []*Route) map[string][]*Route {
handlers := map[string][]*Route{}
handlers[http.MethodHead] = []*Route{}
handlers[http.MethodGet] = []*Route{}
for idx, route := range routes {
found := false
for _, validMethod := range validHTTPMethods {
if route.Method == validMethod {
found = true
break
}
}
if !found {
LOGHANDLER.Fatal(
fmt.Sprintf(
"Unsupported HTTP method provided. Method: '%s'",
route.Method,
),
)
return nil
}
if len(route.Handlers) == 0 {
LOGHANDLER.Fatal(
fmt.Sprintf(
"No handlers provided for the route '%s', method '%s'",
route.Pattern,
route.Method,
),
)
return nil
}
err := route.init()
if err != nil {
LOGHANDLER.Fatal("Unsupported URI pattern.", route.Pattern, err)
return nil
}
checkDuplicateRoutes(idx, route, routes)
handlers[route.Method] = append(handlers[route.Method], route)
}
return handlers
}
================================================
FILE: router_test.go
================================================
package webgo
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
)
func TestRouter_ServeHTTP(t *testing.T) {
t.Parallel()
port := "9696"
router, err := setup(t, port)
if err != nil {
t.Error(err.Error())
return
}
m := func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
w.Header().Add("middleware", "true")
next(w, r)
}
router.Use(m)
router.UseOnSpecialHandlers(m)
router.SetupMiddleware()
list := testTable()
baseAPI := fmt.Sprintf("http://localhost:%s", port)
for _, l := range list {
url := baseAPI
if l.Path != "" {
switch l.TestType {
case "checkpath",
"checkpathnotrailingslash",
"chaining",
"notfound",
"chaining-nofallthrough":
{
url = strings.Join([]string{url, l.Path}, "")
}
case "checkparams", "widlcardwithouttrailingslash":
{
for idx, key := range l.ParamKeys {
// in case of wildcard params, they have to be replaced first for proper URL construction
l.Path = strings.Replace(l.Path, ":"+key+"*", l.Params[idx], 1)
l.Path = strings.Replace(l.Path, ":"+key, l.Params[idx], 1)
}
url = strings.Join([]string{url, l.Path}, "")
}
}
}
respRec := httptest.NewRecorder()
req := httptest.NewRequest(
l.Method,
url,
l.Body,
)
router.ServeHTTP(respRec, req)
switch l.TestType {
case "checkpath", "checkpathnotrailingslash":
{
err = checkPath(req, respRec)
}
case "widlcardwithouttrailingslash":
{
err = checkPathWildCard(req, respRec)
}
case "chaining":
{
err = checkChaining(req, respRec)
}
case "checkparams":
{
err = checkParams(req, respRec, l.ParamKeys, l.Params)
}
case "notimplemented":
{
err = checkNotImplemented(req, respRec)
}
case "notfound":
{
err = checkNotFound(req, respRec)
}
}
if err != nil && !l.WantErr {
t.Errorf(
"'%s' (%s '%s'): %s",
l.Name,
l.Method,
url,
err.Error(),
)
if l.Err != nil {
if !errors.Is(err, l.Err) {
t.Errorf(
"expected error '%s', got %s",
l.Err.Error(),
err.Error(),
)
}
}
} else if err == nil && l.WantErr {
t.Errorf(
"'%s' (%s '%s') expected error, but received nil",
l.Name,
l.Method,
url,
)
}
err = checkMiddleware(req, respRec)
if err != nil {
t.Error(err.Error())
}
}
}
func setup(t *testing.T, port string) (*Router, error) {
t.Helper()
cfg := &Config{
Port: port,
ReadTimeout: time.Second * 1,
WriteTimeout: time.Second * 1,
ShutdownTimeout: time.Second * 10,
CertFile: "tests/ssl/server.crt",
KeyFile: "tests/ssl/server.key",
}
router := NewRouter(cfg, getRoutes(t)...)
return router, nil
}
func getRoutes(t *testing.T) []*Route {
t.Helper()
list := testTable()
rr := make([]*Route, 0, len(list))
for _, l := range list {
switch l.TestType {
case "checkpath", "checkparams", "checkparamswildcard":
{
rr = append(rr,
&Route{
Name: l.Name,
Method: l.Method,
Pattern: l.Path,
TrailingSlash: true,
FallThroughPostResponse: false,
Handlers: []http.HandlerFunc{successHandler},
},
)
}
case "checkpathnotrailingslash", "widlcardwithouttrailingslash":
{
rr = append(rr,
&Route{
Name: l.Name,
Method: l.Method,
Pattern: l.Path,
TrailingSlash: false,
FallThroughPostResponse: false,
Handlers: []http.HandlerFunc{successHandler},
},
)
}
case "chaining":
{
rr = append(
rr,
&Route{
Name: l.Name,
Method: l.Method,
Pattern: l.Path,
TrailingSlash: false,
FallThroughPostResponse: false,
Handlers: []http.HandlerFunc{chainHandler, successHandler},
},
)
}
case "chaining-nofallthrough":
{
{
rr = append(
rr,
&Route{
Name: l.Name,
Method: l.Method,
Pattern: l.Path,
TrailingSlash: false,
FallThroughPostResponse: false,
Handlers: []http.HandlerFunc{chainHandler, chainNoFallthroughHandler, successHandler},
},
)
}
}
}
}
return rr
}
func chainHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Add("chained", "true")
}
func chainNoFallthroughHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Add("chained", "true")
_, _ = w.Write([]byte(`yay, blocked!`))
}
func successHandler(w http.ResponseWriter, r *http.Request) {
wctx := Context(r)
params := wctx.Params()
R200(
w,
map[string]interface{}{
"path": r.URL.Path,
"params": params,
},
)
}
func checkPath(req *http.Request, resp *httptest.ResponseRecorder) error {
want := req.URL.EscapedPath()
rbody, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("error reading response, '%s'", err.Error())
}
body := struct {
Data struct {
Path string
Params map[string]string
}
}{}
err = json.Unmarshal(rbody, &body)
if err != nil {
return fmt.Errorf(
"json decode failed '%s', got response: '%s'",
err.Error(),
string(rbody),
)
}
if want != body.Data.Path {
return fmt.Errorf("wanted URI path '%s', got '%s'", want, body.Data.Path)
}
return nil
}
func checkPathWildCard(req *http.Request, resp *httptest.ResponseRecorder) error {
want := req.URL.EscapedPath()
rbody, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("error reading response, '%s'", err.Error())
}
body := struct {
Data struct {
Path string
Params map[string]string
}
}{}
err = json.Unmarshal(rbody, &body)
if err != nil {
return fmt.Errorf("json decode failed '%s', got response: '%s'", err.Error(), string(rbody))
}
if want != body.Data.Path {
return fmt.Errorf("wanted URI path '%s', got '%s'", want, body.Data.Path)
}
if len(body.Data.Params) != 1 {
return fmt.Errorf("expected no.of params: %d, got %d. response: '%s'", 1, len(body.Data.Params), string(rbody))
}
wantWildcardParamValue := ""
parts := strings.Split(want, "/")[2:]
wantWildcardParamValue = strings.Join(parts, "/")
if body.Data.Params["a"] != wantWildcardParamValue {
return fmt.Errorf(
"wildcard value\nexpected: %s\ngot: %s",
wantWildcardParamValue,
body.Data.Params["a"],
)
}
return nil
}
func checkParams(req *http.Request, resp *httptest.ResponseRecorder, keys []string, expected []string) error {
rbody, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("error reading response, '%s'", err.Error())
}
body := struct {
Data struct {
Params map[string]string
}
}{}
err = json.Unmarshal(rbody, &body)
if err != nil {
return fmt.Errorf("json decode failed '%s', for response '%s'", err.Error(), string(rbody))
}
for idx, key := range keys {
want := expected[idx]
if body.Data.Params[key] != want {
return fmt.Errorf(
"expected value for '%s' is '%s', got '%s'",
key,
want,
body.Data.Params[key],
)
}
}
return nil
}
func checkNotImplemented(req *http.Request, resp *httptest.ResponseRecorder) error {
if resp.Result().StatusCode != http.StatusNotImplemented {
return fmt.Errorf(
"expected code %d, got %d",
http.StatusNotImplemented,
resp.Code,
)
}
return nil
}
func checkNotFound(req *http.Request, resp *httptest.ResponseRecorder) error {
if resp.Result().StatusCode != http.StatusNotFound {
return fmt.Errorf(
"expected code %d, got %d",
http.StatusNotFound,
resp.Code,
)
}
return nil
}
func checkChaining(req *http.Request, resp *httptest.ResponseRecorder) error {
if resp.Header().Get("chained") != "true" {
return fmt.Errorf(
"Expected header value for 'chained', to be 'true', got '%s'",
resp.Header().Get("chained"),
)
}
return nil
}
func checkMiddleware(req *http.Request, resp *httptest.ResponseRecorder) error {
if resp.Header().Get("middleware") != "true" {
return fmt.Errorf(
"Expected header value for 'middleware', to be 'true', got '%s'",
resp.Header().Get("middleware"),
)
}
return nil
}
func testTable() []struct {
Name string
TestType string
Path string
Method string
Want interface{}
WantErr bool
Err error
ParamKeys []string
Params []string
Body io.Reader
} {
return []struct {
Name string
TestType string
Path string
Method string
Want interface{}
WantErr bool
Err error
ParamKeys []string
Params []string
Body io.Reader
}{
{
Name: "Check root path without params",
TestType: "checkpath",
Path: "/",
Method: http.MethodGet,
WantErr: false,
},
{
Name: "Check root path without params - duplicate",
TestType: "checkpath",
Path: "/",
Method: http.MethodGet,
WantErr: false,
},
{
Name: "Check nested path without params - 1",
TestType: "checkpath",
Path: "/a",
Method: http.MethodGet,
WantErr: false,
},
{
Name: "Check nested path without params - 2",
TestType: "checkpath",
Path: "/a/b",
Method: http.MethodGet,
WantErr: false,
},
{
Name: "Check nested path without params - 3",
TestType: "checkpath",
Path: "/a/b/-/c",
Method: http.MethodGet,
WantErr: false,
},
{
Name: "Check nested path without params - 4",
TestType: "checkpath",
Path: "/a/b/-/c/~/d",
Method: http.MethodGet,
WantErr: false,
},
{
Name: "Check nested path without params - 5",
TestType: "checkpath",
Path: "/a/b/-/c/~/d/./e",
Method: http.MethodGet,
WantErr: false,
},
{
Name: "Check nested path without params - 5",
TestType: "checkpathnotrailingslash",
Path: "/a/b/-/c/~/d/./e/notrail",
Method: http.MethodGet,
WantErr: false,
},
{
Name: "Check nested path without params - OPTION",
TestType: "checkpathnotrailingslash",
Path: "/a/b/-/c/~/d/./e",
Method: http.MethodOptions,
WantErr: false,
},
{
Name: "Check nested path without params - HEAD",
TestType: "checkpathnotrailingslash",
Path: "/a/b/-/c/~/d/./e",
Method: http.MethodHead,
WantErr: false,
},
{
Name: "Check nested path without params - POST",
TestType: "checkpathnotrailingslash",
Path: "/a/b/-/c/~/d/./e",
Method: http.MethodPost,
WantErr: false,
},
{
Name: "Check nested path without params - PUT",
TestType: "checkpathnotrailingslash",
Path: "/a/b/-/c/~/d/./e",
Method: http.MethodPut,
WantErr: false,
},
{
Name: "Check nested path without params - PATCH",
TestType: "checkpathnotrailingslash",
Path: "/a/b/-/c/~/d/./e",
Method: http.MethodPatch,
WantErr: false,
},
{
Name: "Check nested path without params - DELETE",
TestType: "checkpathnotrailingslash",
Path: "/a/b/-/c/~/d/./e",
Method: http.MethodDelete,
WantErr: false,
},
{
Name: "Check with params - 1",
TestType: "checkparams",
Path: "/params/:a",
Method: http.MethodGet,
ParamKeys: []string{"a"},
Params: []string{"hello"},
WantErr: false,
},
{
Name: "Check with params - 2",
TestType: "checkparams",
Path: "/params/:a/:b",
Method: http.MethodGet,
ParamKeys: []string{"a", "b"},
Params: []string{"hello", "world"},
WantErr: false,
},
{
Name: "Check with wildcard",
TestType: "checkparams",
Path: "/wildcard/:a*",
Method: http.MethodGet,
ParamKeys: []string{"a"},
Params: []string{"w1/hello/world/hi/there"},
WantErr: false,
},
{
Name: "Check with wildcard - 2",
TestType: "checkparams",
Path: "/wildcard2/:a*",
Method: http.MethodGet,
ParamKeys: []string{"a"},
Params: []string{"w2/hello/world/hi/there/-/~/./again"},
WantErr: false,
},
{
Name: "Check with wildcard - 3",
TestType: "widlcardwithouttrailingslash",
Path: "/wildcard3/:a*",
Method: http.MethodGet,
ParamKeys: []string{"a"},
Params: []string{"w3/hello/world/hi/there/-/~/./again/"},
WantErr: true,
},
{
Name: "Check with wildcard - 4",
TestType: "widlcardwithouttrailingslash",
Path: "/wildcard3/:a*",
Method: http.MethodGet,
ParamKeys: []string{"a"},
Params: []string{"w4/hello/world/hi/there/-/~/./again"},
WantErr: false,
},
{
Name: "Check not implemented",
TestType: "notimplemented",
Path: "/notimplemented",
Method: "HELLO",
WantErr: false,
},
{
Name: "Check not found",
TestType: "notfound",
Path: "/notfound",
Method: http.MethodGet,
WantErr: false,
},
{
Name: "Check chaining",
TestType: "chaining",
Path: "/chained",
Method: http.MethodGet,
WantErr: false,
},
{
Name: "Check chaining",
TestType: "chaining-nofallthrough",
Path: "/chained/nofallthrough",
Method: http.MethodGet,
WantErr: false,
},
}
}
type testLogger struct {
out bytes.Buffer
}
func (tl *testLogger) Debug(data ...interface{}) {
tl.out.Write([]byte(fmt.Sprint(data...)))
}
func (tl *testLogger) Info(data ...interface{}) {
tl.out.Write([]byte(fmt.Sprint(data...)))
}
func (tl *testLogger) Warn(data ...interface{}) {
tl.out.Write([]byte(fmt.Sprint(data...)))
}
func (tl *testLogger) Error(data ...interface{}) {
tl.out.Write([]byte(fmt.Sprint(data...)))
}
func (tl *testLogger) Fatal(data ...interface{}) {
tl.out.Write([]byte(fmt.Sprint(data...)))
}
func Test_httpHandlers(t *testing.T) {
// t.Parallel()
tl := &testLogger{
out: bytes.Buffer{},
}
LOGHANDLER = tl
// test invalid method
httpHandlers(
[]*Route{
{
Name: "invalid method",
Pattern: "/hello/world",
Method: "HELLO",
},
})
got := tl.out.String()
want := "Unsupported HTTP method provided. Method: 'HELLO'"
if got != want {
t.Errorf(
"Expected the error to end with '%s', got '%s'",
want,
got,
)
}
tl.out.Reset()
// test empty handlers
httpHandlers(
[]*Route{
{
Name: "empty handlers",
Pattern: "/hello/world",
Method: http.MethodGet,
},
})
str := tl.out.String()
want = "provided for the route '/hello/world', method 'GET'"
got = str[len(str)-len(want):]
if got != want {
t.Errorf(
"Expected the error to end with '%s', got '%s'",
want,
got,
)
}
tl.out.Reset()
}
func TestWildcardMadness(t *testing.T) {
port := "9696"
t.Helper()
cfg := &Config{
Port: port,
ReadTimeout: time.Second * 1,
WriteTimeout: time.Second * 1,
ShutdownTimeout: time.Second * 10,
CertFile: "tests/ssl/server.crt",
KeyFile: "tests/ssl/server.key",
}
router := NewRouter(cfg, []*Route{
{
Name: "wildcard madness",
Pattern: "/hello/:w*/world/:p1/:w2*/hi/there",
Handlers: []http.HandlerFunc{successHandler},
Method: http.MethodGet,
TrailingSlash: true,
},
}...)
baseAPI := fmt.Sprintf("http://localhost:%s", port)
url := fmt.Sprintf(
"%s%s",
baseAPI,
"/hello/a/b/c/-d/~/e/world/fgh/i/j/k~/l-/hi/there/",
)
req, _ := http.NewRequest(http.MethodGet, url, nil)
respRec := httptest.NewRecorder()
router.ServeHTTP(respRec, req)
rbody, err := io.ReadAll(respRec.Body)
if err != nil {
t.Error(err)
}
if respRec.Code != http.StatusOK {
t.Errorf("expected status code: %d, got: %d. response: '%s'", http.StatusOK, respRec.Code, string(rbody))
}
url = fmt.Sprintf(
"%s%s",
baseAPI,
"/hello/a/b/c/-d/~/e/world/fgh/i/j/k~/l-/hi/there",
)
req, _ = http.NewRequest(http.MethodGet, url, nil)
respRec = httptest.NewRecorder()
router.ServeHTTP(respRec, req)
if respRec.Code != http.StatusOK {
t.Errorf("expected status code: %d, got: %d", http.StatusOK, respRec.Code)
}
err = checkParams(
req,
respRec,
[]string{"w", "p1", "w2"},
[]string{
"a/b/c/-d/~/e",
"fgh",
"i/j/k~/l-",
})
if err != nil {
t.Error(err)
}
}
================================================
FILE: tests/config.json
================================================
{
"host": "127.0.0.1",
"port": "9696",
"httpsPort": "8443",
"certFile": "./ssl/server.crt",
"keyFile": "./ssl/server.key",
"readTimeout": 15000000000,
"writeTimeout": 60000000000,
"insecureSkipVerify": true
}
================================================
FILE: tests/ssl/server.crt
================================================
-----BEGIN CERTIFICATE-----
MIIDkjCCAnoCCQDhnAb7Y802KzANBgkqhkiG9w0BAQsFADCBiTELMAkGA1UEBhMC
SU4xDjAMBgNVBAgMBUluZGlhMQ4wDAYDVQQHDAVJbmRpYTEMMAoGA1UECgwDS0JO
MRMwEQYDVQQLDApPcGVuU291cmNlMRIwEAYDVQQDDAkxMjcuMC4wLjExIzAhBgkq
hkiG9w0BCQEWFGJua2FtYWxlc2hAZ21haWwuY29tMCAXDTE4MDEzMDEwMDQwMVoY
DzIyOTExMTE0MTAwNDAxWjCBiTELMAkGA1UEBhMCSU4xDjAMBgNVBAgMBUluZGlh
MQ4wDAYDVQQHDAVJbmRpYTEMMAoGA1UECgwDS0JOMRMwEQYDVQQLDApPcGVuU291
cmNlMRIwEAYDVQQDDAkxMjcuMC4wLjExIzAhBgkqhkiG9w0BCQEWFGJua2FtYWxl
c2hAZ21haWwuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2t0D
kDIjlAh/kTzu2u8tIhpBQyjXUbw0Kv8T11eWlumkvIBKuNCdei7hkFxLop9Ei0Jt
019uFpRzrjtyXtZ67XDDiBXdLiT1YW7Z/UysNz6FIAt1jPkxEnrX2WbP16puZmmL
a0/vCvwj4xCDlc3bosUkaVknwzaxf4Lb3m9oMKIRQcgovVRnKrq5YJaaPmjZG1Th
AGuazRuR/S1OF4sImNwmGoiLDvgra3TeEyLGb1j3eVysqmEulaa2zHVEQPI3OtSJ
E8Pp7sYqjguAMTWk2HsTMv42z1ITR9KX9JXvTapv15WMV4LB/0iHbGLQPT6uggCV
Eklndtf2q0jGFYjvSQIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQALS5ZQ0CjbXlZc
AX0vGxSlQ4jYoaDjrIqH5CizYGGVnmBkX/+2n1a+xp8kJp/76hiPoKiYBHX5I3tN
XQxU/1DDrjn8y7M5pS2PU/B5q+Uy3FgGnIU9J41hCdagUKeTupUvVtxqVuInpX5w
5JGW1eqLcCK5E5XBY85VpjdOWnOaZXuQmfgye/jKO4XtqDB20jtno7Jo3EIhllFT
SzxDk/Hfr8jeW9rd1/Q4/UCNOXPP2TllDnsRJtQzkY3h1sLtNRlGYMn5gFI3CbT/
xfbkKRmC8OmEyxe2m/qzMS2tJMadZEubuXtheA6W7YB76cmmVKIxL+h1ektVERTA
8doRA1AK
-----END CERTIFICATE-----
================================================
FILE: tests/ssl/server.csr
================================================
-----BEGIN CERTIFICATE REQUEST-----
MIICzzCCAbcCAQAwgYkxCzAJBgNVBAYTAklOMQ4wDAYDVQQIDAVJbmRpYTEOMAwG
A1UEBwwFSW5kaWExDDAKBgNVBAoMA0tCTjETMBEGA1UECwwKT3BlblNvdXJjZTES
MBAGA1UEAwwJMTI3LjAuMC4xMSMwIQYJKoZIhvcNAQkBFhRibmthbWFsZXNoQGdt
YWlsLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANrdA5AyI5QI
f5E87trvLSIaQUMo11G8NCr/E9dXlpbppLyASrjQnXou4ZBcS6KfRItCbdNfbhaU
c647cl7Weu1ww4gV3S4k9WFu2f1MrDc+hSALdYz5MRJ619lmz9eqbmZpi2tP7wr8
I+MQg5XN26LFJGlZJ8M2sX+C295vaDCiEUHIKL1UZyq6uWCWmj5o2RtU4QBrms0b
kf0tTheLCJjcJhqIiw74K2t03hMixm9Y93lcrKphLpWmtsx1REDyNzrUiRPD6e7G
Ko4LgDE1pNh7EzL+Ns9SE0fSl/SV702qb9eVjFeCwf9Ih2xi0D0+roIAlRJJZ3bX
9qtIxhWI70kCAwEAAaAAMA0GCSqGSIb3DQEBCwUAA4IBAQC2Jj9EyBFSzyQQLSu8
3MFbHHbaAcvZpjAmPuPFLlaY3j7iXwsGbK4JRHR4fKKZA/M9RTNq+v7QtMkIEyAK
dNKHjjK/zHfZbXCOYORFmfuR7xfG6FvjvLj2QtHasS0ogjDduqX/wt2aapR+2Q9W
vRgJVc4nCnyboA/f/u96hk45UxrA87g73bnaAiPgYcx4wUvhWqZOglw7nC38oj5g
CfelDqu+i4paf4pglfo8r/Dx3OAKIwFO4uHtGLPDNu27USwEO0/89dDNF5c6MmwI
cSCaEMY1AyRAOHm2jiPYEOtobRH07/SIhskNUszn6FwSOQuUDN00Cn3NsE2ZiPMd
4Gao
-----END CERTIFICATE REQUEST-----
================================================
FILE: tests/ssl/server.key
================================================
-----BEGIN RSA PRIVATE KEY-----
MIIEogIBAAKCAQEA2t0DkDIjlAh/kTzu2u8tIhpBQyjXUbw0Kv8T11eWlumkvIBK
uNCdei7hkFxLop9Ei0Jt019uFpRzrjtyXtZ67XDDiBXdLiT1YW7Z/UysNz6FIAt1
jPkxEnrX2WbP16puZmmLa0/vCvwj4xCDlc3bosUkaVknwzaxf4Lb3m9oMKIRQcgo
vVRnKrq5YJaaPmjZG1ThAGuazRuR/S1OF4sImNwmGoiLDvgra3TeEyLGb1j3eVys
qmEulaa2zHVEQPI3OtSJE8Pp7sYqjguAMTWk2HsTMv42z1ITR9KX9JXvTapv15WM
V4LB/0iHbGLQPT6uggCVEklndtf2q0jGFYjvSQIDAQABAoIBACcKCVqtNt9u7KJR
hiGTTC+rEz0RiebQdVW+DiH5Q6lDn9jn4Ww5+f0TY7TGYc9uLWHRxZlQimiIrmHD
xNDZ3S+BT790dvGGMibhCQ0/ofBwvHpM0PkGchRjySDEUAqeIfcumGnZ5j/FXflg
trf/8k+EbsxD1O3jUaH3C5UPtjwGOBsXnKUQiDIqd27QXCFCecDts2AVIP4uejPY
TqzdnK18k6WvCLs776RZmhDa0tWE0vgDuqYkAQgoYyRRtpWsQ1MyK2dUA9GVnenv
Q7NgOSaEEGRNx5Ko4uXQkNnW7xHxsQT45iIoDvw/GF5Mgii9ZweZoTou6EXNop58
udVa2akCgYEA91p2LeYyICq3vVLF3MWlytxs+asKTtja3NdwVZUMRGMat6GWO7D4
Eqzjt9lSow0d+JhVE9/hyS4MytmVhvRDAnw+cfmfhxlG2CSldeETjKFfAU8YkJQ+
pmx1lgbXqe9N2rUPqBMadMcQcJ/iTzvR1XmnPeKeG7sZdutJR3UVlWMCgYEA4oOZ
HR6WLvCJUnvA0hvli98s1dGrB3gNiwGuX+Qz1n5YbDByGpWnNgoBACe/rUvmoThk
QlYcmw3ItqGFq7TGImeoeOoQhTlpyMOYz28wDf5+8jdUJ5nNGFrfEsg2hUtsl3EO
CSp5iHjGmDxMow2cVHpA4hL6eQyCh7f0GaBkTmMCgYB70m4MhgqbraazAIeJ/+sB
xRxMU0HivI27NaHHRciRR2cte5dAJFPazW9lLkY+1yckteUJAO7/Da1bslY263nL
+bQsy//+2jlro9SsUNK/eFydxCGQ5pUCLJMkWiKFsASyMic3RPDeenQRXQgmD9T3
32FICnSJfzy9GgVh3wvB7wKBgFFRvV5e5LvlTud11juYGEimzonUw/nid7o32EpE
uvd+VHBC1DQHFgiofsN3gbDNVvb6L8RA9fQUdsJaKosCUz92x1zhaxzpB7kzv2B5
Il9jxl9eza+J37+mn/82MZyY/1s/EzLnNMpx0ZpFy52d/Um2uiRve8yJWTMwL0oj
8t7RAoGAcsazEWd6eJz1vmaeMT5YBUQeD82y9IHHFyp6h02wy2ackEjqNTiqjVWG
rzRDWJZWjTJkB+EY7/g7GqiEnAIJ4YqJccew9tJlaM2q58fvGCV/OLNYzzhBJkWo
6YpvCa+uQVJMFejL0WnSiewd9g9HCVi1EHNasb1Zp+BTgyfE1rs=
-----END RSA PRIVATE KEY-----
================================================
FILE: webgo.go
================================================
/*
Package webgo is a lightweight framework for building web apps. It has a multiplexer,
middleware plugging mechanism & context management of its own. The primary goal
of webgo is to get out of the developer's way as much as possible. i.e. it does
not enforce you to build your app in any particular pattern, instead just helps you
get all the trivial things done faster and easier.
e.g.
1. Getting named URI parameters.
2. Multiplexer for regex matching of URI and such.
3. Inject special app level configurations or any such objects to the request context as required.
*/
package webgo
import (
"context"
"crypto/tls"
"net/http"
)
var supportedHTTPMethods = []string{
http.MethodOptions,
http.MethodHead,
http.MethodGet,
http.MethodPost,
http.MethodPut,
http.MethodPatch,
http.MethodDelete,
}
// ctxkey is a custom string type to store the WebGo context inside HTTP request context
type ctxkey string
const wgoCtxKey = ctxkey("webgocontext")
// ContextPayload is the WebgoContext. A new instance of ContextPayload is injected inside every request's context object
type ContextPayload struct {
Route *Route
Err error
URIParams map[string]string
}
// Params returns the URI parameters of the respective route
func (cp *ContextPayload) Params() map[string]string {
return cp.URIParams
}
func (cp *ContextPayload) reset() {
cp.Route = nil
cp.Err = nil
}
// SetError sets the err within the context
func (cp *ContextPayload) SetError(err error) {
cp.Err = err
}
// Error returns the error set within the context
func (cp *ContextPayload) Error() error {
return cp.Err
}
// Context returns the ContextPayload injected inside the HTTP request context
func Context(r *http.Request) *ContextPayload {
return r.Context().Value(wgoCtxKey).(*ContextPayload)
}
// SetError is a helper function to set the error in webgo context
func SetError(r *http.Request, err error) {
ctx := Context(r)
ctx.SetError(err)
}
// GetError is a helper function to get the error from webgo context
func GetError(r *http.Request) error {
return Context(r).Error()
}
// ResponseStatus returns the response status code. It works only if the http.ResponseWriter
// is not wrapped in another response writer before calling ResponseStatus
func ResponseStatus(rw http.ResponseWriter) int {
crw, ok := rw.(*customResponseWriter)
if !ok {
return http.StatusOK
}
return crw.statusCode
}
func (router *Router) setupServer() {
cfg := router.config
router.httpsServer = &http.Server{
Addr: "",
Handler: router,
ReadTimeout: cfg.ReadTimeout,
WriteTimeout: cfg.WriteTimeout,
TLSConfig: &tls.Config{
InsecureSkipVerify: cfg.InsecureSkipVerify,
},
}
router.httpServer = &http.Server{
Addr: "",
Handler: router,
ReadTimeout: cfg.ReadTimeout,
WriteTimeout: cfg.WriteTimeout,
}
router.SetupMiddleware()
}
// SetupMiddleware initializes all the middleware added using "Use".
// This function need not be called explicitly, if using router.Start()
// or router.StartHTTPS(). Instead if the router is being passed to an external server
// then the SetupMiddleware function should be called
func (router *Router) SetupMiddleware() {
// load middleware for all routes
for _, routes := range router.allHandlers {
for _, route := range routes {
route.setupMiddleware(router.config.ReverseMiddleware)
}
}
}
// StartHTTPS starts the server with HTTPS enabled
func (router *Router) StartHTTPS() {
cfg := router.config
if cfg.CertFile == "" {
LOGHANDLER.Fatal("No certificate provided for HTTPS")
}
if cfg.KeyFile == "" {
LOGHANDLER.Fatal("No key file provided for HTTPS")
}
router.setupServer()
host := cfg.Host
if len(cfg.HTTPSPort) > 0 {
host += ":" + cfg.HTTPSPort
}
router.httpsServer.Addr = host
LOGHANDLER.Info("HTTPS server, listening on", router.httpsServer.Addr)
err := router.httpsServer.ListenAndServeTLS(cfg.CertFile, cfg.KeyFile)
if err != nil && err != http.ErrServerClosed {
LOGHANDLER.Error("HTTPS server exited with error:", err.Error())
}
}
// Start starts the HTTP server with the appropriate configurations
func (router *Router) Start() {
router.setupServer()
cfg := router.config
host := cfg.Host
if len(cfg.Port) > 0 {
host += ":" + cfg.Port
}
router.httpServer.Addr = host
LOGHANDLER.Info("HTTP server, listening on", router.httpServer.Addr)
err := router.httpServer.ListenAndServe()
if err != nil && err != http.ErrServerClosed {
LOGHANDLER.Error("HTTP server exited with error:", err.Error())
}
}
// Shutdown gracefully shuts down HTTP server
func (router *Router) Shutdown() error {
if router.httpServer == nil {
return nil
}
timer := router.config.ShutdownTimeout
ctx, cancel := context.WithTimeout(context.TODO(), timer)
defer cancel()
err := router.httpServer.Shutdown(ctx)
if err != nil {
LOGHANDLER.Error(err)
}
return err
}
// ShutdownHTTPS gracefully shuts down HTTPS server
func (router *Router) ShutdownHTTPS() error {
if router.httpsServer == nil {
return nil
}
timer := router.config.ShutdownTimeout
ctx, cancel := context.WithTimeout(context.TODO(), timer)
defer cancel()
err := router.httpsServer.Shutdown(ctx)
if err != nil && err != http.ErrServerClosed {
LOGHANDLER.Error(err)
}
return err
}
// OriginalResponseWriter returns the Go response writer stored within the webgo custom response
// writer
func OriginalResponseWriter(rw http.ResponseWriter) http.ResponseWriter {
crw, ok := rw.(*customResponseWriter)
if !ok {
return nil
}
return crw.ResponseWriter
}
================================================
FILE: webgo_test.go
================================================
/*
Package webgo is a lightweight framework for building web apps. It has a multiplexer,
middleware plugging mechanism & context management of its own. The primary goal
of webgo is to get out of the developer's way as much as possible. i.e. it does
not enforce you to build your app in any particular pattern, instead just helps you
get all the trivial things done faster and easier.
e.g.
1. Getting named URI parameters.
2. Multiplexer for regex matching of URI and such.
3. Inject special app level configurations or any such objects to the request context as required.
*/
package webgo
import (
"errors"
"net/http"
"net/http/httptest"
"testing"
"time"
)
func TestResponseStatus(t *testing.T) {
t.Parallel()
w := newCRW(httptest.NewRecorder(), http.StatusOK)
SendError(w, nil, http.StatusNotFound)
if http.StatusNotFound != ResponseStatus(w) {
t.Errorf(
"Expected status '%d', got '%d'",
http.StatusNotFound,
ResponseStatus(w),
)
}
// ideally we should get 200 from ResponseStatus; but it can get accurate status code only
// when `customresponsewriter` is used
rw := httptest.NewRecorder()
SendError(rw, nil, http.StatusNotFound)
if http.StatusOK != ResponseStatus(rw) {
t.Errorf(
"Expected status '%d', got '%d'",
http.StatusOK,
ResponseStatus(rw),
)
}
}
func TestStart(t *testing.T) {
t.Parallel()
router, _ := setup(t, "9696")
go router.Start()
time.Sleep(time.Second * 2)
err := router.Shutdown()
if err != nil {
t.Fatal(err)
}
}
func TestStartHTTPS(t *testing.T) {
t.Parallel()
router, _ := setup(t, "8443")
go router.StartHTTPS()
time.Sleep(time.Second * 2)
err := router.ShutdownHTTPS()
if err != nil {
t.Fatal(err)
}
}
func TestErrorHandling(t *testing.T) {
t.Parallel()
err := errors.New("hello world, failed")
router, _ := setup(t, "7878")
w := httptest.NewRecorder()
r, _ := http.NewRequest(http.MethodGet, "/", nil)
router.ServeHTTP(w, r)
SetError(r, err)
gotErr := GetError(r)
if !errors.Is(err, gotErr) {
t.Fatalf("expected err %v, got %v", err, gotErr)
}
}
func BenchmarkRouter(b *testing.B) {
GlobalLoggerConfig(nil, nil, LogCfgDisableDebug, LogCfgDisableInfo, LogCfgDisableWarn)
t := &testing.T{}
router, err := setup(t, "1595")
if err != nil {
b.Error(err)
return
}
w := httptest.NewRecorder()
r, _ := http.NewRequest(http.MethodGet, "/a/b/-/c/~/d/./e", nil)
for i := 0; i < b.N; i++ {
router.ServeHTTP(w, r)
if w.Result().StatusCode != http.StatusOK {
b.Error("expected status 200, got", w.Result().StatusCode)
return
}
}
}
gitextract_26pnjm80/ ├── .github/ │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ └── feature_request.md │ └── workflows/ │ └── go.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── _config.yml ├── cmd/ │ ├── README.md │ ├── certs/ │ │ ├── CA.key │ │ ├── CA.pem │ │ ├── CA.srl │ │ ├── localhost.crt │ │ ├── localhost.csr │ │ ├── localhost.decrypted.key │ │ ├── localhost.ext │ │ └── localhost.key │ ├── handlers.go │ ├── main.go │ └── static/ │ ├── css/ │ │ ├── main.css │ │ └── normalize.css │ ├── index.html │ └── js/ │ ├── main.js │ └── sse.js ├── config.go ├── config_test.go ├── errors.go ├── errors_test.go ├── extensions/ │ └── sse/ │ ├── README.md │ ├── client.go │ ├── message.go │ └── sse.go ├── go.mod ├── go.sum ├── middleware/ │ ├── accesslog/ │ │ ├── accesslog.go │ │ └── accesslog_test.go │ └── cors/ │ ├── cors.go │ └── cors_test.go ├── responses.go ├── responses_test.go ├── route.go ├── route_test.go ├── router.go ├── router_test.go ├── tests/ │ ├── config.json │ └── ssl/ │ ├── server.crt │ ├── server.csr │ └── server.key ├── webgo.go └── webgo_test.go
SYMBOL INDEX (219 symbols across 21 files)
FILE: cmd/handlers.go
function StaticFilesHandler (line 17) | func StaticFilesHandler(rw http.ResponseWriter, r *http.Request) {
function OriginalResponseWriterHandler (line 27) | func OriginalResponseWriterHandler(w http.ResponseWriter, r *http.Reques...
function HomeHandler (line 37) | func HomeHandler(w http.ResponseWriter, r *http.Request) {
function pushCSS (line 61) | func pushCSS(pusher http.Pusher, r *http.Request, path string) {
function pushJS (line 74) | func pushJS(pusher http.Pusher, r *http.Request, path string) {
function pushHomepage (line 87) | func pushHomepage(r *http.Request, w http.ResponseWriter) {
function SSEHandler (line 110) | func SSEHandler(sse *sse.SSE) http.HandlerFunc {
function ErrorSetterHandler (line 123) | func ErrorSetterHandler(w http.ResponseWriter, r *http.Request) {
function ParamHandler (line 130) | func ParamHandler(w http.ResponseWriter, r *http.Request) {
function InvalidJSONHandler (line 148) | func InvalidJSONHandler(w http.ResponseWriter, r *http.Request) {
FILE: cmd/main.go
function chain (line 22) | func chain(w http.ResponseWriter, r *http.Request) {
function errLogger (line 27) | func errLogger(w http.ResponseWriter, r *http.Request, next http.Handler...
function routegroupMiddleware (line 39) | func routegroupMiddleware(w http.ResponseWriter, r *http.Request, next h...
function getRoutes (line 44) | func getRoutes(sse *sse.SSE) []*webgo.Route {
function setup (line 106) | func setup() (*webgo.Router, *sse.SSE) {
function main (line 157) | func main() {
FILE: config.go
type Config (line 11) | type Config struct
method Load (line 42) | func (cfg *Config) Load(filepath string) {
method Validate (line 60) | func (cfg *Config) Validate() error {
FILE: config_test.go
function TestConfig_LoadInvalid (line 9) | func TestConfig_LoadInvalid(t *testing.T) {
function TestConfig_LoadValid (line 31) | func TestConfig_LoadValid(t *testing.T) {
function TestConfig_Validate (line 46) | func TestConfig_Validate(t *testing.T) {
FILE: errors.go
type logCfg (line 17) | type logCfg
constant LogCfgDisableDebug (line 21) | LogCfgDisableDebug = logCfg("disable-debug")
constant LogCfgDisableInfo (line 23) | LogCfgDisableInfo = logCfg("disable-info")
constant LogCfgDisableWarn (line 25) | LogCfgDisableWarn = logCfg("disable-warn")
constant LogCfgDisableError (line 27) | LogCfgDisableError = logCfg("disable-err")
constant LogCfgDisableFatal (line 29) | LogCfgDisableFatal = logCfg("disable-fatal")
type Logger (line 33) | type Logger interface
type logHandler (line 42) | type logHandler struct
method Debug (line 51) | func (lh *logHandler) Debug(data ...interface{}) {
method Info (line 59) | func (lh *logHandler) Info(data ...interface{}) {
method Warn (line 67) | func (lh *logHandler) Warn(data ...interface{}) {
method Error (line 75) | func (lh *logHandler) Error(data ...interface{}) {
method Fatal (line 83) | func (lh *logHandler) Fatal(data ...interface{}) {
function init (line 93) | func init() {
function loggerWithCfg (line 97) | func loggerWithCfg(stdout io.Writer, stderr io.Writer, cfgs ...logCfg) *...
function GlobalLoggerConfig (line 135) | func GlobalLoggerConfig(stdout io.Writer, stderr io.Writer, cfgs ...logC...
FILE: errors_test.go
function Test_loggerWithCfg (line 7) | func Test_loggerWithCfg(t *testing.T) {
FILE: extensions/sse/client.go
type ClientManager (line 8) | type ClientManager interface
type Client (line 23) | type Client struct
type eventType (line 30) | type eventType
method String (line 40) | func (et eventType) String() string {
constant eTypeNewClient (line 33) | eTypeNewClient eventType = iota
constant eTypeClientList (line 34) | eTypeClientList
constant eTypeRemoveClient (line 35) | eTypeRemoveClient
constant eTypeActiveClientCount (line 36) | eTypeActiveClientCount
constant eTypeClient (line 37) | eTypeClient
type event (line 54) | type event struct
type eventResponse (line 60) | type eventResponse struct
type Clients (line 65) | type Clients struct
method listener (line 71) | func (cs *Clients) listener(events <-chan event) {
method New (line 106) | func (cs *Clients) New(ctx context.Context, w http.ResponseWriter, cli...
method Range (line 123) | func (cs *Clients) Range(f func(cli *Client)) {
method Remove (line 137) | func (cs *Clients) Remove(clientID string) int {
method Active (line 150) | func (cs *Clients) Active() int {
method Clients (line 157) | func (cs *Clients) Clients() []*Client {
method Client (line 168) | func (cs *Clients) Client(clientID string) *Client {
function NewClientManager (line 178) | func NewClientManager() ClientManager {
FILE: extensions/sse/message.go
type Message (line 12) | type Message struct
method Bytes (line 26) | func (m *Message) Bytes() []byte {
function DefaultUnsupportedMessageHandler (line 48) | func DefaultUnsupportedMessageHandler(w http.ResponseWriter, r *http.Req...
FILE: extensions/sse/sse.go
type SSE (line 14) | type SSE struct
method Handler (line 39) | func (sse *SSE) Handler(w http.ResponseWriter, r *http.Request) error {
method HandlerFunc (line 87) | func (sse *SSE) HandlerFunc(w http.ResponseWriter, r *http.Request) {
method Broadcast (line 92) | func (sse *SSE) Broadcast(msg Message) {
method NewClient (line 98) | func (sse *SSE) NewClient(ctx context.Context, w http.ResponseWriter, ...
method ActiveClients (line 104) | func (sse *SSE) ActiveClients() int {
method RemoveClient (line 108) | func (sse *SSE) RemoveClient(ctx context.Context, clientID string) {
method Client (line 116) | func (sse *SSE) Client(id string) *Client {
function DefaultCreateHook (line 120) | func DefaultCreateHook(ctx context.Context, client *Client, count int) {}
function DefaultRemoveHook (line 121) | func DefaultRemoveHook(ctx context.Context, clientID string, count int) {}
function DefaultOnSend (line 122) | func DefaultOnSend(ctx context.Context, client *Client, err error) {}
function DefaultBeforeSend (line 123) | func DefaultBeforeSend(ctx context.Context, client *Client) {}
function New (line 125) | func New() *SSE {
FILE: middleware/accesslog/accesslog.go
function AccessLog (line 17) | func AccessLog(rw http.ResponseWriter, req *http.Request, next http.Hand...
FILE: middleware/accesslog/accesslog_test.go
function TestAccessLog (line 20) | func TestAccessLog(t *testing.T) {
function handler (line 71) | func handler(w http.ResponseWriter, r *http.Request) {
function setup (line 75) | func setup(port string) (*webgo.Router, error) {
FILE: middleware/cors/cors.go
constant headerOrigin (line 23) | headerOrigin = "Access-Control-Allow-Origin"
constant headerMethods (line 24) | headerMethods = "Access-Control-Allow-Methods"
constant headerCreds (line 25) | headerCreds = "Access-Control-Allow-Credentials"
constant headerAllowHeaders (line 26) | headerAllowHeaders = "Access-Control-Allow-Headers"
constant headerReqHeaders (line 27) | headerReqHeaders = "Access-Control-Request-Headers"
constant headerAccessControlAge (line 28) | headerAccessControlAge = "Access-Control-Max-Age"
constant allowHeaders (line 29) | allowHeaders = "Accept,Content-Type,Content-Length,Accept-Enco...
function allowedDomains (line 36) | func allowedDomains() []string {
function getReqOrigin (line 42) | func getReqOrigin(r *http.Request) string {
function allowedOriginsRegex (line 46) | func allowedOriginsRegex(allowedOrigins ...string) []regexp.Regexp {
function allowedMethods (line 89) | func allowedMethods(routes []*webgo.Route) string {
type Config (line 113) | type Config struct
function allowedHeaders (line 120) | func allowedHeaders(headers []string) string {
function allowOrigin (line 132) | func allowOrigin(reqOrigin string, allowedOriginRegex []regexp.Regexp) b...
function Middleware (line 144) | func Middleware(allowedOriginRegex []regexp.Regexp, corsTimeout, allowed...
function AddOptionsHandlers (line 176) | func AddOptionsHandlers(routes []*webgo.Route) []*webgo.Route {
function CORS (line 207) | func CORS(cfg *Config) webgo.Middleware {
FILE: middleware/cors/cors_test.go
function TestCORSEmptyconfig (line 24) | func TestCORSEmptyconfig(t *testing.T) {
function TestCORSWithConfig (line 107) | func TestCORSWithConfig(t *testing.T) {
function handler (line 211) | func handler(w http.ResponseWriter, r *http.Request) {
function getRoutes (line 215) | func getRoutes() []*webgo.Route {
function setup (line 225) | func setup(port string, routes []*webgo.Route) (*webgo.Router, error) {
FILE: responses.go
type ErrorData (line 15) | type ErrorData struct
type dOutput (line 21) | type dOutput struct
type errOutput (line 27) | type errOutput struct
constant HeaderContentType (line 34) | HeaderContentType = "Content-Type"
constant JSONContentType (line 36) | JSONContentType = "application/json"
constant HTMLContentType (line 38) | HTMLContentType = "text/html; charset=UTF-8"
constant ErrInternalServer (line 41) | ErrInternalServer = "Internal server error"
function SendHeader (line 45) | func SendHeader(w http.ResponseWriter, rCode int) {
function crwAsserter (line 49) | func crwAsserter(w http.ResponseWriter, rCode int) http.ResponseWriter {
function Send (line 60) | func Send(w http.ResponseWriter, contentType string, data interface{}, r...
function SendResponse (line 72) | func SendResponse(w http.ResponseWriter, data interface{}, rCode int) {
function SendError (line 88) | func SendError(w http.ResponseWriter, data interface{}, rCode int) {
function Render (line 104) | func Render(w http.ResponseWriter, data interface{}, rCode int, tpl *tem...
function R200 (line 119) | func R200(w http.ResponseWriter, data interface{}) {
function R201 (line 124) | func R201(w http.ResponseWriter, data interface{}) {
function R204 (line 129) | func R204(w http.ResponseWriter) {
function R302 (line 134) | func R302(w http.ResponseWriter, data interface{}) {
function R400 (line 139) | func R400(w http.ResponseWriter, data interface{}) {
function R403 (line 144) | func R403(w http.ResponseWriter, data interface{}) {
function R404 (line 149) | func R404(w http.ResponseWriter, data interface{}) {
function R406 (line 154) | func R406(w http.ResponseWriter, data interface{}) {
function R451 (line 159) | func R451(w http.ResponseWriter, data interface{}) {
function R500 (line 164) | func R500(w http.ResponseWriter, data interface{}) {
FILE: responses_test.go
function TestSendHeader (line 13) | func TestSendHeader(t *testing.T) {
function TestSendError (line 23) | func TestSendError(t *testing.T) {
function TestSendResponse (line 100) | func TestSendResponse(t *testing.T) {
function TestSend (line 176) | func TestSend(t *testing.T) {
function TestRender (line 215) | func TestRender(t *testing.T) {
function TestResponsehelpers (line 282) | func TestResponsehelpers(t *testing.T) {
FILE: route.go
type Route (line 11) | type Route struct
method parseURIWithParams (line 50) | func (r *Route) parseURIWithParams() {
method setupMiddleware (line 88) | func (r *Route) setupMiddleware(reverse bool) {
method init (line 111) | func (r *Route) init() error {
method matchPath (line 123) | func (r *Route) matchPath(requestURI string) (bool, map[string]string) {
method matchWithWildcard (line 140) | func (r *Route) matchWithWildcard(requestURI string) (bool, map[string...
method use (line 202) | func (r *Route) use(mm ...Middleware) {
type uriFragment (line 43) | type uriFragment struct
function routeServeChainedHandlers (line 209) | func routeServeChainedHandlers(r *Route) http.HandlerFunc {
function defaultRouteServe (line 226) | func defaultRouteServe(r *Route) http.HandlerFunc {
type RouteGroup (line 236) | type RouteGroup struct
method Add (line 245) | func (rg *RouteGroup) Add(rr ...Route) {
method Use (line 254) | func (rg *RouteGroup) Use(mm ...Middleware) {
method Routes (line 260) | func (rg *RouteGroup) Routes() []*Route {
function NewRouteGroup (line 264) | func NewRouteGroup(pathPrefix string, skipRouterMiddleware bool, rr ...R...
FILE: route_test.go
function TestRouteGroupsPathPrefix (line 10) | func TestRouteGroupsPathPrefix(t *testing.T) {
function dummyHandler (line 51) | func dummyHandler(w http.ResponseWriter, r *http.Request) {}
function BenchmarkMatchWithWildcard (line 53) | func BenchmarkMatchWithWildcard(b *testing.B) {
function TestMatchWithWildcard (line 79) | func TestMatchWithWildcard(t *testing.T) {
FILE: router.go
type httpResponseWriter (line 16) | type httpResponseWriter interface
function init (line 23) | func init() {
type customResponseWriter (line 62) | type customResponseWriter struct
method WriteHeader (line 71) | func (crw *customResponseWriter) WriteHeader(code int) {
method Write (line 83) | func (crw *customResponseWriter) Write(body []byte) (int, error) {
method Flush (line 90) | func (crw *customResponseWriter) Flush() {
method Hijack (line 97) | func (crw *customResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter...
method Push (line 105) | func (crw *customResponseWriter) Push(target string, opts *http.PushOp...
method reset (line 112) | func (crw *customResponseWriter) reset() {
type Middleware (line 120) | type Middleware
function discoverRoute (line 123) | func discoverRoute(path string, routes []*Route) (*Route, map[string]str...
type Router (line 133) | type Router struct
method methodRoutes (line 159) | func (rtr *Router) methodRoutes(method string) (routes []*Route) {
method ServeHTTP (line 180) | func (rtr *Router) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
method Use (line 223) | func (rtr *Router) Use(mm ...Middleware) {
method UseOnSpecialHandlers (line 237) | func (rtr *Router) UseOnSpecialHandlers(mm ...Middleware) {
method Add (line 264) | func (rtr *Router) Add(routes ...*Route) {
function newCRW (line 293) | func newCRW(rw http.ResponseWriter, rCode int) *customResponseWriter {
function releaseCRW (line 300) | func releaseCRW(crw *customResponseWriter) {
function newContext (line 305) | func newContext() *ContextPayload {
function releaseContext (line 309) | func releaseContext(cp *ContextPayload) {
function releasePoolResources (line 314) | func releasePoolResources(crw *customResponseWriter, cp *ContextPayload) {
function NewRouter (line 320) | func NewRouter(cfg *Config, routes ...*Route) *Router {
function checkDuplicateRoutes (line 335) | func checkDuplicateRoutes(idx int, route *Route, routes []*Route) {
function httpHandlers (line 370) | func httpHandlers(routes []*Route) map[string][]*Route {
FILE: router_test.go
function TestRouter_ServeHTTP (line 16) | func TestRouter_ServeHTTP(t *testing.T) {
function setup (line 128) | func setup(t *testing.T, port string) (*Router, error) {
function getRoutes (line 142) | func getRoutes(t *testing.T) []*Route {
function chainHandler (line 212) | func chainHandler(w http.ResponseWriter, r *http.Request) {
function chainNoFallthroughHandler (line 216) | func chainNoFallthroughHandler(w http.ResponseWriter, r *http.Request) {
function successHandler (line 221) | func successHandler(w http.ResponseWriter, r *http.Request) {
function checkPath (line 233) | func checkPath(req *http.Request, resp *httptest.ResponseRecorder) error {
function checkPathWildCard (line 262) | func checkPathWildCard(req *http.Request, resp *httptest.ResponseRecorde...
function checkParams (line 302) | func checkParams(req *http.Request, resp *httptest.ResponseRecorder, key...
function checkNotImplemented (line 333) | func checkNotImplemented(req *http.Request, resp *httptest.ResponseRecor...
function checkNotFound (line 344) | func checkNotFound(req *http.Request, resp *httptest.ResponseRecorder) e...
function checkChaining (line 355) | func checkChaining(req *http.Request, resp *httptest.ResponseRecorder) e...
function checkMiddleware (line 365) | func checkMiddleware(req *http.Request, resp *httptest.ResponseRecorder)...
function testTable (line 375) | func testTable() []struct {
type testLogger (line 582) | type testLogger struct
method Debug (line 586) | func (tl *testLogger) Debug(data ...interface{}) {
method Info (line 589) | func (tl *testLogger) Info(data ...interface{}) {
method Warn (line 592) | func (tl *testLogger) Warn(data ...interface{}) {
method Error (line 595) | func (tl *testLogger) Error(data ...interface{}) {
method Fatal (line 598) | func (tl *testLogger) Fatal(data ...interface{}) {
function Test_httpHandlers (line 602) | func Test_httpHandlers(t *testing.T) {
function TestWildcardMadness (line 651) | func TestWildcardMadness(t *testing.T) {
FILE: webgo.go
type ctxkey (line 32) | type ctxkey
constant wgoCtxKey (line 34) | wgoCtxKey = ctxkey("webgocontext")
type ContextPayload (line 37) | type ContextPayload struct
method Params (line 44) | func (cp *ContextPayload) Params() map[string]string {
method reset (line 48) | func (cp *ContextPayload) reset() {
method SetError (line 54) | func (cp *ContextPayload) SetError(err error) {
method Error (line 59) | func (cp *ContextPayload) Error() error {
function Context (line 64) | func Context(r *http.Request) *ContextPayload {
function SetError (line 69) | func SetError(r *http.Request, err error) {
function GetError (line 75) | func GetError(r *http.Request) error {
function ResponseStatus (line 81) | func ResponseStatus(rw http.ResponseWriter) int {
method setupServer (line 88) | func (router *Router) setupServer() {
method SetupMiddleware (line 112) | func (router *Router) SetupMiddleware() {
method StartHTTPS (line 122) | func (router *Router) StartHTTPS() {
method Start (line 148) | func (router *Router) Start() {
method Shutdown (line 166) | func (router *Router) Shutdown() error {
method ShutdownHTTPS (line 183) | func (router *Router) ShutdownHTTPS() error {
function OriginalResponseWriter (line 201) | func OriginalResponseWriter(rw http.ResponseWriter) http.ResponseWriter {
FILE: webgo_test.go
function TestResponseStatus (line 23) | func TestResponseStatus(t *testing.T) {
function TestStart (line 48) | func TestStart(t *testing.T) {
function TestStartHTTPS (line 58) | func TestStartHTTPS(t *testing.T) {
function TestErrorHandling (line 69) | func TestErrorHandling(t *testing.T) {
function BenchmarkRouter (line 85) | func BenchmarkRouter(b *testing.B) {
Condensed preview — 52 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (170K chars).
[
{
"path": ".github/FUNDING.yml",
"chars": 934,
"preview": "# These are supported funding model platforms\n\ngithub: [bnkamalesh] # Replace with up to 4 GitHub Sponsors-enabled usern"
},
{
"path": ".github/ISSUE_TEMPLATE/bug_report.md",
"chars": 834,
"preview": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Describe the b"
},
{
"path": ".github/ISSUE_TEMPLATE/feature_request.md",
"chars": 595,
"preview": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Is your fea"
},
{
"path": ".github/workflows/go.yml",
"chars": 939,
"preview": "# This workflow will build a golang project\n# For more information see: https://docs.github.com/en/actions/automating-bu"
},
{
"path": ".gitignore",
"chars": 1469,
"preview": "# Created by https://www.gitignore.io/api/go,osx,linux,windows\n\n### Go ###\n# Binaries for programs and plugins\n*.exe\n*.d"
},
{
"path": "CODE_OF_CONDUCT.md",
"chars": 3218,
"preview": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, w"
},
{
"path": "CONTRIBUTING.md",
"chars": 1101,
"preview": "Contributions are welcome from everyone. Please adhere to the [code of conduct](https://github.com/naughtygopher/webgo/b"
},
{
"path": "LICENSE",
"chars": 1071,
"preview": "MIT License\n\nCopyright (c) 2024 Naughty Gopher\n\nPermission is hereby granted, free of charge, to any person obtaining a "
},
{
"path": "README.md",
"chars": 9175,
"preview": "<p align=\"center\"><img src=\"https://user-images.githubusercontent.com/1092882/60883564-20142380-a268-11e9-988a-d98fb639a"
},
{
"path": "_config.yml",
"chars": 26,
"preview": "theme: jekyll-theme-cayman"
},
{
"path": "cmd/README.md",
"chars": 2559,
"preview": "# Webgo Sample\n\n### Server Sent Events\n\n => {\n const clientID = Math.random()\n .toString(36)\n .replace(/[^a-z]+/g, \"\")\n .substri"
},
{
"path": "cmd/static/js/sse.js",
"chars": 1573,
"preview": "const sse = (url, config = {}) => {\n const {\n onMessage,\n onError,\n initialBackoff = 10, // milliseconds\n m"
},
{
"path": "config.go",
"chars": 2035,
"preview": "package webgo\n\nimport (\n\t\"encoding/json\"\n\t\"os\"\n\t\"strconv\"\n\t\"time\"\n)\n\n// Config is used for reading app's configuration f"
},
{
"path": "config_test.go",
"chars": 2054,
"preview": "package webgo\n\nimport (\n\t\"bytes\"\n\t\"testing\"\n\t\"time\"\n)\n\nfunc TestConfig_LoadInvalid(t *testing.T) {\n\tt.Parallel()\n\ttl := "
},
{
"path": "errors.go",
"chars": 3124,
"preview": "package webgo\n\nimport (\n\t\"errors\"\n\t\"io\"\n\t\"log\"\n\t\"os\"\n)\n\nvar (\n\t// ErrInvalidPort is the error returned when the port num"
},
{
"path": "errors_test.go",
"chars": 651,
"preview": "package webgo\n\nimport (\n\t\"testing\"\n)\n\nfunc Test_loggerWithCfg(t *testing.T) {\n\tt.Parallel()\n\tcfgs := []logCfg{\n\t\tLogCfgD"
},
{
"path": "extensions/sse/README.md",
"chars": 2152,
"preview": "# Server-Sent Events\n\nThis extension provides support for [Server-Sent](https://developer.mozilla.org/en-US/docs/Web/API"
},
{
"path": "extensions/sse/client.go",
"chars": 3906,
"preview": "package sse\n\nimport (\n\t\"context\"\n\t\"net/http\"\n)\n\ntype ClientManager interface {\n\t// New should return a new client, and t"
},
{
"path": "extensions/sse/message.go",
"chars": 2055,
"preview": "package sse\n\nimport (\n\t\"bytes\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"time\"\n)\n\n// Message represents a valid SSE message\n// ref: https"
},
{
"path": "extensions/sse/sse.go",
"chars": 3943,
"preview": "// Package sse implements Server-Sent Events(SSE)\n// This extension is compliant with any net/http implementation, and i"
},
{
"path": "go.mod",
"chars": 50,
"preview": "module github.com/naughtygopher/webgo/v7\n\ngo 1.22\n"
},
{
"path": "go.sum",
"chars": 0,
"preview": ""
},
{
"path": "middleware/accesslog/accesslog.go",
"chars": 710,
"preview": "/*\nPackage accesslogs provides a simple straight forward access log middleware. The logs are of the\nfollowing format:\n<t"
},
{
"path": "middleware/accesslog/accesslog_test.go",
"chars": 2022,
"preview": "/*\nPackage accesslogs provides a simple straight forward access log middleware. The logs are of the\nfollowing format:\n<t"
},
{
"path": "middleware/cors/cors.go",
"chars": 6008,
"preview": "/*\nPackage cors sets the appropriate CORS(https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS)\nresponse headers, and "
},
{
"path": "middleware/cors/cors_test.go",
"chars": 5114,
"preview": "/*\nPackage cors sets the appropriate CORS(https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS)\nresponse headers, and "
},
{
"path": "responses.go",
"chars": 4979,
"preview": "package webgo\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"html/template\"\n\t\"net/http\"\n)\n\nvar (\n\tjsonErrPayload = []byte{}\n)\n\n// E"
},
{
"path": "responses_test.go",
"chars": 10172,
"preview": "package webgo\n\nimport (\n\t\"encoding/json\"\n\t\"html/template\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"reflect\"\n\t\"testing\"\n)"
},
{
"path": "route.go",
"chars": 6936,
"preview": "package webgo\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strings\"\n)\n\n// Route defines a route for each API\ntype Route struc"
},
{
"path": "route_test.go",
"chars": 6212,
"preview": "package webgo\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"reflect\"\n\t\"testing\"\n)\n\nfunc TestRouteGroupsPathPrefix(t *testing.T) {\n\tt.Pa"
},
{
"path": "router.go",
"chars": 10343,
"preview": "package webgo\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\t\"sync\"\n)\n\n// httpRespo"
},
{
"path": "router_test.go",
"chars": 16341,
"preview": "package webgo\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"strings\"\n\t\"t"
},
{
"path": "tests/config.json",
"chars": 217,
"preview": "{\n\t\"host\": \"127.0.0.1\",\n\t\"port\": \"9696\",\n\t\"httpsPort\": \"8443\",\n\t\"certFile\": \"./ssl/server.crt\",\n\t\"keyFile\": \"./ssl/serve"
},
{
"path": "tests/ssl/server.crt",
"chars": 1298,
"preview": "-----BEGIN CERTIFICATE-----\nMIIDkjCCAnoCCQDhnAb7Y802KzANBgkqhkiG9w0BAQsFADCBiTELMAkGA1UEBhMC\nSU4xDjAMBgNVBAgMBUluZGlhMQ4"
},
{
"path": "tests/ssl/server.csr",
"chars": 1050,
"preview": "-----BEGIN CERTIFICATE REQUEST-----\nMIICzzCCAbcCAQAwgYkxCzAJBgNVBAYTAklOMQ4wDAYDVQQIDAVJbmRpYTEOMAwG\nA1UEBwwFSW5kaWExDDA"
},
{
"path": "tests/ssl/server.key",
"chars": 1675,
"preview": "-----BEGIN RSA PRIVATE KEY-----\nMIIEogIBAAKCAQEA2t0DkDIjlAh/kTzu2u8tIhpBQyjXUbw0Kv8T11eWlumkvIBK\nuNCdei7hkFxLop9Ei0Jt019"
},
{
"path": "webgo.go",
"chars": 5548,
"preview": "/*\nPackage webgo is a lightweight framework for building web apps. It has a multiplexer,\nmiddleware plugging mechanism &"
},
{
"path": "webgo_test.go",
"chars": 2552,
"preview": "/*\nPackage webgo is a lightweight framework for building web apps. It has a multiplexer,\nmiddleware plugging mechanism &"
}
]
About this extraction
This page contains the full source code of the bnkamalesh/webgo GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 52 files (150.0 KB), approximately 47.6k tokens, and a symbol index with 219 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.