aboutsummaryrefslogblamecommitdiffstats
path: root/web/web.go
blob: 2151926ec78b8346900eb4c27b112991f172e12e (plain) (tree)
1
2
3
4
5
6
7
8


           
                                    
                                     

                                         
                         


                                    
                   
             
                  
              
                 
              


                                                           
                                                       
















                                                               




                                             




                                             



                                         
 
                              
                                                                                                 









                                                          










                                                         



























                                                          




                                          












                                                                        















                                                              
                                                                
                                
                                                                  
 











                                                                

                       













                                                                                      
                       
                                                          


                      




                                                                
 


                                                                                    



                    
























                                                              
                       
                                       



                                                           
                                                                  
                    
                                                   
                                                             

                                                                                                                                
                                             
                                                     
                        
                                                       
                 





                                                            
                                                                                 
                             
                                            


                                          
                                   


                            
                                                                                                   

                           
         
                    

 
                                                          
                                                             
                                     
                                     
                        
                                                           




                 
                                                                

                                                                 
                                                            



                                                                                
                                                        
                                                           
                                                                                        





                                                    
                                                         
 





                                                                        
package web

import (
	"adammathes.com/neko/config"
	"adammathes.com/neko/crawler"
	"adammathes.com/neko/models/feed"
	"adammathes.com/neko/models/item"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"golang.org/x/crypto/bcrypt"
	"io/ioutil"
	"log"
	"net/http"
	"path"
	"strconv"
	"time"
)

func indexHandler(w http.ResponseWriter, r *http.Request) {
	http.ServeFile(w, r, staticFilePath("ui.html"))
}

func streamHandler(w http.ResponseWriter, r *http.Request) {

	max_id := 0
	if r.FormValue("max_id") != "" {
		max_id, _ = strconv.Atoi(r.FormValue("max_id"))
	}

	feed_id := int64(0)
	if r.FormValue("feed_url") != "" {
		feed_url := r.FormValue("feed_url")
		var f feed.Feed
		f.ByUrl(feed_url)
		feed_id = f.Id
	}

	category := ""
	if r.FormValue("tag") != "" {
		category = r.FormValue("tag")
	}

	unread_only := true
	if r.FormValue("read_filter") != "" {
		unread_only = false
	}

	starred_only := false
	if r.FormValue("starred") != "" {
		starred_only = true
	}

	var items []*item.Item
	items, err := item.Filter(int64(max_id), feed_id, category, unread_only, starred_only, 0)
	if err != nil {
		log.Println(err)
	}

	w.Header().Set("Content-Type", "application/json")
	js, _ := json.Marshal(items)
	w.Write(js)
}

func itemHandler(w http.ResponseWriter, r *http.Request) {
	switch r.Method {
	case "PUT":
		var i item.Item
		err := json.NewDecoder(r.Body).Decode(&i)
		if err != nil {
			log.Println(err)
		} else {
			i.Save()
		}
	case "GET":
		fullTextHandler(w, r)
	}
	defer r.Body.Close()
}

func feedHandler(w http.ResponseWriter, r *http.Request) {
	if r.Method == "GET" {
		feeds, err := feed.All()
		if err != nil {
			log.Println(err)
		}

		js, err := json.Marshal(feeds)
		if err != nil {
			log.Println(err)
		}
		w.Write(js)
		return
	}

	var f feed.Feed
	err := json.NewDecoder(r.Body).Decode(&f)
	if err != nil {
		log.Println(err)
	}

	switch r.Method {
	case "POST":
		feed.NewFeed(f.Url)
		f.ByUrl(f.Url)
		ch := make(chan string)
		// log.Println("crawling")
		crawler.CrawlFeed(&f, ch)
		log.Println(<-ch)
	case "PUT":
		f.Update()
	case "DELETE":
		feed_id, err := strconv.Atoi(r.URL.Path[len("/feed/"):])
		if err != nil {
			log.Println(err)
			return
		}
		f.Id = int64(feed_id)
		f.Delete()
	}
}

func categoryHandler(w http.ResponseWriter, r *http.Request) {
	if r.Method == "GET" {
		categories, err := feed.Categories()
		if err != nil {
			log.Println(err)
		}

		js, err := json.Marshal(categories)
		if err != nil {
			log.Println(err)
		}
		w.Write(js)
		return
	}
}

