Initial commit: docker compose config
Release Docker multi arch / docker (push) Has been cancelled
Test Install Script / Test Script Syntax (push) Has been cancelled
Test Install Script / Test on almalinux-10 (default) (push) Has been cancelled
Test Install Script / Test on almalinux-10 (root) (push) Has been cancelled
Test Install Script / Test on almalinux-8 (default) (push) Has been cancelled
Test Install Script / Test on almalinux-8 (root) (push) Has been cancelled
Test Install Script / Test on almalinux-9 (default) (push) Has been cancelled
Test Install Script / Test on almalinux-9 (root) (push) Has been cancelled
Test Install Script / Test on amazonlinux-2 (default) (push) Has been cancelled
Test Install Script / Test on amazonlinux-2 (root) (push) Has been cancelled
Test Install Script / Test on debian-11 (default) (push) Has been cancelled
Test Install Script / Test on debian-11 (root) (push) Has been cancelled
Test Install Script / Test on debian-12 (default) (push) Has been cancelled
Test Install Script / Test on debian-12 (root) (push) Has been cancelled
Test Install Script / Test on debian-13 (default) (push) Has been cancelled
Test Install Script / Test on debian-13 (root) (push) Has been cancelled
Test Install Script / Test on fedora-latest (default) (push) Has been cancelled
Test Install Script / Test on fedora-latest (root) (push) Has been cancelled
Test Install Script / Test on rocky-10 (default) (push) Has been cancelled
Test Install Script / Test on rocky-10 (root) (push) Has been cancelled
Test Install Script / Test on rocky-8 (default) (push) Has been cancelled
Test Install Script / Test on rocky-8 (root) (push) Has been cancelled
Test Install Script / Test on rocky-9 (default) (push) Has been cancelled
Test Install Script / Test on rocky-9 (root) (push) Has been cancelled
Test Install Script / Test on ubuntu-22.04 (default) (push) Has been cancelled
Test Install Script / Test on ubuntu-22.04 (root) (push) Has been cancelled
Test Install Script / Test on ubuntu-24.04 (default) (push) Has been cancelled
Test Install Script / Test on ubuntu-24.04 (root) (push) Has been cancelled

