mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-11-30 16:11:39 +00:00
6433ba0ec3
Use [chi](https://github.com/go-chi/chi) instead of the forked [macaron](https://gitea.com/macaron/macaron). Since macaron and chi have conflicts with session share, this big PR becomes a have-to thing. According my previous idea, we can replace macaron step by step but I'm wrong. :( Below is a list of big changes on this PR. - [x] Define `context.ResponseWriter` interface with an implementation `context.Response`. - [x] Use chi instead of macaron, and also a customize `Route` to wrap chi so that the router usage is similar as before. - [x] Create different routers for `web`, `api`, `internal` and `install` so that the codes will be more clear and no magic . - [x] Use https://github.com/unrolled/render instead of macaron's internal render - [x] Use https://github.com/NYTimes/gziphandler instead of https://gitea.com/macaron/gzip - [x] Use https://gitea.com/go-chi/session which is a modified version of https://gitea.com/macaron/session and removed `nodb` support since it will not be maintained. **BREAK** - [x] Use https://gitea.com/go-chi/captcha which is a modified version of https://gitea.com/macaron/captcha - [x] Use https://gitea.com/go-chi/cache which is a modified version of https://gitea.com/macaron/cache - [x] Use https://gitea.com/go-chi/binding which is a modified version of https://gitea.com/macaron/binding - [x] Use https://github.com/go-chi/cors instead of https://gitea.com/macaron/cors - [x] Dropped https://gitea.com/macaron/i18n and make a new one in `code.gitea.io/gitea/modules/translation` - [x] Move validation form structs from `code.gitea.io/gitea/modules/auth` to `code.gitea.io/gitea/modules/forms` to avoid dependency cycle. - [x] Removed macaron log service because it's not need any more. **BREAK** - [x] All form structs have to be get by `web.GetForm(ctx)` in the route function but not as a function parameter on routes definition. - [x] Move Git HTTP protocol implementation to use routers directly. - [x] Fix the problem that chi routes don't support trailing slash but macaron did. - [x] `/api/v1/swagger` now will be redirect to `/api/swagger` but not render directly so that `APIContext` will not create a html render. Notices: - Chi router don't support request with trailing slash - Integration test `TestUserHeatmap` maybe mysql version related. It's failed on my macOS(mysql 5.7.29 installed via brew) but succeed on CI. Co-authored-by: 6543 <6543@obermui.de>
249 lines
6.4 KiB
Go
Vendored
249 lines
6.4 KiB
Go
Vendored
// Copyright 2013 Beego Authors
|
|
// Copyright 2014 The Macaron Authors
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
|
// not use this file except in compliance with the License. You may obtain
|
|
// a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
// License for the specific language governing permissions and limitations
|
|
// under the License.
|
|
|
|
// Package captcha a middleware that provides captcha service for Macaron.
|
|
package captcha
|
|
|
|
import (
|
|
"fmt"
|
|
"html/template"
|
|
"image/color"
|
|
"net/http"
|
|
"path"
|
|
"strings"
|
|
|
|
"gitea.com/go-chi/cache"
|
|
"github.com/unknwon/com"
|
|
)
|
|
|
|
const _VERSION = "0.1.0"
|
|
|
|
func Version() string {
|
|
return _VERSION
|
|
}
|
|
|
|
var (
|
|
defaultChars = []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
|
|
)
|
|
|
|
// Captcha represents a captcha service.
|
|
type Captcha struct {
|
|
Store cache.Cache
|
|
SubURL string
|
|
URLPrefix string
|
|
FieldIdName string
|
|
FieldCaptchaName string
|
|
StdWidth int
|
|
StdHeight int
|
|
ChallengeNums int
|
|
Expiration int64
|
|
CachePrefix string
|
|
ColorPalette color.Palette
|
|
}
|
|
|
|
// generate key string
|
|
func (c *Captcha) key(id string) string {
|
|
return c.CachePrefix + id
|
|
}
|
|
|
|
// generate rand chars with default chars
|
|
func (c *Captcha) genRandChars() string {
|
|
return string(com.RandomCreateBytes(c.ChallengeNums, defaultChars...))
|
|
}
|
|
|
|
// CreateHTML outputs HTML for display and fetch new captcha images.
|
|
func (c *Captcha) CreateHTML() template.HTML {
|
|
value, err := c.CreateCaptcha()
|
|
if err != nil {
|
|
panic(fmt.Errorf("fail to create captcha: %v", err))
|
|
}
|
|
return template.HTML(fmt.Sprintf(`<input type="hidden" name="%[1]s" value="%[2]s">
|
|
<a class="captcha" href="javascript:" tabindex="-1">
|
|
<img onclick="this.src=('%[3]s%[4]s%[2]s.png?reload='+(new Date()).getTime())" class="captcha-img" src="%[3]s%[4]s%[2]s.png">
|
|
</a>`, c.FieldIdName, value, c.SubURL, c.URLPrefix))
|
|
}
|
|
|
|
// create a new captcha id
|
|
func (c *Captcha) CreateCaptcha() (string, error) {
|
|
id := string(com.RandomCreateBytes(15))
|
|
if err := c.Store.Put(c.key(id), c.genRandChars(), c.Expiration); err != nil {
|
|
return "", err
|
|
}
|
|
return id, nil
|
|
}
|
|
|
|
// verify from a request
|
|
func (c *Captcha) VerifyReq(req *http.Request) bool {
|
|
req.ParseForm()
|
|
return c.Verify(req.Form.Get(c.FieldIdName), req.Form.Get(c.FieldCaptchaName))
|
|
}
|
|
|
|
// direct verify id and challenge string
|
|
func (c *Captcha) Verify(id string, challenge string) bool {
|
|
if len(challenge) == 0 || len(id) == 0 {
|
|
return false
|
|
}
|
|
|
|
var chars string
|
|
|
|
key := c.key(id)
|
|
|
|
if v, ok := c.Store.Get(key).(string); ok {
|
|
chars = v
|
|
} else {
|
|
return false
|
|
}
|
|
|
|
defer c.Store.Delete(key)
|
|
|
|
if len(chars) != len(challenge) {
|
|
return false
|
|
}
|
|
|
|
// verify challenge
|
|
for i, c := range []byte(chars) {
|
|
if c != challenge[i]-48 {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
type Options struct {
|
|
// Suburl path. Default is empty.
|
|
SubURL string
|
|
// URL prefix of getting captcha pictures. Default is "/captcha/".
|
|
URLPrefix string
|
|
// Hidden input element ID. Default is "captcha_id".
|
|
FieldIdName string
|
|
// User input value element name in request form. Default is "captcha".
|
|
FieldCaptchaName string
|
|
// Challenge number. Default is 6.
|
|
ChallengeNums int
|
|
// Captcha image width. Default is 240.
|
|
Width int
|
|
// Captcha image height. Default is 80.
|
|
Height int
|
|
// Captcha expiration time in seconds. Default is 600.
|
|
Expiration int64
|
|
// Cache key prefix captcha characters. Default is "captcha_".
|
|
CachePrefix string
|
|
// ColorPalette holds a collection of primary colors used for
|
|
// the captcha's text. If not defined, a random color will be generated.
|
|
ColorPalette color.Palette
|
|
}
|
|
|
|
func prepareOptions(options []Options) Options {
|
|
var opt Options
|
|
if len(options) > 0 {
|
|
opt = options[0]
|
|
}
|
|
|
|
opt.SubURL = strings.TrimSuffix(opt.SubURL, "/")
|
|
|
|
// Defaults.
|
|
if len(opt.URLPrefix) == 0 {
|
|
opt.URLPrefix = "/captcha/"
|
|
} else if opt.URLPrefix[len(opt.URLPrefix)-1] != '/' {
|
|
opt.URLPrefix += "/"
|
|
}
|
|
if len(opt.FieldIdName) == 0 {
|
|
opt.FieldIdName = "captcha_id"
|
|
}
|
|
if len(opt.FieldCaptchaName) == 0 {
|
|
opt.FieldCaptchaName = "captcha"
|
|
}
|
|
if opt.ChallengeNums == 0 {
|
|
opt.ChallengeNums = 6
|
|
}
|
|
if opt.Width == 0 {
|
|
opt.Width = stdWidth
|
|
}
|
|
if opt.Height == 0 {
|
|
opt.Height = stdHeight
|
|
}
|
|
if opt.Expiration == 0 {
|
|
opt.Expiration = 600
|
|
}
|
|
if len(opt.CachePrefix) == 0 {
|
|
opt.CachePrefix = "captcha_"
|
|
}
|
|
|
|
return opt
|
|
}
|
|
|
|
// NewCaptcha initializes and returns a captcha with given options.
|
|
func NewCaptcha(opts ...Options) *Captcha {
|
|
opt := prepareOptions(opts)
|
|
return &Captcha{
|
|
SubURL: opt.SubURL,
|
|
URLPrefix: opt.URLPrefix,
|
|
FieldIdName: opt.FieldIdName,
|
|
FieldCaptchaName: opt.FieldCaptchaName,
|
|
StdWidth: opt.Width,
|
|
StdHeight: opt.Height,
|
|
ChallengeNums: opt.ChallengeNums,
|
|
Expiration: opt.Expiration,
|
|
CachePrefix: opt.CachePrefix,
|
|
ColorPalette: opt.ColorPalette,
|
|
}
|
|
}
|
|
|
|
// Captchaer is a middleware that maps a captcha.Captcha service into the Macaron handler chain.
|
|
// An single variadic captcha.Options struct can be optionally provided to configure.
|
|
// This should be register after cache.Cacher.
|
|
func Captchaer(cpt *Captcha) func(http.Handler) http.Handler {
|
|
return func(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
|
if strings.HasPrefix(req.URL.Path, cpt.URLPrefix) {
|
|
var chars string
|
|
id := path.Base(req.URL.Path)
|
|
if i := strings.Index(id, "."); i > -1 {
|
|
id = id[:i]
|
|
}
|
|
key := cpt.key(id)
|
|
|
|
reloads := req.URL.Query()["reload"]
|
|
// Reload captcha.
|
|
if len(reloads) > 0 && len(reloads[0]) > 0 {
|
|
chars = cpt.genRandChars()
|
|
if err := cpt.Store.Put(key, chars, cpt.Expiration); err != nil {
|
|
w.WriteHeader(500)
|
|
w.Write([]byte("captcha reload error"))
|
|
panic(fmt.Errorf("reload captcha: %v", err))
|
|
}
|
|
} else {
|
|
if v, ok := cpt.Store.Get(key).(string); ok {
|
|
chars = v
|
|
} else {
|
|
w.WriteHeader(404)
|
|
w.Write([]byte("captcha not found"))
|
|
return
|
|
}
|
|
}
|
|
|
|
w.WriteHeader(200)
|
|
if _, err := NewImage([]byte(chars), cpt.StdWidth, cpt.StdHeight, cpt.ColorPalette).WriteTo(w); err != nil {
|
|
panic(fmt.Errorf("write captcha: %v", err))
|
|
}
|
|
return
|
|
}
|
|
|
|
next.ServeHTTP(w, req)
|
|
})
|
|
}
|
|
}
|