This commit is contained in:
Skeris 2021-05-15 17:03:10 +03:00
parent 3858816837
commit 36612dd31b
9 changed files with 319 additions and 286 deletions

@ -3,7 +3,9 @@ package app
import (
"bitbucket.org/skeris/heruvym/dal"
"bitbucket.org/skeris/heruvym/middleware"
"bitbucket.org/skeris/heruvym/router"
"bitbucket.org/skeris/heruvym/service"
"bitbucket.org/skeris/heruvym/tools"
"bitbucket.org/skeris/heruvym/version"
rAL "bitbucket.org/skeris/profile/dal"
"context"
@ -11,7 +13,6 @@ import (
"fmt"
"github.com/BlackBroker/trashlog/wrappers/zaptg"
"github.com/skeris/appInit"
"github.com/skeris/authService/router"
"go.uber.org/zap/zapcore"
"net/http"
"os"
@ -116,8 +117,6 @@ func New(ctx context.Context, opts interface{}) (appInit.CommonApp, error) {
logger := hlog.New(zapLogger)
logger.Emit(InfoSvcStarted{})
apiMux := router.NewRouter(http.NewServeMux())
database, err := dal.New(
ctx,
options.MongoURI,
@ -140,11 +139,39 @@ func New(ctx context.Context, opts interface{}) (appInit.CommonApp, error) {
interrupt := make(chan os.Signal, 1)
signal.Notify(interrupt, os.Interrupt, syscall.SIGTERM)
mux := router.NewRouter(map[string]http.HandlerFunc{
"/support/create": heruvym.CreateTicket,
"/support/subscribe": tools.SseWrapper(heruvym.GetList),
"/support/ticket": tools.SseWrapper(heruvym.Subscribe),
"/support/send": tools.HandlerWrapper(heruvym.PutMessage),
"/support/getTickets": tools.HandlerWrapper(heruvym.GetTickets),
"/support/getMessages": tools.HandlerWrapper(heruvym.GetMessages),
"/support/pick": tools.HandlerWrapper(heruvym.Pick),
"/support/delegate": tools.HandlerWrapper(heruvym.Delegate),
"/support/vote": tools.HandlerWrapper(heruvym.Vote),
"/support/close": tools.HandlerWrapper(heruvym.CloseTicket),
})
mw := middleware.NewMiddleware(
logger,
nil,
"*",
nil,
)
mux.Use(
mw.MiddlewareLogger,
mw.MiddlewareOriginAccess,
mw.MiddlewareRecovery,
mw.MiddlewareJwt,
mw.MiddlewareGetJwt,
//mw.MiddlewareJwtPlug,
//mw.MiddlewareRoleAccess,
)
server := &http.Server{
Handler: http.StripPrefix(
"/support",
http_middleware.Wrap(heruvym.Register(apiMux), zapLogger),
),
Handler: mux,
Addr: fmt.Sprintf(":%s", options.NumberPortLocal),
}

1
go.mod

@ -6,6 +6,7 @@ require (
bitbucket.org/skeris/profile v0.0.0
github.com/BlackBroker/trashlog v0.0.0-20210406151703-e2c4874359bf
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/pkg/errors v0.9.1 // indirect
github.com/rs/xid v1.3.0
github.com/skeris/appInit v0.1.12
github.com/skeris/authService v1.1.1

@ -1,66 +0,0 @@
package hijack
import (
"bufio"
"net"
"net/http"
"sync"
)
func New(w http.ResponseWriter, commit func(http.ResponseWriter)) (http.ResponseWriter, func()) {
responseWriter := &responseWriter{
commit: commit,
w: w,
}
if _, ok := w.(http.Hijacker); ok {
return &responseWriterHijacker{
responseWriter: responseWriter,
}, responseWriter.ensureCommitted
} else {
return responseWriter, responseWriter.ensureCommitted
}
}
var _ http.ResponseWriter = new(responseWriter)
var _ http.ResponseWriter = new(responseWriterHijacker)
var _ http.Hijacker = new(responseWriterHijacker)
type responseWriter struct {
commit func(http.ResponseWriter)
w http.ResponseWriter
hijacked bool
writeOnce sync.Once
}
func (rw *responseWriter) ensureCommitted() {
rw.writeOnce.Do(func() {
if rw.hijacked {
return
}
rw.commit(rw.w)
})
}
func (rw *responseWriter) Header() http.Header {
return rw.w.Header()
}
func (rw *responseWriter) Write(data []byte) (int, error) {
rw.ensureCommitted()
return rw.w.Write(data)
}
func (rw *responseWriter) WriteHeader(status int) {
rw.ensureCommitted()
rw.w.WriteHeader(status)
}
type responseWriterHijacker struct {
*responseWriter
}
func (rw *responseWriterHijacker) Hijack() (net.Conn, *bufio.ReadWriter, error) {
rw.hijacked = true
return rw.w.(http.Hijacker).Hijack()
}

36
middleware/hlogger.go Normal file

@ -0,0 +1,36 @@
package middleware
type ErrorHandlerRecovered struct {
Recovered interface{}
}
type ErrorWritingPanicResponse struct {
Err error
}
type ErrorPanicInHttpHandler struct {
Code int
Message string
Recovered interface{}
}
type DebugHttpRequest struct {
Url string
}
type ErrorOriginAccess struct {
Origin string
Url string
}
type ErrorJwtAccess struct {
Err error
}
type ErrorJwtEncode struct {
Err error
}
type ErrorRoleAccess struct {
Err error
}

@ -1,242 +1,205 @@
package http_middleware
package middleware
import (
"bitbucket.org/skeris/heruvym/jwt_adapter"
"bitbucket.org/skeris/heruvym/middleware/hijack"
"bitbucket.org/skeris/profile/dal"
"bitbucket.org/skeris/profile/errors"
"bitbucket.org/skeris/profile/jwt_adapter"
"context"
"fmt"
"github.com/skeris/authService/errors"
errors2 "github.com/pkg/errors"
"github.com/themakers/hlog"
"net/http"
"runtime/debug"
"strings"
"go.uber.org/zap"
)
type MiddlewareFunc func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc)
// My MiddleWare with gorilla/mux
type AfterFunc func(ctx context.Context, r *http.Request) (context.Context, error)
type SetCookieValueFunc func(w http.ResponseWriter, value string)
func DefaultChain(
log *zap.Logger,
recFn RecoverFunc,
afn AfterFunc,
setCookieValue SetCookieValueFunc,
mws ...MiddlewareFunc,
) http.HandlerFunc {
return Chain(
append(
[]MiddlewareFunc{
MiddlewareRecovery(log, recFn),
MiddlewareLogger(log),
DefaultCookieAndRecoveryMiddleware(
log,
recFn, afn, setCookieValue,
),
},
mws...,
)...,
)
type Middleware struct {
logger hlog.Logger
mongo dal.LayerMongoDb
allowedOrigins string
allowedRoles map[string]string // key - path, value - roles
}
func DefaultCookieAndRecoveryMiddleware(
log *zap.Logger,
recFn RecoverFunc,
afn AfterFunc,
setCookieValue SetCookieValueFunc,
) MiddlewareFunc {
func NewMiddleware(
logger hlog.Logger,
mongo dal.LayerMongoDb,
allowedOrigins string,
allowedRoles map[string]string,
) *Middleware {
return &Middleware{
logger: logger,
mongo: mongo,
allowedOrigins: allowedOrigins,
allowedRoles: allowedRoles,
}
}
const headerKey = jwt_adapter.DefaultHeaderKey
func (mw *Middleware) MiddlewareLogger(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
mw.logger.Emit(DebugHttpRequest{Url: r.URL.String()})
next.ServeHTTP(w, r)
})
}
return func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
var (
err error
tokenHeader string
)
cookie := &jwt_adapter.JwtAdapter{}
func (mw *Middleware) MiddlewareOriginAccess(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if len(r.Header["Origin"]) > 0 {
if mw.allowedOrigins != "*" && !strings.Contains(mw.allowedOrigins, r.Header["Origin"][0]) {
mw.logger.Emit(ErrorOriginAccess{Origin: r.Header["Origin"][0], Url: r.URL.String()})
return
}
}
next.ServeHTTP(w, r)
})
}
if r.Method == http.MethodGet {
tokenHeader = fmt.Sprintf("Bearer %s", r.Form.Get(headerKey))
func recFn(rec interface{}) (int, string) {
var (
code int
message string
)
if err, ok := rec.(error); ok {
if v, ok := errors.IsForbidden(err); ok {
code = http.StatusForbidden
message = v.Error()
} else if v, ok := errors.IsUnauthenticated(err); ok {
code = http.StatusUnauthorized
message = v.Error()
} else {
tokenHeader = r.Header.Get(headerKey)
code = http.StatusInternalServerError
message = err.Error()
}
} else {
code = http.StatusInternalServerError
message = fmt.Sprintf("%v", rec)
}
return code, message
}
func (mw *Middleware) MiddlewareRecovery(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if rec := recover(); rec != nil {
code, message := recFn(rec)
w.WriteHeader(code)
if _, err := fmt.Fprint(w, message); err != nil {
mw.logger.Emit(ErrorWritingPanicResponse{Err: err})
}
mw.logger.Emit(ErrorPanicInHttpHandler{
Code: code,
Message: message,
Recovered: rec,
})
}
}()
next.ServeHTTP(w, r)
})
}
func (mw *Middleware) MiddlewareJwt(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Escape GET requests
if r.Method == http.MethodGet {
next.ServeHTTP(w, r)
return
}
if tokenHeader == "" {
fmt.Println("ERROR NO authHEader")
cookie.Init()
} else {
if len(r.Header[jwt_adapter.DefaultHeaderKey]) <= 0 {
mw.logger.Emit(ErrorJwtAccess{Err: errors2.New(jwt_adapter.DefaultHeaderKey + "header missing")})
w.WriteHeader(http.StatusUnauthorized)
return
}
splitted := strings.Split(tokenHeader, " ")
if len(splitted) != 2 {
w.WriteHeader(http.StatusForbidden)
w.Header().Add("Content-Type", "application/json")
token := r.Header[jwt_adapter.DefaultHeaderKey][0]
token = strings.Replace(token, "Bearer ", "", -1)
adapter, err := jwt_adapter.Decode(token)
if err != nil {
mw.logger.Emit(ErrorJwtAccess{Err: err})
w.WriteHeader(http.StatusUnauthorized)
return
}
err = setJwtHeader(adapter, w, mw.logger)
if err != nil {
mw.logger.Emit(ErrorJwtAccess{Err: err})
w.WriteHeader(http.StatusUnauthorized)
return
}
ctx := context.WithValue(r.Context(), "JWT", adapter)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func getJwtUserId(r *http.Request) (string, error) {
if jwtAdapter, ok := r.Context().Value("JWT").(*jwt_adapter.JwtAdapter); ok {
return jwtAdapter.User, nil
}
return "", errors2.New("no token in context")
}
func (mw *Middleware) MiddlewareRoleAccess(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Если доступ по роли задан
if allowedRoles, ok := mw.allowedRoles[r.URL.Path]; ok {
// Если роли не указаны
if allowedRoles == "" {
next.ServeHTTP(w, r)
return
}
tokenPart := splitted[1]
id, err := getJwtUserId(r)
cookie, err = jwt_adapter.Decode(tokenPart)
if err != nil {
cookie.Init()
mw.logger.Emit(ErrorRoleAccess{Err: err})
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
}
recovery := struct {
val interface{}
trace string
}{}
role, err := mw.mongo.GetProfileRole(r.Context(), id)
ctx := context.WithValue(r.Context(), headerKey, cookie)
if afn != nil {
c, err := afn(ctx, r)
if err != nil {
panic(err)
mw.logger.Emit(ErrorRoleAccess{Err: err})
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
// Если у пользователя не задана роль - блокируем доступ
if role == "" {
err = errors.UserHaveNoRole("User have no role")
mw.logger.Emit(ErrorRoleAccess{err})
http.Error(w, err.Error(), http.StatusForbidden)
return
}
// Если указан астериск - доступ имеет любая роль
if !(allowedRoles == "*" || strings.Contains(allowedRoles, role)) {
err = errors.UserHaveNoRole("User role not allowed")
mw.logger.Emit(ErrorRoleAccess{err})
http.Error(w, err.Error(), http.StatusForbidden)
return
}
ctx = c
}
w, commit := hijack.New(w, func(w http.ResponseWriter) {
cookie.LastSeen = jwt_adapter.Timestamp()
if val, err := cookie.Encode(); err != nil {
panic(err)
} else {
setCookieValue(w, val)
}
if recovery.val != nil {
log.Error("handler recovered", zap.Any("recovered", recovery.val))
code, message := recFn(recovery.val, ctx)
w.WriteHeader(code)
if _, err := fmt.Fprint(w, message); err != nil {
log.Error("error writing panic response", zap.Error(err))
}
}
})
defer func() {
if rec := recover(); rec != nil {
recovery.val = rec
recovery.trace = string(debug.Stack())
}
commit()
}()
next(w, r.WithContext(ctx))
}
next.ServeHTTP(w, r)
})
}
func Chain(mws ...MiddlewareFunc) http.HandlerFunc {
if len(mws) == 0 {
return func(w http.ResponseWriter, r *http.Request) {}
}
h := link(mws[len(mws)-1], nil)
for i := len(mws) - 2; i >= 0; i-- {
mw := mws[i]
// MiddlewareJwtPlug jwt заглушка для отладки кода, удалить в релизе
func (mw *Middleware) MiddlewareJwtPlug(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
adapter := jwt_adapter.JwtAdapter{ID: "604b79aced1d431b9e911f56"}
adapter.Init()
adapter.SetUserID("604b79aced1d431b9e911f56")
ctx := context.WithValue(r.Context(), "JWT", &adapter)
h = link(mw, h)
}
return h
}
func link(mw MiddlewareFunc, next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
mw(w, r, next)
}
}
type RecoverFunc func(rec interface{}, ctx context.Context) (code int, message string)
func MiddlewareRecovery(log *zap.Logger, recFn RecoverFunc) MiddlewareFunc {
return func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
defer func() {
if rec := recover(); rec != nil {
code, message := recFn(rec, r.Context())
w.WriteHeader(code)
if _, err := fmt.Fprint(w, message); err != nil {
log.Error("error writing panic response", zap.Error(err))
}
log.Error("panic in http handler", zap.Int("code", code), zap.String("message", message), zap.Any("recovered", rec))
}
}()
next(w, r)
}
}
func MiddlewareLogger(log *zap.Logger) MiddlewareFunc {
return func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
log.Debug("http request", zap.String("url", r.URL.String()))
next(w, r)
}
}
func Handler(h http.HandlerFunc) MiddlewareFunc {
return func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
h.ServeHTTP(w, r)
if next != nil {
next(w, r)
}
}
}
func Wrap(mux *http.ServeMux, logger *zap.Logger) http.HandlerFunc {
return DefaultChain(
logger,
func(rec interface{}, ctx context.Context) (int, string) {
var (
code int
message string
)
if err, ok := rec.(error); ok {
if v, ok := errors.IsForbidden(err); ok {
code = http.StatusForbidden
message = v.Error()
} else if v, ok := errors.IsUnauthenticated(err); ok {
code = http.StatusUnauthorized
message = v.Error()
} else {
code = http.StatusInternalServerError
message = err.Error()
}
} else {
code = http.StatusInternalServerError
message = fmt.Sprintf("%v", rec)
}
return code, message
},
nil,
func(w http.ResponseWriter, val string) {
var setFn func(key, value string)
if w.Header().Get(jwt_adapter.DefaultHeaderKey) == "" {
setFn = w.Header().Add
} else {
setFn = w.Header().Set
}
setFn(jwt_adapter.DefaultHeaderKey, fmt.Sprintf("Bearer %s", val))
},
Handler((func() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
//todo specify origins
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Credentials", "true")
w.Header().Set("Access-Control-Allow-Headers", "*")
w.Header().Set("Access-Control-Expose-Headers", "*")
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
w.Header().Set("Pragma", "no-cache")
w.Header().Set("Expires", "0")
mux.ServeHTTP(w, r)
}
})()))
next.ServeHTTP(w, r.WithContext(ctx))
})
}

