186 lines
4.5 KiB
Go
186 lines
4.5 KiB
Go
package webserver
|
|
|
|
import (
|
|
"crypto/tls"
|
|
// "encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"path"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/go-playground/form/v4"
|
|
|
|
"gitlab.com/offline-twitter/twitter_offline_engine/pkg/persistence"
|
|
"gitlab.com/offline-twitter/twitter_offline_engine/pkg/scraper"
|
|
)
|
|
|
|
type Middleware func(http.Handler) http.Handler
|
|
|
|
type Application struct {
|
|
accessLog *log.Logger
|
|
traceLog *log.Logger
|
|
InfoLog *log.Logger
|
|
ErrorLog *log.Logger
|
|
|
|
Middlewares []Middleware
|
|
|
|
Profile persistence.Profile
|
|
ActiveUser scraper.User
|
|
DisableScraping bool
|
|
}
|
|
|
|
func NewApp(profile persistence.Profile) Application {
|
|
ret := Application{
|
|
accessLog: log.New(os.Stdout, "ACCESS\t", log.Ldate|log.Ltime),
|
|
traceLog: log.New(os.Stdout, "TRACE\t", log.Ldate|log.Ltime),
|
|
InfoLog: log.New(os.Stdout, "INFO\t", log.Ldate|log.Ltime),
|
|
ErrorLog: log.New(os.Stderr, "ERROR\t", log.Ldate|log.Ltime|log.Lshortfile),
|
|
|
|
Profile: profile,
|
|
ActiveUser: get_default_user(),
|
|
}
|
|
ret.Middlewares = []Middleware{
|
|
secureHeaders,
|
|
ret.logRequest,
|
|
ret.recoverPanic,
|
|
}
|
|
return ret
|
|
}
|
|
|
|
func (app *Application) WithMiddlewares() http.Handler {
|
|
var ret http.Handler = app
|
|
for i := range app.Middlewares {
|
|
ret = app.Middlewares[i](ret)
|
|
}
|
|
return ret
|
|
}
|
|
|
|
func (app *Application) SetActiveUser(handle scraper.UserHandle) error {
|
|
if handle == "no account" {
|
|
scraper.InitApi(scraper.NewGuestSession())
|
|
app.ActiveUser = get_default_user()
|
|
app.DisableScraping = true // API requests will fail b/c not logged in
|
|
} else {
|
|
user, err := app.Profile.GetUserByHandle(handle)
|
|
if err != nil {
|
|
return fmt.Errorf("set active user to %q: %w", handle, err)
|
|
}
|
|
scraper.InitApi(app.Profile.LoadSession(handle))
|
|
app.ActiveUser = user
|
|
app.DisableScraping = false
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func get_default_user() scraper.User {
|
|
return scraper.User{
|
|
ID: 0,
|
|
Handle: "[nobody]",
|
|
DisplayName: "[Not logged in]",
|
|
ProfileImageLocalPath: path.Base(scraper.DEFAULT_PROFILE_IMAGE_URL),
|
|
IsContentDownloaded: true,
|
|
}
|
|
}
|
|
|
|
// Manual router implementation.
|
|
// I don't like the weird matching behavior of http.ServeMux, and it's not hard to write by hand.
|
|
func (app *Application) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path == "/" {
|
|
http.Redirect(w, r, "/timeline", 303)
|
|
return
|
|
}
|
|
|
|
parts := strings.Split(r.URL.Path, "/")[1:]
|
|
switch parts[0] {
|
|
case "static":
|
|
if use_embedded == "true" {
|
|
// Serve directly from the embedded files
|
|
http.FileServer(http.FS(embedded_files)).ServeHTTP(w, r)
|
|
} else {
|
|
http.StripPrefix("/static", http.FileServer(http.Dir(get_filepath("static")))).ServeHTTP(w, r)
|
|
}
|
|
case "tweet":
|
|
app.TweetDetail(w, r)
|
|
case "content":
|
|
http.StripPrefix("/content", http.FileServer(http.Dir(app.Profile.ProfileDir))).ServeHTTP(w, r)
|
|
case "login":
|
|
app.Login(w, r)
|
|
case "change-session":
|
|
app.ChangeSession(w, r)
|
|
case "timeline":
|
|
app.Timeline(w, r)
|
|
case "follow":
|
|
app.UserFollow(w, r)
|
|
case "unfollow":
|
|
app.UserUnfollow(w, r)
|
|
case "search":
|
|
http.StripPrefix("/search", http.HandlerFunc(app.Search)).ServeHTTP(w, r)
|
|
default:
|
|
app.UserFeed(w, r)
|
|
}
|
|
}
|
|
|
|
func (app *Application) Run(address string) {
|
|
srv := &http.Server{
|
|
Addr: address,
|
|
ErrorLog: app.ErrorLog,
|
|
Handler: app.WithMiddlewares(),
|
|
TLSConfig: &tls.Config{
|
|
CurvePreferences: []tls.CurveID{tls.X25519, tls.CurveP256},
|
|
},
|
|
IdleTimeout: time.Minute,
|
|
ReadTimeout: 5 * time.Second,
|
|
WriteTimeout: 10 * time.Second,
|
|
}
|
|
|
|
app.InfoLog.Printf("Starting server on %s", address)
|
|
err := srv.ListenAndServe()
|
|
app.ErrorLog.Fatal(err)
|
|
}
|
|
|
|
// func to_json(t interface{}) string {
|
|
// js, err := json.Marshal(t)
|
|
// if err != nil {
|
|
// panic(err)
|
|
// }
|
|
// return string(js)
|
|
// }
|
|
|
|
func parse_cursor_value(c *persistence.Cursor, r *http.Request) error {
|
|
cursor_param := r.URL.Query().Get("cursor")
|
|
if cursor_param != "" {
|
|
var err error
|
|
c.CursorValue, err = strconv.Atoi(cursor_param)
|
|
if err != nil {
|
|
return fmt.Errorf("attempted to parse cursor value %q as int: %w", c.CursorValue, err)
|
|
}
|
|
c.CursorPosition = persistence.CURSOR_MIDDLE
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type FormErrors map[string]string
|
|
|
|
var formDecoder = form.NewDecoder()
|
|
var (
|
|
ErrCorruptedFormData = errors.New("corrupted form data")
|
|
ErrIncorrectFormParams = errors.New("incorrect form parameters")
|
|
)
|
|
|
|
func parse_form(req *http.Request, result interface{}) error {
|
|
err := req.ParseForm()
|
|
if err != nil {
|
|
return ErrCorruptedFormData
|
|
}
|
|
|
|
if err = formDecoder.Decode(result, req.PostForm); err != nil {
|
|
return ErrIncorrectFormParams
|
|
}
|
|
return nil
|
|
}
|