This commit is contained in:
2026-05-30 12:07:11 +00:00
commit 616c6b1c62
381 changed files with 55145 additions and 0 deletions
+63
View File
@@ -0,0 +1,63 @@
package api
import (
"net/http"
"server/torr"
"github.com/gin-gonic/gin"
"github.com/pkg/errors"
)
// Action: get
type cacheReqJS struct {
requestI
Hash string `json:"hash,omitempty"`
}
// cache godoc
//
// @Summary Return cache stats
// @Description Return cache stats.
//
// @Tags API
//
// @Param request body cacheReqJS true "Cache stats request"
//
// @Produce json
// @Success 200 {object} state.CacheState "Cache stats"
// @Router /cache [post]
func cache(c *gin.Context) {
var req cacheReqJS
err := c.ShouldBindJSON(&req)
if err != nil {
c.AbortWithError(http.StatusBadRequest, err)
return
}
c.Status(http.StatusBadRequest)
switch req.Action {
case "get":
{
getCache(req, c)
}
}
}
func getCache(req cacheReqJS, c *gin.Context) {
if req.Hash == "" {
c.AbortWithError(http.StatusBadRequest, errors.New("hash is empty"))
return
}
tor := torr.GetTorrent(req.Hash)
if tor != nil {
st := tor.CacheState()
if st == nil {
c.JSON(200, struct{}{})
} else {
c.JSON(200, st)
}
} else {
c.Status(http.StatusNotFound)
}
}
+64
View File
@@ -0,0 +1,64 @@
package api
import (
"fmt"
"io"
"net/http"
"strconv"
"time"
"github.com/gin-gonic/gin"
)
type fileReader struct {
pos int64
size int64
io.ReadSeeker
}
func newFR(size int64) *fileReader {
return &fileReader{
pos: 0,
size: size,
}
}
func (f *fileReader) Read(p []byte) (n int, err error) {
f.pos = f.pos + int64(len(p))
return len(p), nil
}
func (f *fileReader) Seek(offset int64, whence int) (int64, error) {
switch whence {
case 0:
f.pos = offset
case 1:
f.pos += offset
case 2:
f.pos = f.size + offset
}
return f.pos, nil
}
// download godoc
//
// @Summary Generates test file of given size
// @Description Download the test file of given size (for speed testing purpose).
//
// @Tags API
//
// @Param size path string true "Test file size (in MB)"
//
// @Produce application/octet-stream
// @Success 200 {file} file
// @Router /download/{size} [get]
func download(c *gin.Context) {
szStr := c.Param("size")
sz, err := strconv.Atoi(szStr)
if err != nil {
c.Error(err)
return
}
http.ServeContent(c.Writer, c.Request, fmt.Sprintln(szStr)+"mb.bin", time.Now(), newFR(int64(sz*1024*1024)))
}
+48
View File
@@ -0,0 +1,48 @@
package api
import (
"errors"
"fmt"
"net/http"
"server/ffprobe"
sets "server/settings"
"github.com/gin-gonic/gin"
)
// ffp godoc
//
// @Summary Gather informations using ffprobe
// @Description Gather informations using ffprobe.
//
// @Tags API
//
// @Param hash path string true "Torrent hash"
// @Param id path string true "File index in torrent"
//
// @Produce json
// @Success 200 "Data returned from ffprobe"
// @Router /ffp/{hash}/{id} [get]
func ffp(c *gin.Context) {
hash := c.Param("hash")
indexStr := c.Param("id")
if hash == "" || indexStr == "" {
c.AbortWithError(http.StatusNotFound, errors.New("link should not be empty"))
return
}
link := "http://127.0.0.1:" + sets.Port + "/play/" + hash + "/" + indexStr
if sets.Ssl {
link = "https://127.0.0.1:" + sets.SslPort + "/play/" + hash + "/" + indexStr
}
data, err := ffprobe.ProbeUrl(link)
if err != nil {
c.AbortWithError(http.StatusBadRequest, fmt.Errorf("error getting data: %v", err))
return
}
c.JSON(200, data)
}
+186
View File
@@ -0,0 +1,186 @@
package api
import (
"bytes"
"encoding/hex"
"fmt"
"net/http"
"net/url"
"path/filepath"
"sort"
"strings"
"time"
"github.com/anacrolix/missinggo/v2/httptoo"
sets "server/settings"
"server/torr"
"server/torr/state"
"server/utils"
"github.com/gin-gonic/gin"
"github.com/pkg/errors"
)
// allPlayList godoc
//
// @Summary Get a M3U playlist with all torrents
// @Description Retrieve all torrents and generates a bundled M3U playlist.
//
// @Tags API
//
// @Produce audio/x-mpegurl
// @Success 200 {file} file
// @Router /playlistall/all.m3u [get]
func allPlayList(c *gin.Context) {
torrs := torr.ListTorrent()
host := utils.GetScheme(c) + "://" + utils.GetHost(c)
list := "#EXTM3U\n"
hash := ""
// fn=file.m3u fix forkplayer bug with end .m3u in link
for _, tr := range torrs {
list += "#EXTINF:0"
if tr.Poster != "" {
list += " tvg-logo=\"" + tr.Poster + "\""
}
list += " type=\"playlist\"," + tr.Title + "\n"
list += host + "/stream/" + url.PathEscape(tr.Title) + ".m3u?link=" + tr.TorrentSpec.InfoHash.HexString() + "&m3u&fn=file.m3u\n"
hash += tr.Hash().HexString()
}
sendM3U(c, "all.m3u", hash, list)
}
// playList godoc
//
// @Summary Get HTTP link of torrent in M3U list
// @Description Get HTTP link of torrent in M3U list.
//
// @Tags API
//
// @Param hash query string true "Torrent hash"
// @Param fromlast query bool false "From last play file"
//
// @Produce audio/x-mpegurl
// @Success 200 {file} file
// @Router /playlist [get]
func playList(c *gin.Context) {
hash, _ := c.GetQuery("hash")
_, fromlast := c.GetQuery("fromlast")
if hash == "" {
c.AbortWithError(http.StatusBadRequest, errors.New("hash is empty"))
return
}
tor := torr.GetTorrent(hash)
if tor == nil {
c.AbortWithStatus(http.StatusNotFound)
return
}
if tor.Stat == state.TorrentInDB {
tor = torr.LoadTorrent(tor)
if tor == nil {
c.AbortWithError(http.StatusInternalServerError, errors.New("error get torrent info"))
return
}
}
host := utils.GetScheme(c) + "://" + utils.GetHost(c)
list := getM3uList(tor.Status(), host, fromlast)
list = "#EXTM3U\n" + list
name := strings.ReplaceAll(c.Param("fname"), `/`, "") // strip starting / from param
if name == "" {
name = tor.Name() + ".m3u"
} else if !strings.HasSuffix(strings.ToLower(name), ".m3u") && !strings.HasSuffix(strings.ToLower(name), ".m3u8") {
name += ".m3u"
}
sendM3U(c, name, tor.Hash().HexString(), list)
}
func sendM3U(c *gin.Context, name, hash string, m3u string) {
c.Header("Content-Type", "audio/x-mpegurl")
c.Header("Connection", "close")
if hash != "" {
etag := hex.EncodeToString([]byte(fmt.Sprintf("%s/%s", hash, name)))
c.Header("ETag", httptoo.EncodeQuotedString(etag))
}
if name == "" {
name = "playlist.m3u"
}
c.Header("Content-Disposition", `attachment; filename="`+name+`"`)
http.ServeContent(c.Writer, c.Request, name, time.Now(), bytes.NewReader([]byte(m3u)))
}
func getM3uList(tor *state.TorrentStatus, host string, fromLast bool) string {
m3u := ""
from := 0
if fromLast {
pos := searchLastPlayed(tor)
if pos != -1 {
from = pos
}
}
for i, f := range tor.FileStats {
if i >= from {
if utils.GetMimeType(f.Path) != "*/*" {
fn := filepath.Base(f.Path)
if fn == "" {
fn = f.Path
}
m3u += "#EXTINF:0," + fn + "\n"
fileNamesakes := findFileNamesakes(tor.FileStats, f) // find external media with same name (audio/subtiles tracks)
if fileNamesakes != nil {
m3u += "#EXTVLCOPT:input-slave=" // include VLC option for external media
for _, namesake := range fileNamesakes { // include play-links to external media, with # splitter
sname := filepath.Base(namesake.Path)
m3u += host + "/stream/" + url.PathEscape(sname) + "?link=" + tor.Hash + "&index=" + fmt.Sprint(namesake.Id) + "&play#"
}
m3u += "\n"
}
name := filepath.Base(f.Path)
m3u += host + "/stream/" + url.PathEscape(name) + "?link=" + tor.Hash + "&index=" + fmt.Sprint(f.Id) + "&play\n"
}
}
}
return m3u
}
func findFileNamesakes(files []*state.TorrentFileStat, file *state.TorrentFileStat) []*state.TorrentFileStat {
// find files with the same name in torrent
name := filepath.Base(strings.TrimSuffix(file.Path, filepath.Ext(file.Path)))
var namesakes []*state.TorrentFileStat
for _, f := range files {
if strings.Contains(f.Path, name) { // external tracks always include name of videofile
if f != file { // exclude itself
namesakes = append(namesakes, f)
}
}
}
return namesakes
}
func searchLastPlayed(tor *state.TorrentStatus) int {
viewed := sets.ListViewed(tor.Hash)
if len(viewed) == 0 {
return -1
}
sort.Slice(viewed, func(i, j int) bool {
return viewed[i].FileIndex > viewed[j].FileIndex
})
lastViewedIndex := viewed[0].FileIndex
for i, stat := range tor.FileStats {
if stat.Id == lastViewedIndex {
if i >= len(tor.FileStats) {
return -1
}
return i
}
}
return -1
}
+85
View File
@@ -0,0 +1,85 @@
package api
import (
"errors"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"server/torr"
"server/torr/state"
"server/web/api/utils"
)
// play godoc
//
// @Summary Play given torrent by infohash
// @Description Play given torrent referenced by infohash and file id.
//
// @Tags API
//
// @Param hash path string true "Torrent infohash"
// @Param id path string true "File index in torrent"
//
// @Produce application/octet-stream
// @Success 200 "Torrent data"
// @Router /play/{hash}/{id} [get]
func play(c *gin.Context) {
hash := c.Param("hash")
indexStr := c.Param("id")
notAuth := c.GetBool("auth_required") && c.GetString(gin.AuthUserKey) == ""
if hash == "" || indexStr == "" {
c.AbortWithError(http.StatusNotFound, errors.New("no infohash or file index in link"))
return
}
spec, err := utils.ParseLink(hash)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
tor := torr.GetTorrent(spec.InfoHash.HexString())
if tor == nil && notAuth {
c.Header("WWW-Authenticate", "Basic realm=Authorization Required")
c.AbortWithStatus(http.StatusUnauthorized)
return
}
if tor == nil {
c.AbortWithError(http.StatusInternalServerError, errors.New("error get torrent"))
return
}
if tor.Stat == state.TorrentInDB {
tor, err = torr.AddTorrent(spec, tor.Title, tor.Poster, tor.Data, tor.Category)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
}
if !tor.GotInfo() {
c.AbortWithError(http.StatusInternalServerError, errors.New("torrent connection timeout"))
return
}
// find file
index := -1
if len(tor.Files()) == 1 {
index = 1
} else {
ind, err := strconv.Atoi(indexStr)
if err == nil {
index = ind
}
}
if index == -1 { // if file index not set and play file exec
c.AbortWithError(http.StatusBadRequest, errors.New("file \"index\" is wrong"))
return
}
tor.Stream(index, c.Request, c.Writer)
}
+67
View File
@@ -0,0 +1,67 @@
package api
import (
config "server/settings"
"server/web/auth"
"github.com/gin-gonic/gin"
)
type requestI struct {
Action string `json:"action,omitempty"`
}
func SetupRoute(route gin.IRouter) {
authorized := route.Group("/", auth.CheckAuth())
authorized.GET("/shutdown", shutdown)
authorized.GET("/shutdown/*reason", shutdown)
authorized.POST("/settings", settings)
authorized.POST("/torznab/test", torznabTest)
authorized.POST("/torrents", torrents)
authorized.POST("/torrent/upload", torrentUpload)
authorized.POST("/cache", cache)
route.HEAD("/stream", stream)
route.GET("/stream", stream)
route.HEAD("/stream/*fname", stream)
route.GET("/stream/*fname", stream)
route.HEAD("/play/:hash/:id", play)
route.GET("/play/:hash/:id", play)
authorized.POST("/viewed", viewed)
authorized.GET("/playlistall/all.m3u", allPlayList)
route.GET("/playlist", playList)
route.GET("/playlist/*fname", playList)
authorized.GET("/download/:size", download)
if config.SearchWA {
route.GET("/search/*query", rutorSearch)
} else {
authorized.GET("/search/*query", rutorSearch)
}
if config.SearchWA {
route.GET("/torznab/search/*query", torznabSearch)
} else {
authorized.GET("/torznab/search/*query", torznabSearch)
}
// Add storage settings endpoints
authorized.GET("/storage/settings", GetStorageSettings)
authorized.POST("/storage/settings", UpdateStorageSettings)
// Add TMDB settings endpoint
authorized.GET("/tmdb/settings", tmdbSettings)
authorized.GET("/ffp/:hash/:id", ffp)
}
+38
View File
@@ -0,0 +1,38 @@
package api
import (
"net/http"
"net/url"
"github.com/gin-gonic/gin"
"server/rutor"
"server/rutor/models"
sets "server/settings"
)
// rutorSearch godoc
//
// @Summary Makes a rutor search
// @Description Makes a rutor search.
//
// @Tags API
//
// @Param query query string true "Rutor query"
//
// @Produce json
// @Success 200 {array} models.TorrentDetails "Rutor torrent search result(s)"
// @Router /search [get]
func rutorSearch(c *gin.Context) {
if !sets.BTsets.EnableRutorSearch {
c.JSON(http.StatusBadRequest, []string{})
return
}
query := c.Query("query")
query, _ = url.QueryUnescape(query)
list := rutor.Search(query)
if list == nil {
list = []*models.TorrentDetails{}
}
c.JSON(200, list)
}
+65
View File
@@ -0,0 +1,65 @@
package api
import (
"net/http"
"server/rutor"
"server/dlna"
"github.com/gin-gonic/gin"
"github.com/pkg/errors"
sets "server/settings"
"server/torr"
)
// Action: get, set, def
type setsReqJS struct {
requestI
Sets *sets.BTSets `json:"sets,omitempty"`
}
// settings godoc
//
// @Summary Get / Set server settings
// @Description Allow to get or set server settings.
//
// @Tags API
//
// @Param request body setsReqJS true "Settings request. Available params for action: get, set, def"
//
// @Accept json
// @Produce json
// @Success 200 {object} sets.BTSets "Settings JSON or nothing. Depends on what action has been asked."
// @Router /settings [post]
func settings(c *gin.Context) {
var req setsReqJS
err := c.ShouldBindJSON(&req)
if err != nil {
c.AbortWithError(http.StatusBadRequest, err)
return
}
if req.Action == "get" {
c.JSON(200, sets.BTsets)
return
} else if req.Action == "set" {
torr.SetSettings(req.Sets)
dlna.Stop()
if req.Sets.EnableDLNA {
dlna.Start()
}
rutor.Stop()
rutor.Start()
c.Status(200)
return
} else if req.Action == "def" {
torr.SetDefSettings()
dlna.Stop()
rutor.Stop()
c.Status(200)
return
}
c.AbortWithError(http.StatusBadRequest, errors.New("action is empty"))
}
+33
View File
@@ -0,0 +1,33 @@
package api
import (
"net/http"
"strings"
"time"
sets "server/settings"
"server/torr"
"github.com/gin-gonic/gin"
)
// shutdown godoc
// @Summary Shuts down server
// @Description Gracefully shuts down server after 1 second.
//
// @Tags API
//
// @Success 200
// @Router /shutdown [get]
func shutdown(c *gin.Context) {
reasonStr := strings.ReplaceAll(c.Param("reason"), `/`, "")
if sets.ReadOnly && reasonStr == "" {
c.Status(http.StatusForbidden)
return
}
c.Status(200)
go func() {
time.Sleep(1000)
torr.Shutdown()
}()
}
+101
View File
@@ -0,0 +1,101 @@
package api
import (
"net/http"
sets "server/settings"
"github.com/gin-gonic/gin"
)
// GetStorageSettings godoc
// @Summary Get storage configuration settings
// @Description Retrieves the current storage preferences for settings and viewed history
// @Tags API
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Success 200 {object} map[string]interface{} "Storage preferences"
// @Failure 401 {object} map[string]string "Unauthorized"
// @Failure 500 {object} map[string]string "Internal server error"
// @Router /storage/settings [get]
func GetStorageSettings(c *gin.Context) {
prefs := sets.GetStoragePreferences()
c.JSON(http.StatusOK, prefs)
}
// UpdateStorageSettings godoc
// @Summary Update storage configuration settings
// @Description Updates the storage preferences for settings and viewed history. Requires application restart for changes to take effect.
// @Tags API
// @Accept json,x-www-form-urlencoded
// @Produce json
// @Security ApiKeyAuth
// @Param request body map[string]interface{} true "Storage preferences to update"
// @Param settings formData string false "Settings storage type" Enums(json,bbolt)
// @Param viewed formData string false "Viewed history storage type" Enums(json,bbolt)
// @Success 200 {object} map[string]string "Update successful"
// @Failure 400 {object} map[string]string "Invalid input data"
// @Failure 401 {object} map[string]string "Unauthorized"
// @Failure 403 {object} map[string]string "Read-only mode"
// @Failure 500 {object} map[string]string "Internal server error"
// @Router /storage/settings [post]
func UpdateStorageSettings(c *gin.Context) {
if sets.ReadOnly {
c.JSON(http.StatusForbidden, gin.H{"error": "Read-only mode"})
return
}
var prefs map[string]interface{}
// Check Content-Type to handle both JSON and form data
contentType := c.GetHeader("Content-Type")
if contentType == "application/x-www-form-urlencoded" {
// Handle form data
settings := c.PostForm("settings")
viewed := c.PostForm("viewed")
prefs = make(map[string]interface{})
if settings != "" {
prefs["settings"] = settings
}
if viewed != "" {
prefs["viewed"] = viewed
}
} else {
// Handle JSON (default)
if err := c.ShouldBindJSON(&prefs); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
}
// Validate preferences - only validate if provided
if settingsPref, ok := prefs["settings"].(string); ok && settingsPref != "" {
if settingsPref != "json" && settingsPref != "bbolt" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid settings storage value"})
return
}
}
if viewedPref, ok := prefs["viewed"].(string); ok && viewedPref != "" {
if viewedPref != "json" && viewedPref != "bbolt" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid viewed storage value"})
return
}
}
// Check if we have at least one value to update
if len(prefs) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "No preferences provided"})
return
}
if err := sets.SetStoragePreferences(prefs); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"status": "ok"})
}
+322
View File
@@ -0,0 +1,322 @@
package api
import (
"net/http"
"net/url"
"server/log"
"server/torrshash"
"strconv"
"strings"
"server/torr"
"server/torr/state"
utils2 "server/utils"
"server/web/api/utils"
"github.com/anacrolix/torrent"
"github.com/gin-gonic/gin"
"github.com/pkg/errors"
)
// get stat
// http://127.0.0.1:8090/stream/fname?link=...&stat
// get m3u
// http://127.0.0.1:8090/stream/fname?link=...&index=1&m3u
// http://127.0.0.1:8090/stream/fname?link=...&index=1&m3u&fromlast
// stream torrent
// http://127.0.0.1:8090/stream/fname?link=...&index=1&play
// http://127.0.0.1:8090/stream/fname?link=...&index=1&play&preload
// http://127.0.0.1:8090/stream/fname?link=...&index=1&play&save
// http://127.0.0.1:8090/stream/fname?link=...&index=1&play&save&title=...&poster=...
// only save
// http://127.0.0.1:8090/stream/fname?link=...&save&title=...&poster=...
// stream godoc
//
// @Summary Multi usage endpoint
// @Description Multi usage endpoint.
//
// @Tags API
//
// @Param link query string true "Magnet/hash/link to torrent"
// @Param index query string false "File index in torrent"
// @Param preload query string false "Should preload torrent"
// @Param stat query string false "Get statistics from torrent"
// @Param save query string false "Should save torrent"
// @Param m3u query string false "Get torrent as M3U playlist"
// @Param fromlast query string false "Get M3U from last played file"
// @Param play query string false "Start stream torrent"
// @Param title query string false "Set title of torrent"
// @Param poster query string false "Set poster link of torrent"
// @Param category query string false "Set category of torrent, used in web: movie, tv, music, other"
//
// @Produce application/octet-stream
// @Success 200 "Data returned according to query"
// @Router /stream [get]
func stream(c *gin.Context) {
link := c.Query("link")
indexStr := c.Query("index")
_, preload := c.GetQuery("preload")
_, stat := c.GetQuery("stat")
_, save := c.GetQuery("save")
_, m3u := c.GetQuery("m3u")
_, fromlast := c.GetQuery("fromlast")
_, play := c.GetQuery("play")
title := c.Query("title")
poster := c.Query("poster")
category := c.Query("category")
data := ""
notAuth := c.GetBool("auth_required") && c.GetString(gin.AuthUserKey) == ""
if notAuth {
err := utils.TestLink(link, !notAuth)
if err != nil {
log.TLogln("Wrong link:", err)
c.AbortWithError(http.StatusBadRequest, errors.New("wrong link"))
return
}
}
if notAuth && (play || m3u) {
streamNoAuth(c)
return
}
if notAuth {
c.Header("WWW-Authenticate", "Basic realm=Authorization Required")
c.AbortWithStatus(http.StatusUnauthorized)
return
}
if link == "" {
c.AbortWithError(http.StatusBadRequest, errors.New("link should not be empty"))
return
}
link, _ = url.QueryUnescape(link)
title, _ = url.QueryUnescape(title)
poster, _ = url.QueryUnescape(poster)
category, _ = url.QueryUnescape(category)
var spec *torrent.TorrentSpec
var torrsHash *torrshash.TorrsHash
var err error
if strings.HasPrefix(link, "torrs://") || (len(link) > 45 && torrshash.IsBase62(link)) {
spec, torrsHash, err = utils.ParseTorrsHash(link)
if err != nil {
log.TLogln("error parse torrshash:", err)
c.AbortWithError(http.StatusBadRequest, err)
return
}
if title == "" {
title = torrsHash.Title()
}
if poster == "" {
poster = torrsHash.Poster()
}
if category == "" {
category = torrsHash.Category()
}
} else {
spec, err = utils.ParseLink(link)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
}
tor := torr.GetTorrent(spec.InfoHash.HexString())
if tor != nil {
title = tor.Title
poster = tor.Poster
data = tor.Data
category = tor.Category
}
if tor == nil || tor.Stat == state.TorrentInDB {
tor, err = torr.AddTorrent(spec, title, poster, data, category)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
}
if !tor.GotInfo() {
c.AbortWithError(http.StatusInternalServerError, errors.New("torrent connection timeout"))
return
}
if tor.Title == "" {
tor.Title = tor.Name()
}
// save to db
if save {
torr.SaveTorrentToDB(tor)
c.Status(200) // only set status, not return
}
// find file
index := -1
if len(tor.Files()) == 1 {
index = 1
} else {
ind, err := strconv.Atoi(indexStr)
if err == nil {
index = ind
}
}
if index == -1 && play { // if file index not set and play file exec
c.AbortWithError(http.StatusBadRequest, errors.New("\"index\" is empty or wrong"))
return
}
// preload torrent
if preload {
torr.Preload(tor, index)
}
// return stat if query
if stat {
c.JSON(200, tor.Status())
return
} else
// return m3u if query
if m3u {
name := strings.ReplaceAll(c.Param("fname"), `/`, "") // strip starting / from param
if name == "" {
name = tor.Name() + ".m3u"
} else if !strings.HasSuffix(strings.ToLower(name), ".m3u") && !strings.HasSuffix(strings.ToLower(name), ".m3u8") {
name += ".m3u"
}
m3ulist := "#EXTM3U\n" + getM3uList(tor.Status(), utils2.GetScheme(c)+"://"+utils2.GetHost(c), fromlast)
sendM3U(c, name, tor.Hash().HexString(), m3ulist)
return
} else
// return play if query
if play {
tor.Stream(index, c.Request, c.Writer)
return
}
}
func streamNoAuth(c *gin.Context) {
link := c.Query("link")
indexStr := c.Query("index")
_, preload := c.GetQuery("preload")
_, m3u := c.GetQuery("m3u")
_, fromlast := c.GetQuery("fromlast")
_, play := c.GetQuery("play")
title := c.Query("title")
poster := c.Query("poster")
category := c.Query("category")
if link == "" {
c.AbortWithError(http.StatusBadRequest, errors.New("link should not be empty"))
return
}
link, _ = url.QueryUnescape(link)
title, _ = url.QueryUnescape(title)
poster, _ = url.QueryUnescape(poster)
category, _ = url.QueryUnescape(category)
var spec *torrent.TorrentSpec
var torrsHash *torrshash.TorrsHash
var err error
if strings.HasPrefix(link, "torrs://") || (len(link) > 45 && torrshash.IsBase62(link)) {
spec, torrsHash, err = utils.ParseTorrsHash(link)
if err != nil {
log.TLogln("error parse torrshash:", err)
c.AbortWithError(http.StatusBadRequest, err)
return
}
if title == "" {
title = torrsHash.Title()
}
if poster == "" {
poster = torrsHash.Poster()
}
if category == "" {
category = torrsHash.Category()
}
} else {
spec, err = utils.ParseLink(link)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
}
tor := torr.GetTorrent(spec.InfoHash.HexString())
if tor == nil {
c.Header("WWW-Authenticate", "Basic realm=Authorization Required")
c.AbortWithStatus(http.StatusUnauthorized)
return
}
if title == "" {
title = tor.Title
}
if poster == "" {
poster = tor.Poster
}
if category == "" {
category = tor.Category
}
data := tor.Data
if tor.Stat == state.TorrentInDB {
tor, err = torr.AddTorrent(spec, title, poster, data, category)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
}
if !tor.GotInfo() {
c.AbortWithError(http.StatusInternalServerError, errors.New("torrent connection timeout"))
return
}
// find file
index := -1
if len(tor.Files()) == 1 {
index = 1
} else {
ind, err := strconv.Atoi(indexStr)
if err == nil {
index = ind
}
}
if index == -1 && play { // if file index not set and play file exec
c.AbortWithError(http.StatusBadRequest, errors.New("\"index\" is empty or wrong"))
return
}
// preload torrent
if preload {
torr.Preload(tor, index)
}
// return m3u if query
if m3u {
name := strings.ReplaceAll(c.Param("fname"), `/`, "") // strip starting / from param
if name == "" {
name = tor.Name() + ".m3u"
} else if !strings.HasSuffix(strings.ToLower(name), ".m3u") && !strings.HasSuffix(strings.ToLower(name), ".m3u8") {
name += ".m3u"
}
m3ulist := "#EXTM3U\n" + getM3uList(tor.Status(), utils2.GetScheme(c)+"://"+utils2.GetHost(c), fromlast)
sendM3U(c, name, tor.Hash().HexString(), m3ulist)
return
} else
// return play if query
if play {
tor.Stream(index, c.Request, c.Writer)
return
}
c.Header("WWW-Authenticate", "Basic realm=Authorization Required")
c.AbortWithStatus(http.StatusUnauthorized)
}
+27
View File
@@ -0,0 +1,27 @@
package api
import (
"net/http"
"github.com/gin-gonic/gin"
sets "server/settings"
)
// tmdbSettings godoc
//
// @Summary Get TMDB settings
// @Description Get TMDB API configuration
//
// @Tags API
//
// @Produce json
// @Success 200 {object} sets.TMDBConfig "TMDB settings"
// @Router /tmdb/settings [get]
func tmdbSettings(c *gin.Context) {
if sets.BTsets == nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Settings not initialized"})
return
}
c.JSON(200, sets.BTsets.TMDBSettings)
}
+229
View File
@@ -0,0 +1,229 @@
package api
import (
"net/http"
"server/torrshash"
"strings"
"server/dlna"
"server/log"
set "server/settings"
"server/torr"
"server/torr/state"
"server/web/api/utils"
"github.com/anacrolix/torrent"
"github.com/gin-gonic/gin"
"github.com/pkg/errors"
)
// Action: add, get, set, rem, list, drop
type torrReqJS struct {
requestI
Link string `json:"link,omitempty"`
Hash string `json:"hash,omitempty"`
Title string `json:"title,omitempty"`
Category string `json:"category,omitempty"`
Poster string `json:"poster,omitempty"`
Data string `json:"data,omitempty"`
SaveToDB bool `json:"save_to_db,omitempty"`
}
// torrents godoc
//
// @Summary Handle torrents informations
// @Description Allow to list, add, remove, get, set, drop, wipe torrents on server. The action depends of what has been asked.
//
// @Tags API
//
// @Param request body torrReqJS true "Torrent request. Available params for action: add, get, set, rem, list, drop, wipe. link required for add, hash required for get, set, rem, drop."
//
// @Accept json
// @Produce json
// @Success 200
// @Router /torrents [post]
func torrents(c *gin.Context) {
var req torrReqJS
err := c.ShouldBindJSON(&req)
if err != nil {
c.AbortWithError(http.StatusBadRequest, err)
return
}
c.Status(http.StatusBadRequest)
switch req.Action {
case "add":
{
addTorrent(req, c)
}
case "get":
{
getTorrent(req, c)
}
case "set":
{
setTorrent(req, c)
}
case "rem":
{
remTorrent(req, c)
}
case "list":
{
listTorrents(c)
}
case "drop":
{
dropTorrent(req, c)
}
case "wipe":
{
wipeTorrents(c)
}
}
}
func addTorrent(req torrReqJS, c *gin.Context) {
if req.Link == "" {
c.AbortWithError(http.StatusBadRequest, errors.New("link is empty"))
return
}
log.TLogln("add torrent", req.Link)
req.Link = strings.ReplaceAll(req.Link, "&", "&")
var torrSpec *torrent.TorrentSpec
var torrsHash *torrshash.TorrsHash
var err error
if strings.HasPrefix(req.Link, "torrs://") {
torrSpec, torrsHash, err = utils.ParseTorrsHash(req.Link)
if err != nil {
log.TLogln("error parse torrshash:", err)
c.AbortWithError(http.StatusBadRequest, err)
return
}
if req.Title == "" {
req.Title = torrsHash.Title()
}
if req.Poster == "" {
req.Poster = torrsHash.Poster()
}
if req.Category == "" {
req.Category = torrsHash.Category()
}
} else {
torrSpec, err = utils.ParseLink(req.Link)
if err != nil {
log.TLogln("error parse link:", err)
c.AbortWithError(http.StatusBadRequest, err)
return
}
}
tor, err := torr.AddTorrent(torrSpec, req.Title, req.Poster, req.Data, req.Category)
if err != nil {
log.TLogln("error add torrent:", err)
c.AbortWithError(http.StatusInternalServerError, err)
return
}
go func() {
if !tor.GotInfo() {
log.TLogln("error add torrent:", "timeout connection get torrent info")
return
}
if tor.Title == "" {
tor.Title = torrSpec.DisplayName // prefer dn over name
tor.Title = strings.ReplaceAll(tor.Title, "rutor.info", "")
tor.Title = strings.ReplaceAll(tor.Title, "_", " ")
tor.Title = strings.Trim(tor.Title, " ")
if tor.Title == "" {
tor.Title = tor.Name()
}
}
if req.SaveToDB {
torr.SaveTorrentToDB(tor)
}
}()
if set.BTsets.EnableDLNA {
dlna.Stop()
dlna.Start()
}
c.JSON(200, tor.Status())
}
func getTorrent(req torrReqJS, c *gin.Context) {
if req.Hash == "" {
c.AbortWithError(http.StatusBadRequest, errors.New("hash is empty"))
return
}
tor := torr.GetTorrent(req.Hash)
if tor != nil {
st := tor.Status()
c.JSON(200, st)
} else {
c.Status(http.StatusNotFound)
}
}
func setTorrent(req torrReqJS, c *gin.Context) {
if req.Hash == "" {
c.AbortWithError(http.StatusBadRequest, errors.New("hash is empty"))
return
}
torr.SetTorrent(req.Hash, req.Title, req.Poster, req.Category, req.Data)
c.Status(200)
}
func remTorrent(req torrReqJS, c *gin.Context) {
if req.Hash == "" {
c.AbortWithError(http.StatusBadRequest, errors.New("hash is empty"))
return
}
torr.RemTorrent(req.Hash)
// TODO: remove
if set.BTsets.EnableDLNA {
dlna.Stop()
dlna.Start()
}
c.Status(200)
}
func listTorrents(c *gin.Context) {
list := torr.ListTorrent()
if len(list) == 0 {
c.JSON(200, []*state.TorrentStatus{})
return
}
var stats []*state.TorrentStatus
for _, tr := range list {
stats = append(stats, tr.Status())
}
c.JSON(200, stats)
}
func dropTorrent(req torrReqJS, c *gin.Context) {
if req.Hash == "" {
c.AbortWithError(http.StatusBadRequest, errors.New("hash is empty"))
return
}
torr.DropTorrent(req.Hash)
c.Status(200)
}
func wipeTorrents(c *gin.Context) {
torrents := torr.ListTorrent()
for _, t := range torrents {
torr.RemTorrent(t.TorrentSpec.InfoHash.HexString())
}
// TODO: remove (copied todo from remTorrent())
if set.BTsets.EnableDLNA {
dlna.Stop()
dlna.Start()
}
c.Status(200)
}
+64
View File
@@ -0,0 +1,64 @@
package api
import (
"net/http"
"net/url"
"strconv"
"github.com/gin-gonic/gin"
"server/rutor/models"
sets "server/settings"
"server/torznab"
)
// torznabSearch godoc
//
// @Summary Makes a torznab search
// @Description Makes a torznab search.
//
// @Tags API
//
// @Param query query string true "Torznab query"
//
// @Produce json
// @Success 200 {array} models.TorrentDetails "Torznab torrent search result(s)"
// @Router /torznab/search [get]
func torznabSearch(c *gin.Context) {
if !sets.BTsets.EnableTorznabSearch {
c.JSON(http.StatusBadRequest, []string{})
return
}
query := c.Query("query")
indexStr := c.DefaultQuery("index", "-1")
index := -1
if i, err := strconv.Atoi(indexStr); err == nil {
index = i
}
query, _ = url.QueryUnescape(query)
list := torznab.Search(query, index)
if list == nil {
list = []*models.TorrentDetails{}
}
c.JSON(200, list)
}
type torznabTestReq struct {
Host string `json:"host"`
Key string `json:"key"`
}
func torznabTest(c *gin.Context) {
var req torznabTestReq
if err := c.ShouldBindJSON(&req); err != nil {
c.AbortWithError(http.StatusBadRequest, err)
return
}
if err := torznab.Test(req.Host, req.Key); err != nil {
c.JSON(200, gin.H{"success": false, "error": err.Error()})
return
}
c.JSON(200, gin.H{"success": true})
}
+114
View File
@@ -0,0 +1,114 @@
package api
import (
"mime/multipart"
"net/http"
"server/log"
set "server/settings"
"server/torr"
"server/torr/state"
"server/web/api/utils"
"github.com/gin-gonic/gin"
)
// torrentUpload godoc
//
// @Summary Add .torrent files
// @Description Supports multiple files. Returns array of statuses.
//
// @Tags API
//
// @Param file formData file true "Torrent file(s) to insert"
// @Param save formData string false "Save to DB"
// @Param title formData string false "Torrent title (single file only)"
// @Param category formData string false "Torrent category"
// @Param poster formData string false "Torrent poster (single file only)"
// @Param data formData string false "Torrent data"
//
// @Accept multipart/form-data
//
// @Produce json
// @Success 200 {array} state.TorrentStatus "Torrent statuses"
// @Router /torrent/upload [post]
func torrentUpload(c *gin.Context) {
form, err := c.MultipartForm()
if err != nil {
c.AbortWithError(http.StatusBadRequest, err)
return
}
defer form.RemoveAll()
save := len(form.Value["save"]) > 0
title := ""
if len(form.Value["title"]) > 0 {
title = form.Value["title"][0]
}
category := ""
if len(form.Value["category"]) > 0 {
category = form.Value["category"][0]
}
poster := ""
if len(form.Value["poster"]) > 0 {
poster = form.Value["poster"][0]
}
data := ""
if len(form.Value["data"]) > 0 {
data = form.Value["data"][0]
}
var files []*multipart.FileHeader
for _, fh := range form.File {
files = append(files, fh...)
}
var stats []*state.TorrentStatus
for _, fh := range files {
log.TLogln("add .torrent", fh.Filename)
torrFile, err := fh.Open()
if err != nil {
log.TLogln("error upload torrent:", err)
continue
}
spec, err := utils.ParseFile(torrFile)
torrFile.Close()
if err != nil {
log.TLogln("error upload torrent:", err)
continue
}
tor, err := torr.AddTorrent(spec, title, poster, data, category)
if err != nil {
log.TLogln("error upload torrent:", err)
continue
}
if tor.Data != "" && set.BTsets.EnableDebug {
log.TLogln("torrent data:", tor.Data)
}
if tor.Category != "" && set.BTsets.EnableDebug {
log.TLogln("torrent category:", tor.Category)
}
go func(t *torr.Torrent) {
if !t.GotInfo() {
log.TLogln("error add torrent:", "torrent connection timeout")
return
}
if t.Title == "" {
t.Title = t.Name()
}
if save {
torr.SaveTorrentToDB(t)
}
}(tor)
stats = append(stats, tor.Status())
}
c.JSON(200, stats)
}
+183
View File
@@ -0,0 +1,183 @@
package utils
import (
"bytes"
"errors"
"fmt"
"mime/multipart"
"net/http"
"net/url"
"runtime"
"server/torrshash"
"strings"
"time"
"github.com/anacrolix/torrent"
"github.com/anacrolix/torrent/metainfo"
)
func ParseFromBytes(data []byte) (*torrent.TorrentSpec, error) {
minfo, err := metainfo.Load(bytes.NewReader(data))
if err != nil {
return nil, err
}
info, err := minfo.UnmarshalInfo()
if err != nil {
return nil, err
}
mag := minfo.Magnet(nil, &info)
return &torrent.TorrentSpec{
InfoBytes: minfo.InfoBytes,
Trackers: [][]string{mag.Trackers},
DisplayName: info.Name,
InfoHash: minfo.HashInfoBytes(),
}, nil
}
func ParseFile(file multipart.File) (*torrent.TorrentSpec, error) {
minfo, err := metainfo.Load(file)
if err != nil {
return nil, err
}
info, err := minfo.UnmarshalInfo()
if err != nil {
return nil, err
}
// mag := minfo.Magnet(info.Name, minfo.HashInfoBytes())
mag := minfo.Magnet(nil, &info)
return &torrent.TorrentSpec{
InfoBytes: minfo.InfoBytes,
Trackers: [][]string{mag.Trackers},
DisplayName: info.Name,
InfoHash: minfo.HashInfoBytes(),
}, nil
}
func ParseLink(link string) (*torrent.TorrentSpec, error) {
urlLink, err := url.Parse(link)
if err != nil {
return nil, err
}
switch strings.ToLower(urlLink.Scheme) {
case "magnet":
return fromMagnet(urlLink.String())
case "http", "https":
return fromHttp(urlLink.String())
case "":
return fromMagnet("magnet:?xt=urn:btih:" + urlLink.Path)
case "file":
return fromFile(urlLink.Path)
default:
err = fmt.Errorf("unknown scheme:", urlLink, urlLink.Scheme)
}
return nil, err
}
func fromMagnet(link string) (*torrent.TorrentSpec, error) {
mag, err := metainfo.ParseMagnetUri(link)
if err != nil {
return nil, err
}
var trackers [][]string
if len(mag.Trackers) > 0 {
trackers = [][]string{mag.Trackers}
}
return &torrent.TorrentSpec{
InfoBytes: nil,
Trackers: trackers,
DisplayName: mag.DisplayName,
InfoHash: mag.InfoHash,
}, nil
}
func ParseTorrsHash(token string) (*torrent.TorrentSpec, *torrshash.TorrsHash, error) {
if strings.HasPrefix(token, "torrs://") {
token = strings.TrimPrefix(token, "torrs://")
}
th, err := torrshash.Unpack(token)
if err != nil {
return nil, nil, err
}
var trackers [][]string
if len(th.Trackers()) > 0 {
trackers = [][]string{th.Trackers()}
}
return &torrent.TorrentSpec{
InfoBytes: nil,
Trackers: trackers,
DisplayName: th.Title(),
InfoHash: metainfo.NewHashFromHex(th.Hash),
}, th, nil
}
func fromHttp(link string) (*torrent.TorrentSpec, error) {
req, err := http.NewRequest("GET", link, nil)
if err != nil {
return nil, err
}
client := new(http.Client)
client.Timeout = time.Duration(time.Second * 60)
req.Header.Set("User-Agent", "DWL/1.1.1 (Torrent)")
resp, err := client.Do(req)
if er, ok := err.(*url.Error); ok {
if strings.HasPrefix(er.URL, "magnet:") {
return fromMagnet(er.URL)
}
}
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return nil, errors.New(resp.Status)
}
minfo, err := metainfo.Load(resp.Body)
if err != nil {
return nil, err
}
info, err := minfo.UnmarshalInfo()
if err != nil {
return nil, err
}
// mag := minfo.Magnet(info.Name, minfo.HashInfoBytes())
mag := minfo.Magnet(nil, &info)
return &torrent.TorrentSpec{
InfoBytes: minfo.InfoBytes,
Trackers: [][]string{mag.Trackers},
DisplayName: info.Name,
InfoHash: minfo.HashInfoBytes(),
}, nil
}
func fromFile(path string) (*torrent.TorrentSpec, error) {
if runtime.GOOS == "windows" && strings.HasPrefix(path, "/") {
path = strings.TrimPrefix(path, "/")
}
minfo, err := metainfo.LoadFromFile(path)
if err != nil {
return nil, err
}
info, err := minfo.UnmarshalInfo()
if err != nil {
return nil, err
}
// mag := minfo.Magnet(info.Name, minfo.HashInfoBytes())
mag := minfo.Magnet(nil, &info)
return &torrent.TorrentSpec{
InfoBytes: minfo.InfoBytes,
Trackers: [][]string{mag.Trackers},
DisplayName: info.Name,
InfoHash: minfo.HashInfoBytes(),
}, nil
}
+27
View File
@@ -0,0 +1,27 @@
package utils
import (
"fmt"
"net/url"
)
func TestLink(link string, auth bool) error {
link, err := url.QueryUnescape(link)
if err != nil {
return err
}
ur, err := url.Parse(link)
if err != nil {
return err
}
if ur.Scheme == "magnet" || ur.Scheme == "torrs" || ur.Scheme == "" {
return nil
}
if !auth {
return fmt.Errorf("auth required")
}
return nil
}
+71
View File
@@ -0,0 +1,71 @@
package api
import (
"net/http"
sets "server/settings"
"github.com/gin-gonic/gin"
)
/*
file index starts from 1
*/
// Action: set, rem, list
type viewedReqJS struct {
requestI
*sets.Viewed
}
// viewed godoc
//
// @Summary Set / List / Remove viewed torrents
// @Description Allow to set, list or remove viewed torrents from server.
//
// @Tags API
//
// @Param request body viewedReqJS true "Viewed torrent request. Available params for action: set, rem, list"
//
// @Accept json
// @Produce json
// @Success 200 {array} sets.Viewed
// @Router /viewed [post]
func viewed(c *gin.Context) {
var req viewedReqJS
err := c.ShouldBindJSON(&req)
if err != nil {
c.AbortWithError(http.StatusBadRequest, err)
return
}
switch req.Action {
case "set":
{
setViewed(req, c)
}
case "rem":
{
remViewed(req, c)
}
case "list":
{
listViewed(req, c)
}
}
}
func setViewed(req viewedReqJS, c *gin.Context) {
sets.SetViewed(req.Viewed)
c.Status(200)
}
func remViewed(req viewedReqJS, c *gin.Context) {
sets.RemViewed(req.Viewed)
c.Status(200)
}
func listViewed(req viewedReqJS, c *gin.Context) {
list := sets.ListViewed(req.Hash)
c.JSON(200, list)
}
+105
View File
@@ -0,0 +1,105 @@
package auth
import (
"encoding/base64"
"encoding/json"
"net/http"
"os"
"path/filepath"
"unsafe"
"github.com/gin-gonic/gin"
"server/log"
"server/settings"
)
func SetupAuth(engine *gin.Engine) {
if !settings.HttpAuth {
return
}
accs := getAccounts()
if accs == nil {
return
}
engine.Use(BasicAuth(accs))
}
func getAccounts() gin.Accounts {
buf, err := os.ReadFile(filepath.Join(settings.Path, "accs.db"))
if err != nil {
return nil
}
var accs gin.Accounts
err = json.Unmarshal(buf, &accs)
if err != nil {
log.TLogln("Error parse accs.db", err)
}
return accs
}
type authPair struct {
value string
user string
}
type authPairs []authPair
func (a authPairs) searchCredential(authValue string) (string, bool) {
if authValue == "" {
return "", false
}
for _, pair := range a {
if pair.value == authValue {
return pair.user, true
}
}
return "", false
}
func BasicAuth(accounts gin.Accounts) gin.HandlerFunc {
pairs := processAccounts(accounts)
return func(c *gin.Context) {
c.Set("auth_required", true)
user, found := pairs.searchCredential(c.Request.Header.Get("Authorization"))
if found {
c.Set(gin.AuthUserKey, user)
}
}
}
func CheckAuth() gin.HandlerFunc {
return func(c *gin.Context) {
if !settings.HttpAuth {
return
}
if _, ok := c.Get(gin.AuthUserKey); ok {
return
}
c.Header("WWW-Authenticate", "Basic realm=Authorization Required")
c.AbortWithStatus(http.StatusUnauthorized)
}
}
func processAccounts(accounts gin.Accounts) authPairs {
pairs := make(authPairs, 0, len(accounts))
for user, password := range accounts {
value := authorizationHeader(user, password)
pairs = append(pairs, authPair{
value: value,
user: user,
})
}
return pairs
}
func authorizationHeader(user, password string) string {
base := user + ":" + password
return "Basic " + base64.StdEncoding.EncodeToString(StringToBytes(base))
}
func StringToBytes(s string) (b []byte) {
return unsafe.Slice(unsafe.StringData(s), len(s))
}
+115
View File
@@ -0,0 +1,115 @@
package blocker
import (
"bufio"
"bytes"
"errors"
"net"
"net/http"
"os"
"path/filepath"
"strings"
"server/log"
"server/settings"
"github.com/gin-gonic/gin"
)
func Blocker() gin.HandlerFunc {
emptyFN := func(c *gin.Context) {
c.Next()
}
name := filepath.Join(settings.Path, "bip.txt")
buf, _ := os.ReadFile(name)
blackIpList := scanBuf(buf)
name = filepath.Join(settings.Path, "wip.txt")
buf, _ = os.ReadFile(name)
whiteIpList := scanBuf(buf)
if blackIpList.NumRanges() == 0 && whiteIpList.NumRanges() == 0 {
return emptyFN
}
return func(c *gin.Context) {
arr := strings.Split(c.Request.RemoteAddr, ":")
if len(arr) > 0 {
ip := net.ParseIP(arr[0])
minifyIP(&ip)
if whiteIpList.NumRanges() > 0 {
if _, ok := whiteIpList.Lookup(ip); !ok {
log.WebLogln("Block ip, not in white list", ip.String())
c.String(http.StatusTeapot, "Banned")
c.Abort()
return
}
}
if blackIpList.NumRanges() > 0 {
if r, ok := blackIpList.Lookup(ip); ok {
log.WebLogln("Block ip, in black list:", ip.String(), "in range", r.Description, ":", r.First, "-", r.Last)
c.String(http.StatusTeapot, "Banned")
c.Abort()
return
}
}
}
c.Next()
}
}
func scanBuf(buf []byte) Ranger {
if len(buf) == 0 {
return New(nil)
}
var ranges []Range
scanner := bufio.NewScanner(strings.NewReader(string(buf)))
for scanner.Scan() {
r, ok, err := parseLine(scanner.Bytes())
if err != nil {
log.TLogln("Error scan ip list:", err)
return New(nil)
}
if ok {
ranges = append(ranges, r)
}
}
err := scanner.Err()
if err != nil {
log.TLogln("Error scan ip list:", err)
}
if len(ranges) > 0 {
return New(ranges)
}
return New(nil)
}
func parseLine(l []byte) (r Range, ok bool, err error) {
l = bytes.TrimSpace(l)
if len(l) == 0 || bytes.HasPrefix(l, []byte("#")) {
return
}
colon := bytes.LastIndexAny(l, ":")
hyphen := bytes.IndexByte(l[colon+1:], '-')
hyphen += colon + 1
if colon >= 0 {
r.Description = string(l[:colon])
}
if hyphen-(colon+1) >= 0 {
r.First = net.ParseIP(string(l[colon+1 : hyphen]))
minifyIP(&r.First)
r.Last = net.ParseIP(string(l[hyphen+1:]))
minifyIP(&r.Last)
} else {
r.First = net.ParseIP(string(l[colon+1:]))
minifyIP(&r.First)
r.Last = r.First
}
if r.First == nil || r.Last == nil || len(r.First) != len(r.Last) {
err = errors.New("bad IP range")
return
}
ok = true
return
}
+87
View File
@@ -0,0 +1,87 @@
package blocker
import (
"bytes"
"fmt"
"net"
)
type Ranger interface {
Lookup(net.IP) (r Range, ok bool)
NumRanges() int
}
type IPList struct {
ranges []Range
}
type Range struct {
First, Last net.IP
Description string
}
func (r Range) String() string {
return fmt.Sprintf("%s-%s: %s", r.First, r.Last, r.Description)
}
// Create a new IP list. The given ranges must already sorted by the lower
// bound IP in each range. Behaviour is undefined for lists of overlapping
// ranges.
func New(initSorted []Range) *IPList {
return &IPList{
ranges: initSorted,
}
}
func (ipl *IPList) NumRanges() int {
if ipl == nil {
return 0
}
return len(ipl.ranges)
}
// Return the range the given IP is in. ok if false if no range is found.
func (ipl *IPList) Lookup(ip net.IP) (r Range, ok bool) {
if ipl == nil {
return
}
v4 := ip.To4()
if v4 != nil {
r, ok = ipl.lookup(v4)
if ok {
return
}
}
v6 := ip.To16()
if v6 != nil {
return ipl.lookup(v6)
}
if v4 == nil && v6 == nil {
r = Range{
Description: "bad IP",
}
ok = true
}
return
}
// Return the range the given IP is in. Returns nil if no range is found.
func (ipl *IPList) lookup(ip net.IP) (Range, bool) {
var rng Range
ok := false
for _, r := range ipl.ranges {
ok = bytes.Compare(r.First, ip) <= 0 && bytes.Compare(ip, r.Last) <= 0
if ok {
rng = r
break
}
}
return rng, ok
}
func minifyIP(ip *net.IP) {
v4 := ip.To4()
if v4 != nil {
*ip = append(make([]byte, 0, 4), v4...)
}
}
+48
View File
@@ -0,0 +1,48 @@
package web
import (
"net"
"net/http"
"net/url"
"server/log"
"server/settings"
)
func runHTTPRedirectToHTTPS(addr string) error {
h := func(w http.ResponseWriter, r *http.Request) {
target := buildHTTPSRedirectTarget(r)
http.Redirect(w, r, target, http.StatusTemporaryRedirect)
}
log.TLogln("Start http server (redirect to https) at", addr)
return http.ListenAndServe(addr, http.HandlerFunc(h))
}
func buildHTTPSRedirectTarget(r *http.Request) string {
host := r.Host
hostName, _, err := net.SplitHostPort(host)
if err != nil {
hostName = host
}
sslPort := settings.SslPort
if sslPort == "" {
sslPort = "8091"
}
var httpsHost string
if sslPort == "443" {
httpsHost = hostName
} else {
httpsHost = net.JoinHostPort(hostName, sslPort)
}
path := r.URL.EscapedPath()
if path == "" {
path = "/"
}
u := &url.URL{
Scheme: "https",
Host: httpsHost,
Path: path,
RawQuery: r.URL.RawQuery,
}
return u.String()
}
+179
View File
@@ -0,0 +1,179 @@
package msx
import (
"encoding/json"
"errors"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"server/settings"
"server/torr"
"server/utils"
"server/version"
"server/web/auth"
"github.com/gin-gonic/gin"
)
const base, files = "tsmsx.yourok.ru", "media"
var param = "menu:request:interaction:{SERVER}@{PREFIX}" + base + "/start.html"
func trn(h string) (st, sc string) {
if h := torr.GetTorrent(h); h != nil {
if h := h.Status(); h != nil && h.Stat < 5 {
switch h.Stat {
case 4:
sc = "msx-red"
case 3:
sc = "msx-green"
default:
sc = "msx-yellow"
}
st = "{ico:north} " + strconv.Itoa(h.ActivePeers) + " / " + strconv.Itoa(h.TotalPeers) + " {ico:south} " + strconv.Itoa(h.ConnectedSeeders)
}
}
return
}
func rsp(c *gin.Context, r *http.Response, e error) {
if e != nil {
c.AbortWithError(http.StatusInternalServerError, e)
} else {
defer r.Body.Close()
c.DataFromReader(r.StatusCode, r.ContentLength, r.Header.Get("Content-Type"), r.Body, nil)
}
}
func SetupRoute(r gin.IRouter) {
authorized := r.Group("/", auth.CheckAuth())
// MSX:
authorized.GET("/msx/", func(c *gin.Context) {
r, e := http.Get("http://" + base)
rsp(c, r, e)
})
authorized.GET("/msx/start.json", func(c *gin.Context) {
c.JSON(http.StatusOK, map[string]any{
"name": "TorrServer",
"version": version.Version,
"parameter": param,
"launcher": map[string]any{
"type": "start",
"image": utils.GetScheme(c) + "://" + c.Request.Host + "/logo.png",
},
})
})
authorized.POST("/msx/start.json", func(c *gin.Context) {
if e := c.BindJSON(&param); e != nil {
c.AbortWithError(http.StatusBadRequest, e)
}
})
authorized.GET("/msx/trn", func(c *gin.Context) {
r := false
if h := c.Query("hash"); h != "" {
for _, t := range settings.ListTorrent() {
if r = (t != nil && t.InfoHash.HexString() == h); r {
break
}
}
}
c.JSON(http.StatusOK, r)
})
authorized.POST("/msx/trn", func(c *gin.Context) {
var r struct {
R struct {
S int `json:"status"`
T string `json:"text"`
M string `json:"message,omitempty"`
D map[string]any `json:"data,omitempty"`
} `json:"response"`
}
if j := struct{ Data string }{Data: c.Query("hash")}; j.Data != "" {
st, sc := trn(j.Data)
if sc != "" {
sc = "{col:" + sc + "}"
}
r.R.S, r.R.D = http.StatusOK, map[string]any{"action": "player:label:position:{VALUE}{tb}{tb}" + sc + st}
} else if e := c.BindJSON(&j); e != nil {
r.R.S, r.R.M = http.StatusBadRequest, e.Error()
} else if j.Data == "" {
r.R.S, r.R.M = http.StatusBadRequest, "data is not set"
} else {
st, sc := trn(j.Data[strings.LastIndexByte(j.Data, ':')+1:])
r.R.D = map[string]any{"stamp": st, "stampColor": sc}
if sc != "" {
r.R.D["live"] = map[string]any{
"type": "airtime", "duration": 3000, "over": map[string]any{
"action": "execute:" + utils.GetScheme(c) + "://" + c.Request.Host + c.Request.URL.Path, "data": j.Data,
},
}
}
r.R.S, r.R.D = http.StatusOK, map[string]any{"action": j.Data, "data": r.R.D}
}
r.R.T = http.StatusText(r.R.S)
c.JSON(http.StatusOK, &r)
})
authorized.Any("/msx/proxy", func(c *gin.Context) {
if u := c.Query("url"); u == "" {
c.AbortWithStatus(http.StatusBadRequest)
} else if q, e := http.NewRequest(c.Request.Method, u, c.Request.Body); e != nil {
c.AbortWithError(http.StatusInternalServerError, e)
} else {
for _, v := range c.QueryArray("header") {
if v := strings.SplitN(v, ":", 2); len(v) == 2 {
q.Header.Add(v[0], v[1])
}
}
r, e := http.DefaultClient.Do(q)
rsp(c, r, e)
}
})
authorized.GET("/msx/imdb/:id", func(c *gin.Context) {
i, j := strings.TrimPrefix(c.Param("id"), "/"), false
if j = strings.HasSuffix(i, ".json"); !j {
i += ".json"
}
if r, e := http.Get("https://v2.sg.media-imdb.com/suggestion/h/" + i); e != nil || r.StatusCode != http.StatusOK || j {
rsp(c, r, e)
} else {
var j struct {
D []struct{ I struct{ ImageUrl string } }
}
if e = json.NewDecoder(r.Body).Decode(&j); e != nil {
c.AbortWithError(http.StatusInternalServerError, e)
} else if len(j.D) == 0 || j.D[0].I.ImageUrl == "" {
c.Status(http.StatusNotFound)
} else {
c.Redirect(http.StatusMovedPermanently, j.D[0].I.ImageUrl)
}
}
})
// Files:
authorized.StaticFS("/files/", gin.Dir(filepath.Join(settings.Path, files), true))
authorized.GET("/files", func(c *gin.Context) {
if l, e := os.Readlink(filepath.Join(settings.Path, files)); e == nil || os.IsNotExist(e) {
c.JSON(http.StatusOK, l)
} else {
c.JSON(http.StatusInternalServerError, e.Error)
}
})
authorized.POST("/files", func(c *gin.Context) {
var l string
if e := c.BindJSON(&l); e != nil {
c.AbortWithError(http.StatusBadRequest, e)
} else if e = os.Remove(filepath.Join(settings.Path, files)); e != nil && !os.IsNotExist(e) {
c.AbortWithError(http.StatusInternalServerError, e)
} else if l != "" {
if f, e := os.Stat(l); e != nil {
c.AbortWithError(http.StatusBadRequest, e)
} else if !f.IsDir() {
c.AbortWithError(http.StatusBadRequest, errors.New(l+" is not a directory"))
} else if e = os.Symlink(l, filepath.Join(settings.Path, files)); e != nil {
c.AbortWithError(http.StatusInternalServerError, e)
}
}
})
}
+83
View File
@@ -0,0 +1,83 @@
package pages
import (
"net/http"
"server/proxy"
"github.com/anacrolix/torrent/metainfo"
"github.com/gin-gonic/gin"
"server/settings"
"server/torr"
"server/web/auth"
"server/web/pages/template"
"golang.org/x/exp/slices"
)
func SetupRoute(route gin.IRouter) {
authorized := route.Group("/", auth.CheckAuth())
webPagesAuth := route.Group("/", func() gin.HandlerFunc {
return func(c *gin.Context) {
if slices.Contains([]string{"/site.webmanifest"}, c.FullPath()) {
return
}
auth.CheckAuth()(c)
}
}())
template.RouteWebPages(webPagesAuth)
authorized.GET("/stat", statPage)
authorized.GET("/magnets", getTorrents)
authorized.Any("/proxy/*url", proxyUrl)
}
// stat godoc
//
// @Summary TorrServer Statistics
// @Description Show server and torrents statistics.
//
// @Tags Pages
//
// @Produce text/plain
// @Success 200 "TorrServer statistics"
// @Router /stat [get]
func statPage(c *gin.Context) {
torr.WriteStatus(c.Writer)
c.Status(200)
}
// getTorrents godoc
//
// @Summary Get HTML of magnet links
// @Description Get HTML of magnet links.
//
// @Tags Pages
//
// @Produce text/html
// @Success 200 "HTML with Magnet links"
// @Router /magnets [get]
func getTorrents(c *gin.Context) {
list := settings.ListTorrent()
http := "<div>"
for _, db := range list {
ts := db.TorrentSpec
mi := metainfo.MetaInfo{
AnnounceList: ts.Trackers,
}
// mag := mi.Magnet(ts.DisplayName, ts.InfoHash)
mag := mi.Magnet(&ts.InfoHash, &metainfo.Info{Name: ts.DisplayName})
http += "<p><a href='" + mag.String() + "'>magnet:?xt=urn:btih:" + mag.InfoHash.HexString() + "</a></p>"
}
http += "</div>"
c.Data(200, "text/html; charset=utf-8", []byte(http))
}
func proxyUrl(c *gin.Context) {
if proxy.P2Proxy != nil {
proxy.P2Proxy.GinHandler(c)
return
}
c.AbortWithStatus(http.StatusNotFound)
}
+149
View File
@@ -0,0 +1,149 @@
package template
import (
_ "embed"
)
//go:embed pages/apple-splash-1125-2436.jpg
var Applesplash11252436jpg []byte
//go:embed pages/apple-splash-1136-640.jpg
var Applesplash1136640jpg []byte
//go:embed pages/apple-splash-1170-2532.jpg
var Applesplash11702532jpg []byte
//go:embed pages/apple-splash-1242-2208.jpg
var Applesplash12422208jpg []byte
//go:embed pages/apple-splash-1242-2688.jpg
var Applesplash12422688jpg []byte
//go:embed pages/apple-splash-1284-2778.jpg
var Applesplash12842778jpg []byte
//go:embed pages/apple-splash-1334-750.jpg
var Applesplash1334750jpg []byte
//go:embed pages/apple-splash-1536-2048.jpg
var Applesplash15362048jpg []byte
//go:embed pages/apple-splash-1620-2160.jpg
var Applesplash16202160jpg []byte
//go:embed pages/apple-splash-1668-2224.jpg
var Applesplash16682224jpg []byte
//go:embed pages/apple-splash-1668-2388.jpg
var Applesplash16682388jpg []byte
//go:embed pages/apple-splash-1792-828.jpg
var Applesplash1792828jpg []byte
//go:embed pages/apple-splash-2048-1536.jpg
var Applesplash20481536jpg []byte
//go:embed pages/apple-splash-2048-2732.jpg
var Applesplash20482732jpg []byte
//go:embed pages/apple-splash-2160-1620.jpg
var Applesplash21601620jpg []byte
//go:embed pages/apple-splash-2208-1242.jpg
var Applesplash22081242jpg []byte
//go:embed pages/apple-splash-2224-1668.jpg
var Applesplash22241668jpg []byte
//go:embed pages/apple-splash-2388-1668.jpg
var Applesplash23881668jpg []byte
//go:embed pages/apple-splash-2436-1125.jpg
var Applesplash24361125jpg []byte
//go:embed pages/apple-splash-2532-1170.jpg
var Applesplash25321170jpg []byte
//go:embed pages/apple-splash-2688-1242.jpg
var Applesplash26881242jpg []byte
//go:embed pages/apple-splash-2732-2048.jpg
var Applesplash27322048jpg []byte
//go:embed pages/apple-splash-2778-1284.jpg
var Applesplash27781284jpg []byte
//go:embed pages/apple-splash-640-1136.jpg
var Applesplash6401136jpg []byte
//go:embed pages/apple-splash-750-1334.jpg
var Applesplash7501334jpg []byte
//go:embed pages/apple-splash-828-1792.jpg
var Applesplash8281792jpg []byte
//go:embed pages/asset-manifest.json
var Assetmanifestjson []byte
//go:embed pages/browserconfig.xml
var Browserconfigxml []byte
//go:embed pages/dlnaicon-120.png
var Dlnaicon120png []byte
//go:embed pages/dlnaicon-48.png
var Dlnaicon48png []byte
//go:embed pages/favicon-16x16.png
var Favicon16x16png []byte
//go:embed pages/favicon-32x32.png
var Favicon32x32png []byte
//go:embed pages/favicon.ico
var Faviconico []byte
//go:embed pages/icon.png
var Iconpng []byte
//go:embed pages/index.html
var Indexhtml []byte
//go:embed pages/logo.png
var Logopng []byte
//go:embed pages/lordicon/jkzgajyr.json
var Lordiconjkzgajyrjson []byte
//go:embed pages/lordicon/lord-icon-2.0.2.js
var Lordiconlordicon202js []byte
//go:embed pages/lordicon/wrprwmwt.json
var Lordiconwrprwmwtjson []byte
//go:embed pages/mstile-150x150.png
var Mstile150x150png []byte
//go:embed pages/site.webmanifest
var Sitewebmanifest []byte
//go:embed pages/static/js/2.17225dbd.chunk.js
var Staticjs217225dbdchunkjs []byte
//go:embed pages/static/js/2.17225dbd.chunk.js.LICENSE.txt
var Staticjs217225dbdchunkjsLICENSEtxt []byte
//go:embed pages/static/js/2.17225dbd.chunk.js.map
var Staticjs217225dbdchunkjsmap []byte
//go:embed pages/static/js/main.c7b9a3c5.chunk.js
var Staticjsmainc7b9a3c5chunkjs []byte
//go:embed pages/static/js/main.c7b9a3c5.chunk.js.map
var Staticjsmainc7b9a3c5chunkjsmap []byte
//go:embed pages/static/js/runtime-main.5ed86a79.js
var Staticjsruntimemain5ed86a79js []byte
//go:embed pages/static/js/runtime-main.5ed86a79.js.map
var Staticjsruntimemain5ed86a79jsmap []byte
Binary file not shown.