20
middleware/middleware.go Normal file

@ -0,0 +1,20 @@
package middleware
import (
"bitbucket.org/skeris/profile/jwt_adapter"
"github.com/themakers/hlog"
"net/http"
)
func setJwtHeader(adapter *jwt_adapter.JwtAdapter, w http.ResponseWriter, logger hlog.Logger) error {
adapter.LastSeen = jwt_adapter.Timestamp()
token, err := adapter.Encode()
if err != nil {
logger.Emit(ErrorJwtEncode{Err: err})
return err
}
w.Header().Set(jwt_adapter.DefaultHeaderKey, token)
return nil
}

@ -0,0 +1,29 @@
package middleware
import (
"bitbucket.org/skeris/profile/jwt_adapter"
"context"
"net/http"
)
func (mw *Middleware) MiddlewareGetJwt(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Escape non-GET requests
if r.Method != http.MethodGet {
next.ServeHTTP(w, r)
return
}
bearer := r.URL.Query().Get(jwt_adapter.DefaultHeaderKey)
adapter, err := jwt_adapter.Decode(bearer)
if err != nil {
mw.logger.Emit(ErrorJwtAccess{Err: err})
w.WriteHeader(http.StatusUnauthorized)
return
}
ctx := context.WithValue(r.Context(), "JWT", adapter)
next.ServeHTTP(w, r.WithContext(ctx))
})
}