func imageProxyHandler(w http.ResponseWriter, r *http.Request) {
	imgURL := r.URL.String()
	decodedURL, err := base64.URLEncoding.DecodeString(imgURL)

	// pseudo-caching
	if r.Header.Get("If-None-Match") == string(decodedURL) {
		w.WriteHeader(http.StatusNotModified)
		return
	}

	if r.Header.Get("Etag") == string(decodedURL) {
		w.WriteHeader(http.StatusNotModified)
		return
	}

	// set headers

	// grab the img
	c := &http.Client{
		// give up after 5 seconds
		Timeout: 5 * time.Second,
	}

	request, err := http.NewRequest("GET", string(decodedURL), nil)
	if err != nil {
		log.Fatalln(err)
	}

	userAgent := "neko RSS Reader Image Proxy +https://github.com/adammathes/neko"
	request.Header.Set("User-Agent", userAgent)
	resp, err := c.Do(request)

	if err != nil {
		http.Error(w, "filed to proxy image", 404)
		return
	}

	bts, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		http.Error(w, "failed to read proxy image", 404)
		return
	}

	w.Header().Set("ETag", string(decodedURL))
	w.Header().Set("Cache-Control", "public")
	w.Header().Set("Expires", time.Now().Add(48*time.Hour).Format(time.RFC1123))
	w.Write(bts)
	return
}

func fullTextHandler(w http.ResponseWriter, r *http.Request) {
	fmt.Printf("request: %v\n\n", r)

	fmt.Printf("url string: %s\n\n", r.URL.String())

	itemID, _ := strconv.Atoi(r.URL.String())
	//	fmt.Printf("item id: %v\n\n", itemID)

	if itemID == 0 {
		fmt.Printf("wah wah wah\n")
		return
	}

	i := item.ItemById(int64(itemID))
	// fmt.Println("item fetched: %v\n\n", i)

	if i.FullContent == "" {
		i.GetFullContent()
	}

	w.Header().Set("Content-Type", "application/json")
	js, _ := json.Marshal(i)
	w.Write(js)
}

var AuthCookie = "auth"
var SecondsInAYear = 60 * 60 * 24 * 365

func loginHandler(w http.ResponseWriter, r *http.Request) {
	switch r.Method {
	case "GET":
		http.ServeFile(w, r, staticFilePath("login.html"))
	case "POST":
		password := r.FormValue("password")
		if password == config.Config.DigestPassword {
			v, _ := bcrypt.GenerateFromPassword([]byte(password), 0)
			c := http.Cookie{Name: AuthCookie, Value: string(v), Path: "/", MaxAge: SecondsInAYear, HttpOnly: false}
			http.SetCookie(w, &c)
			http.Redirect(w, r, "/", 307)
		} else {
			http.Error(w, "bad login", 401)
		}
	default:
		http.Error(w, "nope", 500)
	}
}

func logoutHandler(w http.ResponseWriter, r *http.Request) {
	c := http.Cookie{Name: AuthCookie, MaxAge: 0, Path: "/", HttpOnly: false}
	http.SetCookie(w, &c)
	fmt.Fprintf(w, "you are logged out")
}

func Authenticated(r *http.Request) bool {
	pc, err := r.Cookie("auth")
	if err != nil {
		return false
	}
	err = bcrypt.CompareHashAndPassword([]byte(pc.Value), []byte(config.Config.DigestPassword))
	if err == nil {
		return true
	}
	return false
}

func AuthWrap(wrapped http.HandlerFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		if Authenticated(r) {
			wrapped(w, r)
		} else {
			http.Redirect(w, r, "/login/", 307)
		}
	}
}

func Serve() {
	fs := http.FileServer(http.Dir(config.Config.StaticDir))
	http.Handle("/static/", http.StripPrefix("/static/", fs))

	http.HandleFunc("/stream/", AuthWrap(streamHandler))

	//	http.HandleFunc("/item/", AuthWrap(itemHandler))
	http.Handle("/item/", http.StripPrefix("/item/", AuthWrap(itemHandler)))

	http.HandleFunc("/feed/", AuthWrap(feedHandler))
	http.HandleFunc("/tag/", AuthWrap(categoryHandler))
	http.Handle("/image/", http.StripPrefix("/image/", AuthWrap(imageProxyHandler)))

	http.HandleFunc("/login/", loginHandler)
	http.HandleFunc("/logout/", logoutHandler)

	http.HandleFunc("/", AuthWrap(indexHandler))

	http.ListenAndServe(config.Config.WebServer, nil)
}

// given a path, prepend config.Config.StaticDir
// TODO: compile these into the binary to remove dependency on the files
func staticFilePath(p string) string {
	return path.Join(config.Config.StaticDir, p)
}