Add non-root url path support

This commit is contained in:
hcl 2021-01-25 17:52:15 +08:00 committed by nkanaev
parent 63b265fa04
commit 4f79c919f0
6 changed files with 97 additions and 54 deletions

View File

@ -90,12 +90,12 @@
Import Import
</label> </label>
</b-dropdown-form> </b-dropdown-form>
<b-dropdown-item href="/opml/export"> <b-dropdown-item href="./opml/export">
<span class="icon mr-1">{% inline "upload.svg" %}</span> <span class="icon mr-1">{% inline "upload.svg" %}</span>
Export Export
</b-dropdown-item> </b-dropdown-item>
<b-dropdown-divider v-if="authenticated"></b-dropdown-divider> <b-dropdown-divider v-if="authenticated"></b-dropdown-divider>
<b-dropdown-item-button v-if="authenticated"> <b-dropdown-item-button v-if="authenticated" @click="logout()">
<span class="icon mr-1">{% inline "log-out.svg" %}</span> <span class="icon mr-1">{% inline "log-out.svg" %}</span>
Log out Log out
</b-dropdown-item-button> </b-dropdown-item-button>
@ -137,7 +137,7 @@
<input type="radio" name="feed" :value="'feed:'+feed.id" v-model="feedSelected"> <input type="radio" name="feed" :value="'feed:'+feed.id" v-model="feedSelected">
<div class="selectgroup-label d-flex align-items-center w-100"> <div class="selectgroup-label d-flex align-items-center w-100">
<span class="icon mr-2" v-if="!feed.has_icon">{% inline "rss.svg" %}</span> <span class="icon mr-2" v-if="!feed.has_icon">{% inline "rss.svg" %}</span>
<span class="icon mr-2" v-else><img v-lazy="'/api/feeds/'+feed.id+'/icon'" alt=""></span> <span class="icon mr-2" v-else><img v-lazy="'./api/feeds/'+feed.id+'/icon'" alt=""></span>
<span class="flex-fill text-left text-truncate">{{ feed.title }}</span> <span class="flex-fill text-left text-truncate">{{ feed.title }}</span>
<span class="counter text-right">{{ filteredFeedStats[feed.id] || '' }}</span> <span class="counter text-right">{{ filteredFeedStats[feed.id] || '' }}</span>
</div> </div>
@ -334,7 +334,7 @@
<div v-for="feed in folder.feeds" class="list-row d-flex align-items-center" :key="feed.id"> <div v-for="feed in folder.feeds" class="list-row d-flex align-items-center" :key="feed.id">
<div class="w-100 text-truncate"> <div class="w-100 text-truncate">
<span class="icon mr-2" v-if="!feed.has_icon">{% inline "rss.svg" %}</span> <span class="icon mr-2" v-if="!feed.has_icon">{% inline "rss.svg" %}</span>
<span class="icon mr-2" v-else><img v-lazy="'/api/feeds/'+feed.id+'/icon'" alt=""></span> <span class="icon mr-2" v-else><img v-lazy="'./api/feeds/'+feed.id+'/icon'" alt=""></span>
{{ feed.title }} {{ feed.title }}
</div> </div>
<span class="icon flex-shrink-0 mx-2" <span class="icon flex-shrink-0 mx-2"

View File