18
router/router.go Normal file

@ -0,0 +1,18 @@
package router
import (
"github.com/gorilla/mux"
"net/http"
)
func NewRouter(additional... map[string]http.HandlerFunc) *mux.Router {
apiMux := mux.NewRouter()
for _, handler := range additional {
for endpoint, handler := range handler {
apiMux.HandleFunc(endpoint, handler)
}
}
return apiMux
}

@ -6,7 +6,6 @@ import (
"fmt"
"net/http"
"reflect"
"strings"
"time"
)
@ -15,7 +14,13 @@ type DataEmitter func(ctx context.Context) chan interface{}
const ContextURLKey = "url"
func SseWrapper(emitter DataEmitter) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request,) {
return func(w http.ResponseWriter, r *http.Request) {
// Отправляем header event-stream
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
flusher, ok := w.(http.Flusher)
if !ok {
http.Error(
@ -29,8 +34,8 @@ func SseWrapper(emitter DataEmitter) http.HandlerFunc {
ctx := context.WithValue(
r.Context(),
ContextURLKey,
strings.Split(r.URL.Path, "/"),
)
r.URL.Query().Get("ticket"),
)
dE := emitter(ctx)
@ -110,4 +115,4 @@ func HandlerWrapper(f interface{}) http.HandlerFunc {
panic(err)
}
}
}
}