开发...

This commit is contained in:
kenneth
2023-11-29 09:46:09 +00:00
parent 6b8eafe5f5
commit d07d13151a
31 changed files with 920 additions and 665 deletions

37
internal/handlers/base.go Normal file
View File

@@ -0,0 +1,37 @@
package handlers
import (
"net/http"
"github.com/rs/xid"
"github.com/zhang2092/mediahls/internal/pkg/cookie"
)
const (
AuthorizeCookie = "authorize"
ContextUser CtxTypeUser = "context_user"
)
type CtxTypeUser string
type authorize struct {
AuthID string `json:"auth_id"`
AuthName string `json:"auth_name"`
}
func genId() string {
id := xid.New()
return id.String()
}
func (server *Server) isRedirect(w http.ResponseWriter, r *http.Request) {
_, err := server.withCookie(r)
if err != nil {
// 1. 删除cookie
cookie.DeleteCookie(w, cookie.AuthorizeName)
return
}
// cookie 校验成功
http.Redirect(w, r, "/", http.StatusFound)
}

View File

@@ -1,20 +1,24 @@
package handlers
import (
"html/template"
"net/http"
)
func (server *Server) home(w http.ResponseWriter, r *http.Request) {
t, err := template.ParseFiles("web/templates/home.html.tmpl")
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
err = t.Execute(w, nil)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
type pageData struct {
AuthID string
AuthName string
}
func (server *Server) home(w http.ResponseWriter, r *http.Request) {
pd := pageData{}
auth, err := server.withCookie(r)
if err == nil {
pd.AuthID = auth.AuthID
pd.AuthName = auth.AuthName
}
renderHome(w, pd)
}
func renderHome(w http.ResponseWriter, data any) {
render(w, data, "web/templates/home.html.tmpl", "web/templates/base/header.html.tmpl", "web/templates/base/footer.html.tmpl")
}

View File

@@ -0,0 +1,65 @@
package handlers
import (
"context"
"encoding/json"
"log"
"net/http"
"github.com/zhang2092/mediahls/internal/pkg/convert"
)
func (server *Server) authorizeMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
u, err := server.withCookie(r)
if err != nil {
http.Redirect(w, r, "/login", http.StatusFound)
return
}
b, err := json.Marshal(u)
if err != nil {
log.Printf("json marshal authorize user: %v", err)
http.Redirect(w, r, "/login", http.StatusFound)
return
}
ctx := r.Context()
ctx = context.WithValue(ctx, ContextUser, b)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func (server *Server) withCookie(r *http.Request) (*authorize, error) {
cookie, err := r.Cookie(AuthorizeCookie)
if err != nil {
log.Printf("get cookie: %v", err)
return nil, err
}
u := &authorize{}
err = server.secureCookie.Decode(AuthorizeCookie, cookie.Value, u)
if err != nil {
log.Printf("secure decode cookie: %v", err)
return nil, err
}
return u, nil
}
func withUser(ctx context.Context) *authorize {
var result authorize
ctxValue, err := convert.ToByteE(ctx.Value(ContextUser))
log.Printf("ctx: %s", ctxValue)
if err != nil {
log.Printf("1: %v", err)
return nil
}
err = json.Unmarshal(ctxValue, &result)
if err != nil {
log.Printf("2: %v", err)
return nil
}
return &result
}

View File

@@ -0,0 +1,23 @@
package handlers
import (
"html/template"
"log"
"net/http"
)
func render(w http.ResponseWriter, data any, tmpls ...string) {
t, err := template.ParseFiles(tmpls...)
if err != nil {
log.Printf("template parse: %v", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
err = t.Execute(w, data)
if err != nil {
log.Printf("template execute: %v", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
}

View File

@@ -13,6 +13,7 @@ import (
"time"
"github.com/gorilla/mux"
"github.com/gorilla/securecookie"
"github.com/zhang2092/mediahls/internal/db"
"github.com/zhang2092/mediahls/internal/pkg/config"
"github.com/zhang2092/mediahls/internal/pkg/logger"
@@ -20,8 +21,9 @@ import (
)
type Server struct {
conf *config.Config
router *mux.Router
conf *config.Config
router *mux.Router
secureCookie *securecookie.SecureCookie
store db.Store
tokenMaker token.Maker
@@ -33,10 +35,16 @@ func NewServer(conf *config.Config, store db.Store) (*Server, error) {
return nil, fmt.Errorf("cannot create token maker: %w", err)
}
hashKey := securecookie.GenerateRandomKey(32)
blockKey := securecookie.GenerateRandomKey(32)
secureCookie := securecookie.New(hashKey, blockKey)
secureCookie.MaxAge(7200)
server := &Server{
conf: conf,
store: store,
tokenMaker: tokenMaker,
conf: conf,
secureCookie: secureCookie,
store: store,
tokenMaker: tokenMaker,
}
server.setupRouter()
@@ -45,18 +53,23 @@ func NewServer(conf *config.Config, store db.Store) (*Server, error) {
func (server *Server) setupRouter() {
router := mux.NewRouter()
router.Use(mux.CORSMethodMiddleware(router))
router.PathPrefix("/statics/").Handler(http.StripPrefix("/statics/", http.FileServer(http.Dir("web/statics"))))
router.HandleFunc("/register", server.registerView).Methods(http.MethodGet)
router.HandleFunc("/register", server.register).Methods(http.MethodPost)
router.HandleFunc("/login", server.loginView).Methods(http.MethodGet)
router.HandleFunc("/login", server.login).Methods(http.MethodPost)
router.HandleFunc("/logout", server.logout).Methods(http.MethodGet)
router.HandleFunc("/", server.home).Methods(http.MethodGet)
router.HandleFunc("/play/{xid}", server.play).Methods(http.MethodGet)
router.HandleFunc("/media/{xid}/stream/", server.stream).Methods(http.MethodGet)
router.HandleFunc("/media/{xid}/stream/{segName:index[0-9]+.ts}", server.stream).Methods(http.MethodGet)
router.HandleFunc("/upload", server.upload).Methods(http.MethodGet, http.MethodPost)
subRouter := router.PathPrefix("/").Subrouter()
subRouter.Use(server.authorizeMiddleware)
subRouter.HandleFunc("/upload", server.uploadView).Methods(http.MethodGet)
subRouter.HandleFunc("/upload", server.upload).Methods(http.MethodPost)
server.router = router
}

View File

@@ -1,7 +1,6 @@
package handlers
import (
"html/template"
"io"
"log"
"net/http"
@@ -14,89 +13,83 @@ import (
"github.com/zhang2092/mediahls/internal/pkg/fileutil"
)
func (server *Server) uploadView(w http.ResponseWriter, r *http.Request) {
user := withUser(r.Context())
log.Printf("%v", user)
renderUpload(w, nil)
}
func renderUpload(w http.ResponseWriter, data any) {
render(w, data, "web/templates/me/upload.html.tmpl", "web/templates/base/header.html.tmpl", "web/templates/base/footer.html.tmpl")
}
func (server *Server) upload(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodPost {
defer r.Body.Close()
defer r.Body.Close()
file, fileHeader, err := r.FormFile("file")
if err != nil {
w.WriteHeader(http.StatusBadRequest)
_, err = w.Write([]byte(err.Error()))
if err != nil {
log.Printf("%v", err)
}
return
}
defer file.Close()
buff := make([]byte, 512)
_, err = file.Read(buff)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// filetype := http.DetectContentType(buff)
// if filetype != "image/jpeg" && filetype != "image/png" {
// http.Error(w, "The provided file format is not allowed. Please upload a JPEG or PNG image", http.StatusBadRequest)
// return
// }
_, err = file.Seek(0, io.SeekStart)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
dir := path.Join("upload", time.Now().Format("20060102"))
exist, _ := fileutil.PathExists(dir)
if !exist {
err := os.MkdirAll(dir, os.ModePerm)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
filename, err := nanoId.Nanoid()
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
filePath := path.Join("", dir, filename+filepath.Ext(fileHeader.Filename))
f, err := os.Create(filePath)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
defer f.Close()
_, err = io.Copy(f, file)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
w.WriteHeader(http.StatusOK)
_, err = w.Write([]byte(filePath))
file, fileHeader, err := r.FormFile("file")
if err != nil {
w.WriteHeader(http.StatusBadRequest)
_, err = w.Write([]byte(err.Error()))
if err != nil {
log.Printf("%v", err)
}
return
}
defer file.Close()
buff := make([]byte, 512)
_, err = file.Read(buff)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
t, err := template.ParseFiles("web/templates/upload.html.tmpl")
// filetype := http.DetectContentType(buff)
// if filetype != "image/jpeg" && filetype != "image/png" {
// http.Error(w, "The provided file format is not allowed. Please upload a JPEG or PNG image", http.StatusBadRequest)
// return
// }
_, err = file.Seek(0, io.SeekStart)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
err = t.Execute(w, nil)
dir := path.Join("upload", time.Now().Format("20060102"))
exist, _ := fileutil.PathExists(dir)
if !exist {
err := os.MkdirAll(dir, os.ModePerm)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
filename, err := nanoId.Nanoid()
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
filePath := path.Join("", dir, filename+filepath.Ext(fileHeader.Filename))
f, err := os.Create(filePath)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
defer f.Close()
_, err = io.Copy(f, file)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
w.WriteHeader(http.StatusOK)
_, err = w.Write([]byte(filePath))
if err != nil {
log.Printf("%v", err)
}
}

View File

@@ -2,45 +2,72 @@ package handlers
import (
"database/sql"
"html/template"
"log"
"net/http"
"time"
"github.com/zhang2092/mediahls/internal/db"
"github.com/zhang2092/mediahls/internal/pkg/cookie"
pwd "github.com/zhang2092/mediahls/internal/pkg/password"
)
func (server *Server) registerView(w http.ResponseWriter, r *http.Request) {
t, err := template.ParseFiles("web/templates/user/register.html.tmpl", "web/templates/base/header.html.tmpl", "web/templates/base/footer.html.tmpl")
if err != nil {
log.Printf("%v", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
err = t.Execute(w, nil)
if err != nil {
log.Printf("%v", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
// 是否已经登录
server.isRedirect(w, r)
renderRegister(w, nil)
}
type userResponse struct {
Username string `json:"username"`
FullName string `json:"full_name"`
Email string `json:"email"`
PasswordChangedAt time.Time `json:"password_changed_at"`
CreatedAt time.Time `json:"created_at"`
func renderRegister(w http.ResponseWriter, data any) {
render(w, data, "web/templates/user/register.html.tmpl", "web/templates/base/header.html.tmpl", "web/templates/base/footer.html.tmpl")
}
func newUserResponse(user db.User) userResponse {
return userResponse{
Username: user.Username,
Email: user.Email,
CreatedAt: user.CreatedAt,
// type userResponse struct {
// Username string `json:"username"`
// FullName string `json:"full_name"`
// Email string `json:"email"`
// PasswordChangedAt time.Time `json:"password_changed_at"`
// CreatedAt time.Time `json:"created_at"`
// }
// func newUserResponse(user db.User) userResponse {
// return userResponse{
// Username: user.Username,
// Email: user.Email,
// CreatedAt: user.CreatedAt,
// }
// }
func viladatorRegister(email, username, password string) (*respErrs, bool) {
ok := true
errs := &respErrs{
Email: email,
Username: username,
Password: password,
}
if !ValidateRxEmail(email) {
errs.EmailErr = "请填写正确的邮箱地址"
ok = false
}
if !ValidateRxUsername(username) {
errs.UsernameErr = "名称(6-20,字母,数字)"
ok = false
}
if !ValidatePassword(password) {
errs.PasswordErr = "密码(8-20位)"
ok = false
}
return errs, ok
}
type respErrs struct {
Summary string
Email string
Username string
Password string
EmailErr string
UsernameErr string
PasswordErr string
}
func (server *Server) register(w http.ResponseWriter, r *http.Request) {
@@ -51,9 +78,14 @@ func (server *Server) register(w http.ResponseWriter, r *http.Request) {
return
}
username := r.PostFormValue("username")
email := r.PostFormValue("email")
username := r.PostFormValue("username")
password := r.PostFormValue("password")
errs, ok := viladatorRegister(email, username, password)
if !ok {
renderRegister(w, errs)
return
}
hashedPassword, err := pwd.BcryptHashPassword(password)
if err != nil {
@@ -62,88 +94,116 @@ func (server *Server) register(w http.ResponseWriter, r *http.Request) {
}
arg := db.CreateUserParams{
ID: genId(),
Username: username,
HashedPassword: hashedPassword,
Email: email,
}
user, err := server.store.CreateUser(r.Context(), arg)
_, err = server.store.CreateUser(r.Context(), arg)
if err != nil {
if server.store.IsUniqueViolation(err) {
http.Error(w, "数据已经存在", http.StatusInternalServerError)
errs.Summary = "邮箱或名称已经存在"
renderRegister(w, errs)
return
}
http.Error(w, err.Error(), http.StatusInternalServerError)
errs.Summary = "请求网络错误,请刷新重试"
renderRegister(w, errs)
return
}
rsp := newUserResponse(user)
Respond(w, "ok", rsp, http.StatusOK)
http.Redirect(w, r, "/login", http.StatusFound)
// rsp := newUserResponse(user)
// Respond(w, "ok", rsp, http.StatusOK)
}
func (server *Server) loginView(w http.ResponseWriter, r *http.Request) {
t, err := template.ParseFiles("web/templates/user/login.html.tmpl", "web/templates/base/header.html.tmpl", "web/templates/base/footer.html.tmpl")
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
err = t.Execute(w, nil)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
// 是否已经登录
server.isRedirect(w, r)
renderLogin(w, nil)
}
type loginUserResponse struct {
AccessToken string `json:"access_token"`
AccessTokenExpiresAt time.Time `json:"access_token_expires_at"`
User userResponse `json:"user"`
func renderLogin(w http.ResponseWriter, data any) {
render(w, data, "web/templates/user/login.html.tmpl", "web/templates/base/header.html.tmpl", "web/templates/base/footer.html.tmpl")
}
// type loginUserResponse struct {
// AccessToken string `json:"access_token"`
// AccessTokenExpiresAt time.Time `json:"access_token_expires_at"`
// User userResponse `json:"user"`
// }
func viladatorLogin(email, password string) (*respErrs, bool) {
ok := true
errs := &respErrs{
Email: email,
Password: password,
}
if !ValidateRxEmail(email) {
errs.EmailErr = "请填写正确的邮箱地址"
ok = false
}
if len(password) == 0 {
errs.PasswordErr = "请填写正确的密码"
ok = false
}
return errs, ok
}
func (server *Server) login(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
if err := r.ParseForm(); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
renderLogin(w, respErrs{Summary: "请求网络错误,请刷新重试"})
return
}
username := r.PostFormValue("username")
email := r.PostFormValue("email")
password := r.PostFormValue("password")
ctx := r.Context()
errs, ok := viladatorLogin(email, password)
if !ok {
renderLogin(w, errs)
return
}
user, err := server.store.GetUserByName(ctx, username)
ctx := r.Context()
user, err := server.store.GetUserByEmail(ctx, email)
if err != nil {
if server.store.IsNoRows(sql.ErrNoRows) {
http.Error(w, "用户不存在", http.StatusInternalServerError)
errs.Summary = "邮箱或密码错误"
renderLogin(w, errs)
return
}
http.Error(w, err.Error(), http.StatusInternalServerError)
errs.Summary = "请求网络错误,请刷新重试"
renderLogin(w, errs)
return
}
err = pwd.BcryptComparePassword(password, user.HashedPassword)
err = pwd.BcryptComparePassword(user.HashedPassword, password)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
errs.Summary = "邮箱或密码错误"
renderLogin(w, errs)
return
}
accessToken, accessPayload, err := server.tokenMaker.CreateToken(
user.ID,
user.Username,
server.conf.AccessTokenDuration,
)
encoded, err := server.secureCookie.Encode(AuthorizeCookie, &authorize{AuthID: user.ID, AuthName: user.Username})
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
errs.Summary = "请求网络错误,请刷新重试(cookie)"
renderLogin(w, errs)
return
}
rsp := loginUserResponse{
AccessToken: accessToken,
AccessTokenExpiresAt: accessPayload.ExpiresAt.Time,
User: newUserResponse(user),
}
Respond(w, "ok", rsp, http.StatusOK)
c := cookie.NewCookie(cookie.AuthorizeName, encoded, time.Now().Add(time.Duration(7200)*time.Second))
http.SetCookie(w, c)
http.Redirect(w, r, "/", http.StatusFound)
}
func (server *Server) logout(w http.ResponseWriter, r *http.Request) {
cookie.DeleteCookie(w, cookie.AuthorizeName)
http.Redirect(w, r, "/login", http.StatusFound)
}

View File

@@ -0,0 +1,38 @@
package handlers
import (
"regexp"
"strings"
)
var (
rxPhone = regexp.MustCompile(`^(13|14|15|16|17|18|19)\d{9}$`)
rxEmail = regexp.MustCompile(`\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*`)
rxUsername = regexp.MustCompile(`^[a-z0-9A-Z]{6,20}$`) // 6到20位字母数字
//rxPassword = regexp.MustCompile(`^(?=.*[a-zA-Z])(?=.*[0-9])[A-Za-z0-9]{8,18}$`) // 最少6位包括至少1个大写字母1个小写字母1个数字1个特殊字符
)
func ValidateRxPhone(phone string) bool {
phone = strings.TrimSpace(phone)
return rxPhone.MatchString(phone)
}
func ValidateRxEmail(email string) bool {
email = strings.TrimSpace(email)
return rxEmail.MatchString(email)
}
func ValidateRxUsername(username string) bool {
username = strings.TrimSpace(username)
return rxUsername.MatchString(username)
}
// func ValidateRxPassword(password string) bool {
// password = strings.TrimSpace(password)
// return rxPassword.MatchString(password)
// }
func ValidatePassword(password string) bool {
password = strings.TrimSpace(password)
return len(password) >= 8 && len(password) <= 20
}

View File

@@ -1,9 +1,30 @@
package handlers
import "net/http"
import (
"fmt"
"net/http"
"github.com/gorilla/mux"
)
type playData struct {
AuthID string
AuthName string
Url string
}
func (server *Server) play(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
xid := vars["xid"]
data := playData{
Url: "/media/" + xid + "/stream/",
}
auth, err := server.withCookie(r)
if err == nil {
data.AuthID = auth.AuthID
data.AuthName = auth.AuthName
}
render(w, data, "web/templates/video/play.html.tmpl", "web/templates/base/header.html.tmpl", "web/templates/base/footer.html.tmpl")
}
/*
@@ -18,3 +39,45 @@ defer video.Close()
http.ServeContent(w, r, "git", time.Now(), video)
*/
func (server *Server) stream(response http.ResponseWriter, request *http.Request) {
vars := mux.Vars(request)
mId := vars["xid"]
fmt.Println(mId)
segName, ok := vars["segName"]
if !ok {
mediaBase := getMediaBase(mId)
m3u8Name := "index.m3u8"
serveHlsM3u8(response, request, mediaBase, m3u8Name)
} else {
mediaBase := getMediaBase(mId)
serveHlsTs(response, request, mediaBase, segName)
}
}
func getMediaBase(mId string) string {
mediaRoot := "media"
return fmt.Sprintf("%s/%s", mediaRoot, mId)
}
func serveHlsM3u8(w http.ResponseWriter, r *http.Request, mediaBase, m3u8Name string) {
fmt.Println("serveHlsM3u8...")
mediaFile := fmt.Sprintf("%s/%s", mediaBase, m3u8Name)
fmt.Println(mediaFile)
http.ServeFile(w, r, mediaFile)
w.Header().Set("Content-Type", "application/x-mpegURL")
}
func serveHlsTs(w http.ResponseWriter, r *http.Request, mediaBase, segName string) {
fmt.Println("serveHlsTs...")
mediaFile := fmt.Sprintf("%s/%s", mediaBase, segName)
fmt.Println(mediaFile)
http.ServeFile(w, r, mediaFile)
w.Header().Set("Content-Type", "video/MP2T")
}