After

Width:  |  Height:  |  Size: 386 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 411 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 403 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 455 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 480 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 476 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 516 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 547 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 572 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 535 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 734 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 595 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 537 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 624 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 675 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 596 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 638 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 691 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 848 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 730 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 KiB

@@ -0,0 +1,17 @@
{
"files": {
"main.js": "./static/js/main.c7b9a3c5.chunk.js",
"main.js.map": "./static/js/main.c7b9a3c5.chunk.js.map",
"runtime-main.js": "./static/js/runtime-main.5ed86a79.js",
"runtime-main.js.map": "./static/js/runtime-main.5ed86a79.js.map",
"static/js/2.17225dbd.chunk.js": "./static/js/2.17225dbd.chunk.js",
"static/js/2.17225dbd.chunk.js.map": "./static/js/2.17225dbd.chunk.js.map",
"index.html": "./index.html",
"static/js/2.17225dbd.chunk.js.LICENSE.txt": "./static/js/2.17225dbd.chunk.js.LICENSE.txt"
},
"entrypoints": [
"static/js/runtime-main.5ed86a79.js",
"static/js/2.17225dbd.chunk.js",
"static/js/main.c7b9a3c5.chunk.js"
]
}
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square150x150logo src="/mstile-150x150.png"/>
<TileColor>#00a572</TileColor>
<TileImage src="/mstile-150x150.png" />
</tile>
</msapplication>
</browserconfig>
Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 824 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 213 KiB