@ -26,74 +26,74 @@
window.api = { window.api = {
feeds: { feeds: {
list: function() { list: function() {
return api('get', '/api/feeds').then(json) return api('get', './api/feeds').then(json)
}, },
create: function(data) { create: function(data) {
return api('post', '/api/feeds', data).then(json) return api('post', './api/feeds', data).then(json)
}, },
update: function(id, data) { update: function(id, data) {
return api('put', '/api/feeds/' + id, data) return api('put', './api/feeds/' + id, data)
}, },
delete: function(id) { delete: function(id) {
return api('delete', '/api/feeds/' + id) return api('delete', './api/feeds/' + id)
}, },
list_items: function(id) { list_items: function(id) {
return api('get', '/api/feeds/' + id + '/items').then(json) return api('get', './api/feeds/' + id + '/items').then(json)
}, },
refresh: function() { refresh: function() {
return api('post', '/api/feeds/refresh') return api('post', './api/feeds/refresh')
}, },
list_errors: function() { list_errors: function() {
return api('get', '/api/feeds/errors').then(json) return api('get', './api/feeds/errors').then(json)
}, },
}, },
folders: { folders: {
list: function() { list: function() {
return api('get', '/api/folders').then(json) return api('get', './api/folders').then(json)
}, },
create: function(data) { create: function(data) {
return api('post', '/api/folders', data).then(json) return api('post', './api/folders', data).then(json)
}, },
update: function(id, data) { update: function(id, data) {
return api('put', '/api/folders/' + id, data) return api('put', './api/folders/' + id, data)
}, },
delete: function(id) { delete: function(id) {
return api('delete', '/api/folders/' + id) return api('delete', './api/folders/' + id)
}, },
list_items: function(id) { list_items: function(id) {
return api('get', '/api/folders/' + id + '/items').then(json) return api('get', './api/folders/' + id + '/items').then(json)
} }
}, },
items: { items: {
list: function(query) { list: function(query) {
return api('get', '/api/items' + param(query)).then(json) return api('get', './api/items' + param(query)).then(json)
}, },
update: function(id, data) { update: function(id, data) {
return api('put', '/api/items/' + id, data) return api('put', './api/items/' + id, data)
}, },
mark_read: function(query) { mark_read: function(query) {
return api('put', '/api/items' + param(query)) return api('put', './api/items' + param(query))
}, },
}, },
settings: { settings: {
get: function() { get: function() {
return api('get', '/api/settings').then(json) return api('get', './api/settings').then(json)
}, },
update: function(data) { update: function(data) {
return api('put', '/api/settings', data) return api('put', './api/settings', data)
}, },
}, },
status: function() { status: function() {
return api('get', '/api/status').then(json) return api('get', './api/status').then(json)
}, },
upload_opml: function(form) { upload_opml: function(form) {
return fetch('/opml/import', { return fetch('./opml/import', {
method: 'post', method: 'post',
body: new FormData(form), body: new FormData(form),
}) })
}, },
crawl: function(url) { crawl: function(url) {
return fetch('/page?url=' + url).then(function(res) { return fetch('./page?url=' + url).then(function(res) {
return res.text() return res.text()
}) })
} }

20
main.go
View File

@ -4,14 +4,15 @@ import (
"bufio" "bufio"
"flag" "flag"
"fmt" "fmt"
"github.com/nkanaev/yarr/platform"
"github.com/nkanaev/yarr/server"
"github.com/nkanaev/yarr/storage"
sdopen "github.com/skratchdot/open-golang/open"
"log" "log"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/nkanaev/yarr/platform"
"github.com/nkanaev/yarr/server"
"github.com/nkanaev/yarr/storage"
sdopen "github.com/skratchdot/open-golang/open"
) )
var Version string = "0.0" var Version string = "0.0"
@ -22,6 +23,7 @@ func main() {
var ver, open bool var ver, open bool
flag.StringVar(&addr, "addr", "127.0.0.1:7070", "address to run server on") flag.StringVar(&addr, "addr", "127.0.0.1:7070", "address to run server on")
flag.StringVar(&authfile, "auth-file", "", "path to a file containing username:password") flag.StringVar(&authfile, "auth-file", "", "path to a file containing username:password")
flag.StringVar(&server.BasePath, "base", "", "base path of the service url")
flag.StringVar(&certfile, "cert-file", "", "path to cert file for https") flag.StringVar(&certfile, "cert-file", "", "path to cert file for https")
flag.StringVar(&keyfile, "key-file", "", "path to key file for https") flag.StringVar(&keyfile, "key-file", "", "path to key file for https")
flag.StringVar(&db, "db", "", "storage file path") flag.StringVar(&db, "db", "", "storage file path")
@ -34,6 +36,16 @@ func main() {
return return
} }
if server.BasePath != "" && !strings.HasPrefix(server.BasePath, "/") {
server.BasePath = "/" + server.BasePath
}
if server.BasePath != "" && strings.HasSuffix(server.BasePath, "/") {
server.BasePath = strings.TrimSuffix(server.BasePath, "/")
}
server.BasePathReady <- true
logger := log.New(os.Stdout, "", log.Ldate|log.Ltime|log.Lshortfile) logger := log.New(os.Stdout, "", log.Ldate|log.Ltime|log.Lshortfile)
configPath, err := os.UserConfigDir() configPath, err := os.UserConfigDir()

View File

@ -11,7 +11,7 @@ import (
) )
func userIsAuthenticated(req *http.Request, username, password string) bool { func userIsAuthenticated(req *http.Request, username, password string) bool {
cookie, _ := req.Cookie("auth") cookie, _ := req.Cookie("auth")
if cookie == nil { if cookie == nil {
return false return false
} }
@ -23,11 +23,18 @@ func userIsAuthenticated(req *http.Request, username, password string) bool {
} }
func userAuthenticate(rw http.ResponseWriter, username, password string) { func userAuthenticate(rw http.ResponseWriter, username, password string) {
expires := time.Now().Add(time.Hour * 24 * 7) // 1 week expires := time.Now().Add(time.Hour * 24 * 7) // 1 week
var cookiePath string
if BasePath != "" {
cookiePath = BasePath
} else {
cookiePath = "/"
}
cookie := http.Cookie{ cookie := http.Cookie{
Name: "auth", Name: "auth",
Value: username + ":" + secret(username, password), Value: username + ":" + secret(username, password),
Expires: expires, Expires: expires,
Path: cookiePath,
} }
http.SetCookie(rw, &cookie) http.SetCookie(rw, &cookie)
} }

View File

@ -6,7 +6,6 @@ import (
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/nkanaev/yarr/storage"
"html" "html"
"html/template" "html/template"
"io" "io"
@ -19,27 +18,41 @@ import (
"reflect" "reflect"
"strconv" "strconv"
"strings" "strings"
"github.com/nkanaev/yarr/storage"
) )
var routes []Route = []Route{ var routes []Route
p("/", IndexHandler).ManualAuth(),
p("/static/*path", StaticHandler).ManualAuth(),
p("/api/status", StatusHandler), var BasePathReady = make(chan bool)
p("/api/folders", FolderListHandler),
p("/api/folders/:id", FolderHandler), func init() {
p("/api/feeds", FeedListHandler), go func() {
p("/api/feeds/find", FeedHandler), <-BasePathReady
p("/api/feeds/refresh", FeedRefreshHandler), routes = []Route{
p("/api/feeds/errors", FeedErrorsHandler), p(BasePath+"/", IndexHandler).ManualAuth(),
p("/api/feeds/:id/icon", FeedIconHandler), p(BasePath+"/static/*path", StaticHandler).ManualAuth(),
p("/api/feeds/:id", FeedHandler),
p("/api/items", ItemListHandler), p(BasePath+"/api/status", StatusHandler),
p("/api/items/:id", ItemHandler), p(BasePath+"/api/folders", FolderListHandler),
p("/api/settings", SettingsHandler), p(BasePath+"/api/folders/:id", FolderHandler),
p("/opml/import", OPMLImportHandler), p(BasePath+"/api/feeds", FeedListHandler),
p("/opml/export", OPMLExportHandler), p(BasePath+"/api/feeds/find", FeedHandler),
p("/page", PageCrawlHandler), p(BasePath+"/api/feeds/refresh", FeedRefreshHandler),
p(BasePath+"/api/feeds/errors", FeedErrorsHandler),
p(BasePath+"/api/feeds/:id/icon", FeedIconHandler),
p(BasePath+"/api/feeds/:id", FeedHandler),
p(BasePath+"/api/items", ItemListHandler),
p(BasePath+"/api/items/:id", ItemHandler),
p(BasePath+"/api/settings", SettingsHandler),
p(BasePath+"/opml/import", OPMLImportHandler),
p(BasePath+"/opml/export", OPMLExportHandler),
p(BasePath+"/page", PageCrawlHandler),
}
if BasePath != "" {
routes = append(routes, p(BasePath, RedirectBasePathHandler).ManualAuth())
}
}()
} }
type asset struct { type asset struct {
@ -140,6 +153,11 @@ func IndexHandler(rw http.ResponseWriter, req *http.Request) {
func StaticHandler(rw http.ResponseWriter, req *http.Request) { func StaticHandler(rw http.ResponseWriter, req *http.Request) {
path := Vars(req)["path"] path := Vars(req)["path"]
if BasePath != "" {
path = strings.TrimPrefix(path, BasePath)
}
ctype := mime.TypeByExtension(filepath.Ext(path)) ctype := mime.TypeByExtension(filepath.Ext(path))
if assets != nil { if assets != nil {
@ -528,3 +546,7 @@ func PageCrawlHandler(rw http.ResponseWriter, req *http.Request) {
} }
} }
} }
func RedirectBasePathHandler(rw http.ResponseWriter, req *http.Request) {
http.Redirect(rw, req, req.URL.Path+"/", http.StatusFound)
}

View File

@ -5,10 +5,12 @@ import (
"regexp" "regexp"
) )
var BasePath string = ""
type Route struct { type Route struct {
url string url string
urlRegex *regexp.Regexp urlRegex *regexp.Regexp
handler func(http.ResponseWriter, *http.Request) handler func(http.ResponseWriter, *http.Request)
manualAuth bool manualAuth bool
} }