File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

@@ -0,0 +1,21 @@
{
"name": "TorrServer",
"short_name": "TorrServer",
"icons": [
{
"src": "icon.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any"
},
{
"src": "logo.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,117 @@
/*
object-assign
(c) Sindre Sorhus
@license MIT
*/
/*!
* The buffer module from node.js, for the browser.
*
* @author Feross Aboukhadijeh <http://feross.org>
* @license MIT
*/
/*! blob-to-buffer. MIT License. Feross Aboukhadijeh <https://feross.org/opensource> */
/*! https://mths.be/punycode v1.4.1 by @mathias */
/*! ieee754. BSD-3-Clause License. Feross Aboukhadijeh <https://feross.org/opensource> */
/*! magnet-uri. MIT License. WebTorrent LLC <https://webtorrent.io/opensource> */
/*! parse-torrent. MIT License. WebTorrent LLC <https://webtorrent.io/opensource> */
/*! queue-microtask. MIT License. Feross Aboukhadijeh <https://feross.org/opensource> */
/*! safe-buffer. MIT License. Feross Aboukhadijeh <https://feross.org/opensource> */
/*! simple-concat. MIT License. Feross Aboukhadijeh <https://feross.org/opensource> */
/*! simple-get. MIT License. Feross Aboukhadijeh <https://feross.org/opensource> */
/**
* A better abstraction over CSS.
*
* @copyright Oleg Isonen (Slobodskoi) / Isonen 2014-present
* @website https://github.com/cssinjs/jss
* @license MIT
*/
/** @license React v0.20.2
* scheduler.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/** @license React v16.13.1
* react-is.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/** @license React v17.0.2
* react-dom.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/** @license React v17.0.2
* react-is.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/** @license React v17.0.2
* react-jsx-runtime.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/** @license React v17.0.2
* react.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**!
* @fileOverview Kickass library to create and place poppers near their reference elements.
* @version 1.16.1-lts
* @license
* Copyright (c) 2016 Federico Zivolo and contributors
*
* 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 diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,2 @@
!function(e){function r(r){for(var n,l,f=r[0],i=r[1],a=r[2],c=0,s=[];c<f.length;c++)l=f[c],Object.prototype.hasOwnProperty.call(o,l)&&o[l]&&s.push(o[l][0]),o[l]=0;for(n in i)Object.prototype.hasOwnProperty.call(i,n)&&(e[n]=i[n]);for(p&&p(r);s.length;)s.shift()();return u.push.apply(u,a||[]),t()}function t(){for(var e,r=0;r<u.length;r++){for(var t=u[r],n=!0,f=1;f<t.length;f++){var i=t[f];0!==o[i]&&(n=!1)}n&&(u.splice(r--,1),e=l(l.s=t[0]))}return e}var n={},o={1:0},u=[];function l(r){if(n[r])return n[r].exports;var t=n[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,l),t.l=!0,t.exports}l.m=e,l.c=n,l.d=function(e,r,t){l.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},l.r=function(e){"undefined"!==typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},l.t=function(e,r){if(1&r&&(e=l(e)),8&r)return e;if(4&r&&"object"===typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(l.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var n in e)l.d(t,n,function(r){return e[r]}.bind(null,n));return t},l.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return l.d(r,"a",r),r},l.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},l.p="./";var f=this.webpackJsonptorrserver_web=this.webpackJsonptorrserver_web||[],i=f.push.bind(f);f.push=r,f=f.slice();for(var a=0;a<f.length;a++)r(f[a]);var p=i;t()}([]);
//# sourceMappingURL=runtime-main.5ed86a79.js.map
File diff suppressed because one or more lines are too long
+352
View File
@@ -0,0 +1,352 @@
package template
import (
"crypto/md5"
"fmt"
"github.com/gin-gonic/gin"
)
func RouteWebPages(route gin.IRouter) {
route.GET("/", func(c *gin.Context) {
etag := fmt.Sprintf("%x", md5.Sum(Indexhtml))
c.Header("Cache-Control", "public, max-age=31536000")
c.Header("ETag", etag)
c.Data(200, "text/html; charset=utf-8", Indexhtml)
})
route.GET("/apple-splash-1125-2436.jpg", func(c *gin.Context) {
etag := fmt.Sprintf("%x", md5.Sum(Applesplash11252436jpg))
c.Header("Cache-Control", "public, max-age=31536000")
c.Header("ETag", etag)
c.Data(200, "image/jpeg", Applesplash11252436jpg)
})
route.GET("/apple-splash-1136-640.jpg", func(c *gin.Context) {
etag := fmt.Sprintf("%x", md5.Sum(Applesplash1136640jpg))
c.Header("Cache-Control", "public, max-age=31536000")
c.Header("ETag", etag)
c.Data(200, "image/jpeg", Applesplash1136640jpg)
})
route.GET("/apple-splash-1170-2532.jpg", func(c *gin.Context) {
etag := fmt.Sprintf("%x", md5.Sum(Applesplash11702532jpg))
c.Header("Cache-Control", "public, max-age=31536000")
c.Header("ETag", etag)
c.Data(200, "image/jpeg", Applesplash11702532jpg)
})
route.GET("/apple-splash-1242-2208.jpg", func(c *gin.Context) {
etag := fmt.Sprintf("%x", md5.Sum(Applesplash12422208jpg))
c.Header("Cache-Control", "public, max-age=31536000")
c.Header("ETag", etag)
c.Data(200, "image/jpeg", Applesplash12422208jpg)
})
route.GET("/apple-splash-1242-2688.jpg", func(c *gin.Context) {
etag := fmt.Sprintf("%x", md5.Sum(Applesplash12422688jpg))
c.Header("Cache-Control", "public, max-age=31536000")
c.Header("ETag", etag)
c.Data(200, "image/jpeg", Applesplash12422688jpg)
})
route.GET("/apple-splash-1284-2778.jpg", func(c *gin.Context) {
etag := fmt.Sprintf("%x", md5.Sum(Applesplash12842778jpg))
c.Header("Cache-Control", "public, max-age=31536000")
c.Header("ETag", etag)
c.Data(200, "image/jpeg", Applesplash12842778jpg)
})
route.GET("/apple-splash-1334-750.jpg", func(c *gin.Context) {
etag := fmt.Sprintf("%x", md5.Sum(Applesplash1334750jpg))
c.Header("Cache-Control", "public, max-age=31536000")
c.Header("ETag", etag)
c.Data(200, "image/jpeg", Applesplash1334750jpg)
})
route.GET("/apple-splash-1536-2048.jpg", func(c *gin.Context) {
etag := fmt.Sprintf("%x", md5.Sum(Applesplash15362048jpg))
c.Header("Cache-Control", "public, max-age=31536000")
c.Header("ETag", etag)
c.Data(200, "image/jpeg", Applesplash15362048jpg)
})
route.GET("/apple-splash-1620-2160.jpg", func(c *gin.Context) {
etag := fmt.Sprintf("%x", md5.Sum(Applesplash16202160jpg))
c.Header("Cache-Control", "public, max-age=31536000")
c.Header("ETag", etag)
c.Data(200, "image/jpeg", Applesplash16202160jpg)
})
route.GET("/apple-splash-1668-2224.jpg", func(c *gin.Context) {
etag := fmt.Sprintf("%x", md5.Sum(Applesplash16682224jpg))
c.Header("Cache-Control", "public, max-age=31536000")
c.Header("ETag", etag)
c.Data(200, "image/jpeg", Applesplash16682224jpg)
})
route.GET("/apple-splash-1668-2388.jpg", func(c *gin.Context) {
etag := fmt.Sprintf("%x", md5.Sum(Applesplash16682388jpg))
c.Header("Cache-Control", "public, max-age=31536000")
c.Header("ETag", etag)
c.Data(200, "image/jpeg", Applesplash16682388jpg)
})
route.GET("/apple-splash-1792-828.jpg", func(c *gin.Context) {
etag := fmt.Sprintf("%x", md5.Sum(Applesplash1792828jpg))
c.Header("Cache-Control", "public, max-age=31536000")
c.Header("ETag", etag)
c.Data(200, "image/jpeg", Applesplash1792828jpg)
})
route.GET("/apple-splash-2048-1536.jpg", func(c *gin.Context) {
etag := fmt.Sprintf("%x", md5.Sum(Applesplash20481536jpg))
c.Header("Cache-Control", "public, max-age=31536000")
c.Header("ETag", etag)
c.Data(200, "image/jpeg", Applesplash20481536jpg)
})
route.GET("/apple-splash-2048-2732.jpg", func(c *gin.Context) {
etag := fmt.Sprintf("%x", md5.Sum(Applesplash20482732jpg))
c.Header("Cache-Control", "public, max-age=31536000")
c.Header("ETag", etag)
c.Data(200, "image/jpeg", Applesplash20482732jpg)
})
route.GET("/apple-splash-2160-1620.jpg", func(c *gin.Context) {
etag := fmt.Sprintf("%x", md5.Sum(Applesplash21601620jpg))
c.Header("Cache-Control", "public, max-age=31536000")
c.Header("ETag", etag)
c.Data(200, "image/jpeg", Applesplash21601620jpg)
})
route.GET("/apple-splash-2208-1242.jpg", func(c *gin.Context) {
etag := fmt.Sprintf("%x", md5.Sum(Applesplash22081242jpg))
c.Header("Cache-Control", "public, max-age=31536000")
c.Header("ETag", etag)
c.Data(200, "image/jpeg", Applesplash22081242jpg)
})
route.GET("/apple-splash-2224-1668.jpg", func(c *gin.Context) {
etag := fmt.Sprintf("%x", md5.Sum(Applesplash22241668jpg))
c.Header("Cache-Control", "public, max-age=31536000")
c.Header("ETag", etag)
c.Data(200, "image/jpeg", Applesplash22241668jpg)
})
route.GET("/apple-splash-2388-1668.jpg", func(c *gin.Context) {
etag := fmt.Sprintf("%x", md5.Sum(Applesplash23881668jpg))
c.Header("Cache-Control", "public, max-age=31536000")
c.Header("ETag", etag)
c.Data(200, "image/jpeg", Applesplash23881668jpg)
})
route.GET("/apple-splash-2436-1125.jpg", func(c *gin.Context) {
etag := fmt.Sprintf("%x", md5.Sum(Applesplash24361125jpg))
c.Header("Cache-Control", "public, max-age=31536000")
c.Header("ETag", etag)
c.Data(200, "image/jpeg", Applesplash24361125jpg)
})
route.GET("/apple-splash-2532-1170.jpg", func(c *gin.Context) {
etag := fmt.Sprintf("%x", md5.Sum(Applesplash25321170jpg))
c.Header("Cache-Control", "public, max-age=31536000")
c.Header("ETag", etag)
c.Data(200, "image/jpeg", Applesplash25321170jpg)
})
route.GET("/apple-splash-2688-1242.jpg", func(c *gin.Context) {
etag := fmt.Sprintf("%x", md5.Sum(Applesplash26881242jpg))
c.Header("Cache-Control", "public, max-age=31536000")
c.Header("ETag", etag)
c.Data(200, "image/jpeg", Applesplash26881242jpg)
})
route.GET("/apple-splash-2732-2048.jpg", func(c *gin.Context) {
etag := fmt.Sprintf("%x", md5.Sum(Applesplash27322048jpg))
c.Header("Cache-Control", "public, max-age=31536000")
c.Header("ETag", etag)
c.Data(200, "image/jpeg", Applesplash27322048jpg)
})
route.GET("/apple-splash-2778-1284.jpg", func(c *gin.Context) {
etag := fmt.Sprintf("%x", md5.Sum(Applesplash27781284jpg))
c.Header("Cache-Control", "public, max-age=31536000")
c.Header("ETag", etag)
c.Data(200, "image/jpeg", Applesplash27781284jpg)
})
route.GET("/apple-splash-640-1136.jpg", func(c *gin.Context) {
etag := fmt.Sprintf("%x", md5.Sum(Applesplash6401136jpg))
c.Header("Cache-Control", "public, max-age=31536000")
c.Header("ETag", etag)
c.Data(200, "image/jpeg", Applesplash6401136jpg)
})
route.GET("/apple-splash-750-1334.jpg", func(c *gin.Context) {
etag := fmt.Sprintf("%x", md5.Sum(Applesplash7501334jpg))
c.Header("Cache-Control", "public, max-age=31536000")
c.Header("ETag", etag)
c.Data(200, "image/jpeg", Applesplash7501334jpg)
})
route.GET("/apple-splash-828-1792.jpg", func(c *gin.Context) {
etag := fmt.Sprintf("%x", md5.Sum(Applesplash8281792jpg))
c.Header("Cache-Control", "public, max-age=31536000")
c.Header("ETag", etag)
c.Data(200, "image/jpeg", Applesplash8281792jpg)
})
route.GET("/asset-manifest.json", func(c *gin.Context) {
etag := fmt.Sprintf("%x", md5.Sum(Assetmanifestjson))
c.Header("Cache-Control", "public, max-age=31536000")
c.Header("ETag", etag)
c.Data(200, "application/json", Assetmanifestjson)
})
route.GET("/browserconfig.xml", func(c *gin.Context) {
etag := fmt.Sprintf("%x", md5.Sum(Browserconfigxml))
c.Header("Cache-Control", "public, max-age=31536000")
c.Header("ETag", etag)
c.Data(200, "application/xml; charset=utf-8", Browserconfigxml)
})
route.GET("/dlnaicon-120.png", func(c *gin.Context) {
etag := fmt.Sprintf("%x", md5.Sum(Dlnaicon120png))
c.Header("Cache-Control", "public, max-age=31536000")
c.Header("ETag", etag)
c.Data(200, "image/png", Dlnaicon120png)
})
route.GET("/dlnaicon-48.png", func(c *gin.Context) {
etag := fmt.Sprintf("%x", md5.Sum(Dlnaicon48png))
c.Header("Cache-Control", "public, max-age=31536000")
c.Header("ETag", etag)
c.Data(200, "image/png", Dlnaicon48png)
})
route.GET("/favicon-16x16.png", func(c *gin.Context) {
etag := fmt.Sprintf("%x", md5.Sum(Favicon16x16png))
c.Header("Cache-Control", "public, max-age=31536000")
c.Header("ETag", etag)
c.Data(200, "image/png", Favicon16x16png)
})
route.GET("/favicon-32x32.png", func(c *gin.Context) {
etag := fmt.Sprintf("%x", md5.Sum(Favicon32x32png))
c.Header("Cache-Control", "public, max-age=31536000")
c.Header("ETag", etag)
c.Data(200, "image/png", Favicon32x32png)
})
route.GET("/favicon.ico", func(c *gin.Context) {
etag := fmt.Sprintf("%x", md5.Sum(Faviconico))
c.Header("Cache-Control", "public, max-age=31536000")
c.Header("ETag", etag)
c.Data(200, "image/vnd.microsoft.icon", Faviconico)
})
route.GET("/icon.png", func(c *gin.Context) {
etag := fmt.Sprintf("%x", md5.Sum(Iconpng))
c.Header("Cache-Control", "public, max-age=31536000")
c.Header("ETag", etag)
c.Data(200, "image/png", Iconpng)
})
route.GET("/index.html", func(c *gin.Context) {
etag := fmt.Sprintf("%x", md5.Sum(Indexhtml))
c.Header("Cache-Control", "public, max-age=31536000")
c.Header("ETag", etag)
c.Data(200, "text/html; charset=utf-8", Indexhtml)
})
route.GET("/logo.png", func(c *gin.Context) {
etag := fmt.Sprintf("%x", md5.Sum(Logopng))
c.Header("Cache-Control", "public, max-age=31536000")
c.Header("ETag", etag)
c.Data(200, "image/png", Logopng)
})
route.GET("/lordicon/jkzgajyr.json", func(c *gin.Context) {
etag := fmt.Sprintf("%x", md5.Sum(Lordiconjkzgajyrjson))
c.Header("Cache-Control", "public, max-age=31536000")
c.Header("ETag", etag)
c.Data(200, "application/json", Lordiconjkzgajyrjson)
})
route.GET("/lordicon/lord-icon-2.0.2.js", func(c *gin.Context) {
etag := fmt.Sprintf("%x", md5.Sum(Lordiconlordicon202js))
c.Header("Cache-Control", "public, max-age=31536000")
c.Header("ETag", etag)
c.Data(200, "application/javascript; charset=utf-8", Lordiconlordicon202js)
})
route.GET("/lordicon/wrprwmwt.json", func(c *gin.Context) {
etag := fmt.Sprintf("%x", md5.Sum(Lordiconwrprwmwtjson))
c.Header("Cache-Control", "public, max-age=31536000")
c.Header("ETag", etag)
c.Data(200, "application/json", Lordiconwrprwmwtjson)
})
route.GET("/mstile-150x150.png", func(c *gin.Context) {
etag := fmt.Sprintf("%x", md5.Sum(Mstile150x150png))
c.Header("Cache-Control", "public, max-age=31536000")
c.Header("ETag", etag)
c.Data(200, "image/png", Mstile150x150png)
})
route.GET("/site.webmanifest", func(c *gin.Context) {
etag := fmt.Sprintf("%x", md5.Sum(Sitewebmanifest))
c.Header("Cache-Control", "public, max-age=31536000")
c.Header("ETag", etag)
c.Data(200, "application/manifest+json", Sitewebmanifest)
})
route.GET("/static/js/2.17225dbd.chunk.js", func(c *gin.Context) {
etag := fmt.Sprintf("%x", md5.Sum(Staticjs217225dbdchunkjs))
c.Header("Cache-Control", "public, max-age=31536000")
c.Header("ETag", etag)
c.Data(200, "application/javascript; charset=utf-8", Staticjs217225dbdchunkjs)
})
route.GET("/static/js/2.17225dbd.chunk.js.LICENSE.txt", func(c *gin.Context) {
etag := fmt.Sprintf("%x", md5.Sum(Staticjs217225dbdchunkjsLICENSEtxt))
c.Header("Cache-Control", "public, max-age=31536000")
c.Header("ETag", etag)
c.Data(200, "text/plain; charset=utf-8", Staticjs217225dbdchunkjsLICENSEtxt)
})
route.GET("/static/js/2.17225dbd.chunk.js.map", func(c *gin.Context) {
etag := fmt.Sprintf("%x", md5.Sum(Staticjs217225dbdchunkjsmap))
c.Header("Cache-Control", "public, max-age=31536000")
c.Header("ETag", etag)
c.Data(200, "application/json", Staticjs217225dbdchunkjsmap)
})
route.GET("/static/js/main.c7b9a3c5.chunk.js", func(c *gin.Context) {
etag := fmt.Sprintf("%x", md5.Sum(Staticjsmainc7b9a3c5chunkjs))
c.Header("Cache-Control", "public, max-age=31536000")
c.Header("ETag", etag)
c.Data(200, "application/javascript; charset=utf-8", Staticjsmainc7b9a3c5chunkjs)
})
route.GET("/static/js/main.c7b9a3c5.chunk.js.map", func(c *gin.Context) {
etag := fmt.Sprintf("%x", md5.Sum(Staticjsmainc7b9a3c5chunkjsmap))
c.Header("Cache-Control", "public, max-age=31536000")
c.Header("ETag", etag)
c.Data(200, "application/json", Staticjsmainc7b9a3c5chunkjsmap)
})
route.GET("/static/js/runtime-main.5ed86a79.js", func(c *gin.Context) {
etag := fmt.Sprintf("%x", md5.Sum(Staticjsruntimemain5ed86a79js))
c.Header("Cache-Control", "public, max-age=31536000")
c.Header("ETag", etag)
c.Data(200, "application/javascript; charset=utf-8", Staticjsruntimemain5ed86a79js)
})
route.GET("/static/js/runtime-main.5ed86a79.js.map", func(c *gin.Context) {
etag := fmt.Sprintf("%x", md5.Sum(Staticjsruntimemain5ed86a79jsmap))
c.Header("Cache-Control", "public, max-age=31536000")
c.Header("ETag", etag)
c.Data(200, "application/json", Staticjsruntimemain5ed86a79jsmap)
})
}
+186
View File
@@ -0,0 +1,186 @@
package web
import (
"net"
"os"
"server/proxy"
"sort"
"server/torrfs/fuse"
"server/torrfs/webdav"
"server/rutor"
"github.com/gin-contrib/cors"
"github.com/gin-contrib/location/v2"
"github.com/gin-gonic/gin"
"github.com/wlynxg/anet"
"server/dlna"
"server/settings"
"server/web/msx"
"server/log"
"server/torr"
"server/version"
"server/web/api"
"server/web/auth"
"server/web/blocker"
"server/web/pages"
"server/web/sslcerts"
swaggerFiles "github.com/swaggo/files" // swagger embed files
ginSwagger "github.com/swaggo/gin-swagger" // gin-swagger middleware
)
var (
BTS = torr.NewBTS()
waitChan = make(chan error)
)
// @title Swagger Torrserver API
// @version {version.Version}
// @description Torrent streaming server.
// @license.name GPL 3.0
// @BasePath /
// @securityDefinitions.basic BasicAuth
// @externalDocs.description OpenAPI
// @externalDocs.url https://swagger.io/resources/open-api/
func Start() {
log.TLogln("Start TorrServer " + version.Version + " torrent " + version.GetTorrentVersion())
ips := GetLocalIps()
if len(ips) > 0 {
log.TLogln("Local IPs:", ips)
}
err := BTS.Connect()
if err != nil {
log.TLogln("BTS.Connect() error!", err) // waitChan <- err
os.Exit(1) // return
}
rutor.Start()
gin.SetMode(gin.ReleaseMode)
// corsCfg := cors.DefaultConfig()
// corsCfg.AllowAllOrigins = true
// corsCfg.AllowHeaders = []string{"*"}
// corsCfg.AllowMethods = []string{"*"}
corsCfg := cors.DefaultConfig()
corsCfg.AllowAllOrigins = true
corsCfg.AllowPrivateNetwork = true
corsCfg.AllowHeaders = []string{"Origin", "Content-Length", "Content-Type", "X-Requested-With", "Accept", "Authorization"}
route := gin.New()
route.Use(log.WebLogger(), blocker.Blocker(), gin.Recovery(), cors.New(corsCfg), location.Default())
auth.SetupAuth(route)
route.GET("/echo", echo)
api.SetupRoute(route)
msx.SetupRoute(route)
pages.SetupRoute(route)
if settings.Args.WebDAV {
webdav.MountWebDAV(route)
}
if settings.BTsets.EnableDLNA {
dlna.Start()
}
// Auto-mount FUSE filesystem if enabled
fuse.FuseAutoMount()
route.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
// check if https enabled
if settings.Ssl {
// if no cert and key files set in db/settings, generate new self-signed cert and key files
if settings.BTsets.SslCert == "" || settings.BTsets.SslKey == "" {
settings.BTsets.SslCert, settings.BTsets.SslKey = sslcerts.MakeCertKeyFiles(ips)
log.TLogln("Saving path to ssl cert and key in db", settings.BTsets.SslCert, settings.BTsets.SslKey)
settings.SetBTSets(settings.BTsets)
}
// verify if cert and key files are valid
err = sslcerts.VerifyCertKeyFiles(settings.BTsets.SslCert, settings.BTsets.SslKey, settings.SslPort)
// if not valid, generate new self-signed cert and key files
if err != nil {
log.TLogln("Error checking certificate and private key files:", err)
settings.BTsets.SslCert, settings.BTsets.SslKey = sslcerts.MakeCertKeyFiles(ips)
log.TLogln("Saving path to ssl cert and key in db", settings.BTsets.SslCert, settings.BTsets.SslKey)
settings.SetBTSets(settings.BTsets)
}
go func() {
log.TLogln("Start https server at", settings.IP+":"+settings.SslPort)
waitChan <- route.RunTLS(settings.IP+":"+settings.SslPort, settings.BTsets.SslCert, settings.BTsets.SslKey)
}()
}
go func() {
addr := settings.IP + ":" + settings.Port
if settings.Args != nil && settings.Args.ForceHTTPS && settings.Ssl {
waitChan <- runHTTPRedirectToHTTPS(addr)
return
}
log.TLogln("Start http server at", addr)
waitChan <- route.Run(addr)
}()
}
func Wait() error {
return <-waitChan
}
func Stop() {
dlna.Stop()
// Unmount FUSE filesystem if mounted
fuse.FuseCleanup()
BTS.Disconnect()
proxy.Stop()
waitChan <- nil
}
// echo godoc
//
// @Summary Tests server status
// @Description Tests whether server is alive or not
//
// @Tags API
//
// @Produce plain
// @Success 200 {string} string "Server version"
// @Router /echo [get]
func echo(c *gin.Context) {
c.String(200, "%v", version.Version)
}
func GetLocalIps() []string {
ifaces, err := anet.Interfaces()
if err != nil {
log.TLogln("Error get local IPs")
return nil
}
var list []string
for _, i := range ifaces {
addrs, _ := anet.InterfaceAddrsByInterface(&i)
if i.Flags&net.FlagUp == net.FlagUp {
for _, addr := range addrs {
var ip net.IP
switch v := addr.(type) {
case *net.IPNet:
ip = v.IP
case *net.IPAddr:
ip = v.IP
}
if !ip.IsLoopback() && !ip.IsLinkLocalUnicast() && !ip.IsLinkLocalMulticast() {
list = append(list, ip.String())
}
}
}
}
sort.Strings(list)
return list
}
+146
View File
@@ -0,0 +1,146 @@
package sslcerts
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"errors"
"math/big"
"net"
"os"
"path/filepath"
"time"
"server/log"
"server/settings"
)
func generateSelfSignedCert(ips []string) ([]byte, []byte, error) {
priv, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
if err != nil {
return nil, nil, err
}
notBefore := time.Now()
notAfter := notBefore.Add(365 * 24 * time.Hour) // Valid for 1 year
serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
if err != nil {
return nil, nil, err
}
netIps := make([]net.IP, 0)
if len(ips) != 0 {
for _, ip := range ips {
netIps = append(netIps, net.ParseIP(ip))
}
}
template := x509.Certificate{
SerialNumber: serialNumber,
Subject: pkix.Name{
Organization: []string{"TorrServer"},
},
NotBefore: notBefore,
NotAfter: notAfter,
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
DNSNames: []string{"localhost"},
IPAddresses: netIps,
}
certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
if err != nil {
return nil, nil, err
}
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
privBytes, err := x509.MarshalECPrivateKey(priv)
if err != nil {
return nil, nil, err
}
privPEM := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: privBytes})
return certPEM, privPEM, nil
}
func MakeCertKeyFiles(ips []string) (string, string) {
certPEM, privPEM, err := generateSelfSignedCert(ips)
if err != nil {
log.TLogln("Error generating certificate:", err)
os.Exit(1)
}
certFile, err := os.Create(filepath.Join(settings.Path, "server.pem"))
if err != nil {
log.TLogln("Error creating certificate file:", err)
os.Exit(1)
}
defer certFile.Close()
privFile, err := os.Create(filepath.Join(settings.Path, "server.key"))
if err != nil {
log.TLogln("Error creating private key file:", err)
os.Exit(1)
}
defer privFile.Close()
_, err = certFile.Write(certPEM)
if err != nil {
log.TLogln("Error writing certificate file:", err)
os.Exit(1)
}
_, err = privFile.Write(privPEM)
if err != nil {
log.TLogln("Error writing private key file:", err)
os.Exit(1)
}
log.TLogln("Self-signed certificate and private key generated successfully.")
return getAbsPath(certFile.Name()), getAbsPath(privFile.Name())
}
func getAbsPath(fileName string) string {
filePath, err := filepath.Abs(fileName)
if err != nil {
log.TLogln("Error getting absolute path:", err)
os.Exit(1)
}
return filePath
}
func VerifyCertKeyFiles(certFile, keyFile, port string) error {
// Load the certificate and key
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
if err != nil {
return err
}
// Check if the certificate chain is expired
for _, cert := range cert.Certificate {
x509Cert, err := x509.ParseCertificate(cert)
if err != nil {
return err
}
if x509Cert.NotAfter.Before(time.Now()) {
return errors.New("certificate has expired")
}
}
// Create a TLS configuration
config := tls.Config{
Certificates: []tls.Certificate{cert},
}
// Create a listener to check the certificate and key
ln, err := tls.Listen("tcp", ":"+port, &config)
if err != nil {
return err
}
defer ln.Close()
log.TLogln("Certificate and key are valid.")
return nil
}