idiotbox-go

youtube web application in Go
git clone git://git.codemadness.org/idiotbox-go
Log | Files | Refs | README | LICENSE

commit 3a8963498b96c169b2545c9d0c4fe828decec927
Author: Hiltjo Posthuma <hiltjo@codemadness.org>
Date:   Thu, 14 Sep 2017 19:30:11 +0200

initial insertion (twss)

Diffstat:
.gitignore | 1+
LICENSE | 13+++++++++++++
Makefile | 5+++++
README | 24++++++++++++++++++++++++
TODO | 5+++++
asm.s | 8++++++++
data/static/css/dark.css | 53+++++++++++++++++++++++++++++++++++++++++++++++++++++
data/static/css/light.css | 53+++++++++++++++++++++++++++++++++++++++++++++++++++++
data/static/css/pink.css | 53+++++++++++++++++++++++++++++++++++++++++++++++++++++
data/static/css/templeos.css | 58++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
data/static/favicon.png | 0
data/static/robots.txt | 2++
data/templates/pages/search.html | 95+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
data/templates/themes/default/page.html | 17+++++++++++++++++
jewtoob.go | 158+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
main.go | 282+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
openbsd_pledge.go | 39+++++++++++++++++++++++++++++++++++++++
pledge.go | 8++++++++
rc_idiotbox | 65+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
templatefuncs.go | 69+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
types.go | 59+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
21 files changed, 1067 insertions(+), 0 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -0,0 +1 @@ +idiotbox diff --git a/LICENSE b/LICENSE @@ -0,0 +1,13 @@ +Copyright (c) 2017 Hiltjo Posthuma <hiltjo@codemadness.org> + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. diff --git a/Makefile b/Makefile @@ -0,0 +1,5 @@ +build: clean + GOPATH=`pwd` go build -ldflags=-s + +clean: + rm -f jewtoob diff --git a/README b/README @@ -0,0 +1,24 @@ +idiotbox +======== + +alternate Youtube interface + +Recommended to use with youtube-dl. + +Build dependencies: +- go 1.4.2+ + + +Features: +- No Javascript required (and no SVG, websockets, localstorage, cookies, canvas, etc...). + Thus usable with Tor in "maximum security mode" aswell. +- Optional CSS. +- Usable in (old) text-based browsers. +- Link to the channel RSS/Atom feed. +- Dark and light mode stylesheet. +- Safe-search off by default. +- Detailed "likes", "dislikes" and "views". + + +Cons: +- Limited searches due to use of the Google API (estimate about 10k per day, 1mil "units"). diff --git a/TODO b/TODO @@ -0,0 +1,5 @@ +- tweak <hr/> color. +- quota limit counter. +- test links, lynx, w3m +- cycle developer keys and use tor hidden service? +? search playlist and channel support. diff --git a/asm.s b/asm.s @@ -0,0 +1,8 @@ +// +build openbsd +// +build 386 amd64 arm +// Everything below this line is ripped from the standard library source + +#include "textflag.h" + +TEXT ·use(SB),NOSPLIT,$0 + RET diff --git a/data/static/css/dark.css b/data/static/css/dark.css @@ -0,0 +1,52 @@ +body { + background-color: #000; + color: #eee; + width: 80ex; + margin: 0 auto; +} +.nowrap { + whitespace: no-wrap; +} +.a-r { + text-align: right; +} +table.videos, +table.search, +table.search .input, +input.search { + width: 100%; +} +table.search input.search { + padding: 5px; +} +table.search input.button { + padding: 5px 15px; +} +table.videos.videos { + border-collapse: collapse; +} +table.videos.videos tbody tr:hover td { + background-color: #333; +} +table.videos.videos td { + vertical-align: top; + padding: 3px 3px; +} +table.videos tbody tr td { + border-bottom: 2px solid #333; +} +td.thumb { + width: 120px; + text-align: center; +} +td.thumb img { + height: 90px; +} +a { + color: #fff; +} +hr { + color: #333; + height: 1px; + border-bottom: 1px solid #333; +}+ \ No newline at end of file diff --git a/data/static/css/light.css b/data/static/css/light.css @@ -0,0 +1,52 @@ +body { + background-color: #fff; + color: #000; + width: 80ex; + margin: 0 auto; +} +.nowrap { + whitespace: no-wrap; +} +.a-r { + text-align: right; +} +table.videos, +table.search, +table.search .input, +input.search { + width: 100%; +} +table.search input.search { + padding: 5px; +} +table.search input.button { + padding: 5px 15px; +} +table.videos.videos { + border-collapse: collapse; +} +table.videos.videos tbody tr:hover td { + background-color: #eee; +} +table.videos.videos td { + vertical-align: top; + padding: 3px 3px; +} +table.videos tbody tr td { + border-bottom: 2px solid #777; +} +td.thumb { + width: 120px; + text-align: center; +} +td.thumb img { + height: 90px; +} +a { + color: #000; +} +hr { + color: #777; + height: 1px; + border-bottom: 1px solid #777; +}+ \ No newline at end of file diff --git a/data/static/css/pink.css b/data/static/css/pink.css @@ -0,0 +1,52 @@ +body { + background-color: pink; + color: #000; + width: 80ex; + margin: 0 auto; +} +.nowrap { + whitespace: no-wrap; +} +.a-r { + text-align: right; +} +table.videos, +table.search, +table.search .input, +input.search { + width: 100%; +} +table.search input.search { + padding: 5px; +} +table.search input.button { + padding: 5px 15px; +} +table.videos.videos { + border-collapse: collapse; +} +table.videos.videos tbody tr:hover td { + background-color: #fff; +} +table.videos.videos td { + vertical-align: top; + padding: 3px 3px; +} +table.videos tbody tr td { + border-bottom: 2px solid #777; +} +td.thumb { + width: 120px; + text-align: center; +} +td.thumb img { + height: 90px; +} +a { + color: #000; +} +hr { + color: #777; + height: 1px; + border-bottom: 1px solid #777; +}+ \ No newline at end of file diff --git a/data/static/css/templeos.css b/data/static/css/templeos.css @@ -0,0 +1,57 @@ +* { + font-family: monospace; +} +body { + background-color: #55ffff; + color: #aa00aa; + width: 80ex; + margin: 0 auto; +} +.nowrap { + whitespace: no-wrap; +} +.a-r { + text-align: right; +} +table.videos, +table.search, +table.search .input, +input.search { + width: 100%; +} +table.search input.search { + padding: 5px; +} +table.search input.button { + padding: 5px 15px; +} +table.videos.videos { + border-collapse: collapse; +} +table.videos.videos tbody tr:hover td { + background-color: #fff; +} +table.videos.videos td { + vertical-align: top; + padding: 3px 3px; +} +table.videos tbody tr td { + border-bottom: 2px solid #777; +} +td.thumb { + width: 120px; + text-align: center; +} +td.thumb img { + height: 90px; +} +a { + text-decoration: none; + color: #aa0000; + border-bottom: 1px solid #0000aa; +} +hr { + color: #777; + height: 1px; + border-bottom: 1px solid #777; +}+ \ No newline at end of file diff --git a/data/static/favicon.png b/data/static/favicon.png Binary files differ. diff --git a/data/static/robots.txt b/data/static/robots.txt @@ -0,0 +1,2 @@ +User-Agent: * +Disallow: / diff --git a/data/templates/pages/search.html b/data/templates/pages/search.html @@ -0,0 +1,95 @@ +{{define "title"}}Search{{end}} +{{define "class"}}search{{end}} + +{{define "head"}} +<link rel="stylesheet" href="{{.Stylesheet}}" type="text/css" media="screen" /> +{{end}} + +{{define "content"}} + +<form method="post" action="?m={{.Mode}}"> +<table class="search" width="100%" border="0" cellpadding="0" cellspacing="0"> +<tr> + <td width="100%" class="input"> + <input type="search" name="q" value="{{.Query}}" placeholder="Search..." size="72" autofocus="autofocus" class="search" /> + </td> + <td nowrap class="nowrap"> + <input type="submit" value="Search" class="button" /> + <label>Style: </label> + {{if eq .Mode "light"}} + <a href="?m=dark&q={{.Query}}" title="Dark mode">Dark</a> + {{else}} + <a href="?m=light&q={{.Query}}" title="Light mode">Light</a> + {{end}} + </td> +</tr> +</table> +</form> + +{{if .Searchresult}} +<hr/> + +<table class="videos" width="100%" border="0" cellpadding="0" cellspacing="0"> +{{if .Searchresult.Videolist.Items}} +<tbody> + {{range .Searchresult.Videolist.Items}} + <tr> + <td class="thumb" width="120" align="center"> + {{if .Snippet.Thumbnails.Default.Url}} + <a href="https://www.youtube.com/watch?v={{.Id}}"><img src="{{.Snippet.Thumbnails.Default.Url}}" alt="" height="90" /></a> + {{end}} + </td> + <td title="{{.Snippet.Description}}"> + <span class="title"><a href="https://www.youtube.com/watch?v={{.Id}}">{{.Snippet.Title}}</a></span><br/> + <span class="channel" title="{{.Snippet.Channeltitle}} RSS/Atom feed"><a href="https://www.youtube.com/feeds/videos.xml?channel_id={{.Snippet.Channelid}}">{{.Snippet.Channeltitle}}</a></span><br/> + <span class="publishedat" title="{{.Snippet.Publishedat}}">Published: {{Friendlytime .Snippet.Publishedat}}</span><br/> + <span class="stats"> + {{if .Statistics.Viewcount}} + {{Count .Statistics.Viewcount}} views + {{end}} + {{if .Statistics.Likecount}} + {{Count .Statistics.Likecount}} likes + {{end}} + {{if .Statistics.Dislikecount}} + {{Count .Statistics.Dislikecount}} dislikes + {{end}} + </span><br/> + </td> + <td align="right" class="a-r"> + <span class="duration">{{Duration .Contentdetails.Duration}}</span> + </td> + </tr> + {{end}} +</tbody> +{{if (or .Searchresult.Searchlist.Prevpagetoken .Searchresult.Searchlist.Nextpagetoken)}} +<tfoot> +<tr> + <td align="left" class="nowrap" nowrap> + {{if .Searchresult.Searchlist.Prevpagetoken}} + <a href="?q={{.Query}}&next={{.Searchresult.Searchlist.Prevpagetoken}}&m={{.Mode}}" rel="prev nofollow">&larr; prev</a> + {{end}} + </td> + <td></td> + <td align="right" nowrap class="a-r nowrap"> + {{if .Searchresult.Searchlist.Nextpagetoken}} + <a href="?q={{.Query}}&next={{.Searchresult.Searchlist.Nextpagetoken}}&m={{.Mode}}" rel="next nofollow">next &rarr;</a> + {{end}} + </td> +</tr> +{{end}} +</tfoot> + +{{else}} + +<tbody> +<tr> + <td colspan="2"><strong>No results for "{{.Query}}"</strong></td> +</tr> +</tbody> + +{{end}} + +</table> +{{end}} + +{{end}} diff --git a/data/templates/themes/default/page.html b/data/templates/themes/default/page.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<html dir="ltr"> +<head> + <meta http-equiv="Content-Type" content="text/html; charset=UTF8" /> + <title>{{template "title" .}} - Idiotbox</title> + {{template "head" .}} + <meta name="robots" content="noindex, nofollow" /> + <meta name="robots" content="none" /> + <link rel="icon" type="image/png" href="/favicon.png" /> + <meta content="width=device-width" name="viewport" /> +</head> +<body class="{{template "class" .}}"> + +{{template "content" .}} + +</body> +</html> diff --git a/jewtoob.go b/jewtoob.go @@ -0,0 +1,158 @@ +package main + +import ( + "encoding/json" + "errors" + "io/ioutil" + "net/http" + "net/url" + "strings" + "time" +) + +// Parse ISO-8601 duration. +// NOTE: floating-point not supported, months are not supported. +func ParseISO8601Duration(str string) (time.Duration, error) { + tm := false + dur := time.Duration(0) + n := int64(0) + + tab := map[uint8]time.Duration{ + 'Y': 3600 * 24 * 7 * 365 * time.Second, + //'M': 3600 * 24 * 7 * time.Second, + 'W': 3600 * 24 * 7 * time.Second, + 'D': 3600 * 24 * time.Second, + 'H': 3600 * time.Second, + 'M': 60 * time.Second, + 'S': 1 * time.Second, + } + + for i := 0; i < len(str); i++ { + c := str[i] + switch c { + case 'P', ':', '-': // ignore + case 'T': + tm = true + n = 0 + case 'H', 'M', 'S': + if !tm && c == 'M' { + n = 0 + continue // ignore month + } + if !tm { + return dur, errors.New("time format specifier without T indicator") + } + fallthrough + case 'Y', 'W', 'D': + dur += (tab[c] * time.Duration(n)) + n = 0 + default: + if c >= '0' || c <= '9' { + if n != 0 { + n *= 10 + } + n += int64(c - '0') + + } else { + // error unknown char + } + } + } + return dur, nil +} + +type SearchResult struct { + Searchlist *SearchList + Videolist *VideoList +} + +func SearchVideos(q, next string) (*SearchResult, error) { + u := "https://www.googleapis.com/youtube/v3/search?key=" + u += url.QueryEscape(config_apikey) + u += "&maxResults=10" + // TODO: statistics,contentDetails + // TODO: optimize part / fields parameter (dont get all fields, see quota). + // TODO: type=channel,playlist,video + u += "&part=id&safeSearch=none&alt=json&type=video&&q=" + u += url.QueryEscape(q) + if len(next) > 0 { + u += "&pageToken=" + url.QueryEscape(next) + } + data, err := fetchdata(u) + if err != nil { + return nil, err + } + + searchlist := SearchList{} + err = json.Unmarshal(data, &searchlist) + if err != nil { + return nil, err + } + + videoids := make([]string, 0) + //channelids := make([]string, 0) + //playlistids := make([]string, 0) + for _, item := range searchlist.Items { + if v, ok := item.Id["videoId"]; ok { + videoids = append(videoids, v) + //} else if v, ok := item.Id["channelId"]; ok { + // channelids = append(channelids, v) + //} else if v, ok := item.Id["playlistId"]; ok { + // playlistids = append(playlistids, v) + } + } + + ids := strings.Join(videoids, ",") + + u = "https://www.googleapis.com/youtube/v3/videos?key=" + u += url.QueryEscape(config_apikey) + // TODO: optimize part / fields parameter (dont get all fields, see quota). + u += "&part=snippet,statistics,contentDetails" + u += "&id=" + url.QueryEscape(ids) + + data, err = fetchdata(u) + if err != nil { + return nil, err + } + + videolist := VideoList{} + err = json.Unmarshal(data, &videolist) + if err != nil { + return nil, err + } + + return &SearchResult{ + Searchlist: &searchlist, + Videolist: &videolist, + }, nil +} + +func fetchdata(u string) ([]byte, error) { + client := &http.Client{Timeout: 30 * time.Second} + + req, err := http.NewRequest("GET", u, nil) + if err != nil { + return nil, err + } + // NOTE: User-Agent of official API client: bump version if needed. + req.Header.Add("User-Agent", "google-api-go-client/0.5") // (gzip) + + resp, err := client.Do(req) + if resp != nil { + defer resp.Body.Close() // err != nil && resp != nil can happen on redirect failure. + } + if err != nil { + return nil, err + } + // check HTTP statuscode: don't show the exact error to the client + // (may contain sensitive information, who knows). + if resp.StatusCode != 200 { + return nil, errors.New("an error occured while communicating with the API") + } + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + return body, nil +} diff --git a/main.go b/main.go @@ -0,0 +1,282 @@ +package main + +import ( + "errors" + "flag" + "fmt" + "html/template" + "io" + "io/ioutil" + "log" + "net" + "net/http" + "os" + "path/filepath" + "strings" + "time" +) + +type Templates struct { + Pages map[string]*template.Template + Themes map[string]*template.Template +} + +var templates *Templates + +// config +var config_addr string +var config_addrtype string +var config_chmod uint +var config_apikey string +var config_datadir string +var config_templatethemedir string = "templates/themes/default/" +var config_templatepagedir string = "templates/pages/" +var config_staticcontentdir string = "static/" + +func NewTemplates() *Templates { + t := &Templates{} + t.Pages = make(map[string]*template.Template) + t.Themes = make(map[string]*template.Template) + return t +} + +// NOTE: uses "themes" and "pages" variable: map[string]*Template. +func (t *Templates) Render(w io.Writer, pagename string, themename string, data interface{}) error { + if _, ok := t.Themes[themename]; !ok { + return errors.New(fmt.Sprintf("theme template \"%s\" not found", themename)) + } + if _, ok := t.Pages[pagename]; !ok { + return errors.New(fmt.Sprintf("page template \"%s\" not found", pagename)) + } + render, err := t.Pages[pagename].Clone() + if err != nil { + return err + } + // NOTE: the template.Tree must be copied after Clone() too. + _, err = render.AddParseTree("render", t.Themes[themename].Tree.Copy()) + if err != nil { + return err + } + err = render.ExecuteTemplate(w, "render", data) + if err != nil { + return err + } + return nil +} + +func (t *Templates) LoadPages(path string) error { + templates, err := t.LoadTemplates(path) + if err == nil { + t.Pages = templates + } + return err +} + +func (t *Templates) LoadThemes(path string) error { + templates, err := t.LoadTemplates(path) + if err == nil { + t.Themes = templates + } + return err +} + +func (t *Templates) LoadTemplates(path string) (map[string]*template.Template, error) { + m := make(map[string]*template.Template) + path, err := filepath.Abs(path) + if err != nil { + return m, err + } + err = filepath.Walk(path, func(p string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() { + return nil + } + // strip prefix. + name := strings.TrimPrefix(p, path) + // replace potentially inconsistent paths (Windows). + name = strings.Replace(name, "\\", "/", -1) + name = strings.TrimPrefix(name, "/") + // read template data from file.. + data, err := ioutil.ReadFile(p) + if err != nil { + return err + } + t := template.New(name) + t = t.Funcs(map[string]interface{}{ + "Count": TF_Count, + "Duration": TF_Duration, + "Friendlytime": TF_Friendlytime, + }) + _, err = t.Parse(string(data)) + if err != nil { + return err + } + m[name] = t + return err + }) + return m, err +} + +func BadRequest(w http.ResponseWriter, err string) { + http.Error(w, "400 "+err, http.StatusBadRequest) +} + +func MakeHandler(h func(http.ResponseWriter, *http.Request) error) func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + err := r.ParseForm() + if err == nil { + err = h(w, r) + } else { + BadRequest(w, "Can't parse form: "+err.Error()) + return + } + // unhandled error so far: 500 + if err != nil { + http.Error(w, "500 "+err.Error(), http.StatusInternalServerError) + } + } +} + +func SearchHandler(w http.ResponseWriter, r *http.Request) error { + var searchresult *SearchResult + var err error + + next := r.FormValue("next") + q := r.FormValue("q") + if len(q) > 0 { + searchresult, err = SearchVideos(q, next) + if err != nil { + // NOTE: don't show error to the user (may contain sensitive data). + http.Error(w, "500 Internal Server Error", http.StatusInternalServerError) + return nil + // return err + } + } + + mode := r.FormValue("m") + switch mode { + case "dark", "light", "pink", "templeos": + default: + mode = "light" + } + stylesheet := "css/" + mode + ".css" + + return templates.Render(w, "search.html", "page.html", struct { + Next string + Mode string + Query string + Stylesheet string + Searchresult *SearchResult + }{ + Next: next, + Mode: mode, + Query: q, + Stylesheet: stylesheet, + Searchresult: searchresult, + }) +} + +func usage() { + fmt.Fprintf(os.Stderr, "Usage: %s\n", os.Args[0]) + flag.PrintDefaults() +} + +func main() { + flag.StringVar(&config_apikey, "k", "", "Google Developer API key") + flag.StringVar(&config_datadir, "d", "", "Chdir to data directory") + flag.StringVar(&config_addr, "l", "127.0.0.1:8080", "listen address") + flag.UintVar(&config_chmod, "m", 0755, "Permission for unix domain socket") + flag.StringVar(&config_addrtype, "t", "tcp4", `listen type: "tcp", "tcp4", "tcp6", "unix" or "unixpacket"`) + flag.Parse() + + if len(config_apikey) == 0 { + usage() + os.Exit(1) + } + + if config_addrtype == "unix" { + err := Pledge("stdio rpath cpath wpath dns unix fattr", nil) + if err != nil { + log.Fatalln(err) + } + } else { + err := Pledge("stdio rpath cpath wpath dns inet", nil) + if err != nil { + log.Fatalln(err) + } + } + + // Remove previous UDS if it exists. + if config_addrtype == "unix" { + os.Remove(config_addr) + } + + l, err := net.Listen(config_addrtype, config_addr) + if err != nil { + log.Fatalln(err) + } + + // Set permission on UDS. + if config_addrtype == "unix" { + err = os.Chmod(config_addr, os.FileMode(config_chmod)) + if err != nil { + log.Fatalln(err) + } + err := Pledge("stdio rpath cpath wpath dns unix", nil) + if err != nil { + log.Fatalln(err) + } + } + + if config_datadir != "" { + err = os.Chdir(config_datadir) + if err != nil { + log.Fatalln(err) + } + } + + templates = NewTemplates() + // Parse templates and keep in-memory. + err = templates.LoadPages(config_templatepagedir) + if err != nil { + log.Fatalln(err) + } + err = templates.LoadThemes(config_templatethemedir) + if err != nil { + log.Fatalln(err) + } + + http.HandleFunc("/search", MakeHandler(SearchHandler)) + + fileserv := http.FileServer(http.Dir(config_staticcontentdir)).ServeHTTP + http.HandleFunc("/", MakeHandler(func(w http.ResponseWriter, r *http.Request) error { + if r.URL.Path == "/" { + return SearchHandler(w, r) + } else { + fileserv(w, r) + return nil + } + })) + + s := &http.Server{ + ReadTimeout: 10 * time.Second, + WriteTimeout: 10 * time.Second, + MaxHeaderBytes: 1 << 20, + } + + if config_addrtype == "unix" { + err := Pledge("stdio rpath dns unix", nil) + if err != nil { + log.Fatalln(err) + } + } else { + err := Pledge("stdio rpath dns inet", nil) + if err != nil { + log.Fatalln(err) + } + } + + s.Serve(l) +} diff --git a/openbsd_pledge.go b/openbsd_pledge.go @@ -0,0 +1,39 @@ +// +build openbsd +// +build 386 amd64 arm + +package main + +import ( + "syscall" + "unsafe" +) + +const ( + SYS_PLEDGE = 108 +) + +//go:noescape +func use(p unsafe.Pointer) + +// Pledge implements its respective syscall. For more information see pledge(2). +func Pledge(promises string, paths []string) (err error) { + promisesPtr, err := syscall.BytePtrFromString(promises) + if err != nil { + return + } + promisesUnsafe, pathsUnsafe := unsafe.Pointer(promisesPtr), unsafe.Pointer(nil) + if paths != nil { + var pathsPtr []*byte + if pathsPtr, err = syscall.SlicePtrFromStrings(paths); err != nil { + return + } + pathsUnsafe = unsafe.Pointer(&pathsPtr[0]) + } + _, _, e := syscall.Syscall(SYS_PLEDGE, uintptr(promisesUnsafe), uintptr(pathsUnsafe), 0) + use(promisesUnsafe) + use(pathsUnsafe) + if e != 0 { + err = e + } + return err +} diff --git a/pledge.go b/pledge.go @@ -0,0 +1,8 @@ +// +build !openbsd + +package main + +// Pledge implements its respective syscall. For more information see pledge(2). +func Pledge(promises string, paths []string) (err error) { + return nil +} diff --git a/rc_idiotbox b/rc_idiotbox @@ -0,0 +1,65 @@ +#!/bin/sh +# This sets up a chroot for a service. +# the service is priv-dropped. +# NOTE: depending on your service some build_chroot steps can be omitted. +# +# Some tips: +# - idealy setup a separate partition for services with mount options: +# nodev,nosuid,ro options. +# - pledge(2) the service program. +# - specific pf rules for service. +# - setup resource limits for service user. + +chroot_daemon="/bin/idiotbox-go" +original_daemon="/usr/local/sbin/idiotbox-go" +chroot="/services/idiotbox" +user="_idiotbox" +group="_idiotbox" + +daemon="chroot -u ${user} -g ${group} $chroot ${chroot_daemon}" +daemon_flags="-t tcp4 -d /data -l 127.0.0.1:8081 -k youtube_api_key_here" + +. /etc/rc.d/rc.subr + +rc_reload=NO +rc_bg=YES + +pexp="${chroot_daemon} .*" + +build_chroot() { + # Locations of binaries and libraries. + mkdir -p "$chroot/etc" \ + "$chroot/bin" \ + "$chroot/dev" \ + "$chroot/usr/lib" \ + "$chroot/usr/libexec" + + # Copy original daemon. + cp "$original_daemon" "$chroot/bin" + + # Copy password and group information. + cp /etc/passwd /etc/resolv.conf "$chroot/etc" + grep "$group" "/etc/group" > "$chroot/etc/group" + + # cert bundle. + mkdir -p "$chroot/etc/ssl" + cp /etc/ssl/cert.pem "$chroot/etc/ssl" + + # copy shared core libraries. + cp /usr/lib/libpthread.so.* "$chroot/usr/lib" + cp /usr/lib/libc.so.* "$chroot/usr/lib" + cp /usr/libexec/ld.so "$chroot/usr/libexec" + + # setup /dev + # NOTE: make sure mount in $chroot does not have "nodev" set. + test -e "$chroot/dev/urandom" || mknod -m 644 "$chroot/dev/urandom" c 45 2 + test -e "$chroot/dev/null" || mknod -m 644 "$chroot/dev/null" c 2 2 +} + +rc_pre() { + build_chroot +} + +rc_cmd $1 + + diff --git a/templatefuncs.go b/templatefuncs.go @@ -0,0 +1,69 @@ +package main + +import ( + "fmt" + "strconv" + "time" +) + +func TF_Duration(s string) string { + d, err := ParseISO8601Duration(s) + if err != nil { + return "" + } + d -= (d % time.Second) // truncate to seconds + + sec := d % time.Minute + min := (d - sec) % time.Hour + hour := (d - min - sec) + + s = fmt.Sprintf("%02d:%02d", min/time.Minute, sec/time.Second) + if hour == 0 { + return s + } + if hour/time.Hour < 10 { + return fmt.Sprintf("%02d:%s", hour/time.Hour, s) + } else { + return fmt.Sprintf("%d:%s", hour/time.Hour, s) + } +} + +/* +fmt.Printf(" -1000: %s\n", TF_Count("-1000")) +fmt.Printf(" 1000: %s\n", TF_Count("1000")) +fmt.Printf(" 999: %s\n", TF_Count("999")) +fmt.Printf(" 999123: %s\n", TF_Count("999123")) +fmt.Printf(" 1999123: %s\n", TF_Count("1999123")) +fmt.Printf(" 2099023: %s\n", TF_Count("2099023")) +fmt.Printf(" 0: %s\n", TF_Count("0")) +fmt.Printf("-2099023: %s\n", TF_Count("-2099023")) +fmt.Printf(" : %s\n", TF_Count("")) +*/ +func TF_Count(s string) string { + n, err := strconv.ParseInt(s, 10, 64) + if err != nil { + return "" + } + sign := "" + if n < 0 { + sign = "-" + n = -n + } + + for s = ""; n >= 1000; { + s = fmt.Sprintf(",%03d", n%1000) + s + n -= (n % 1000) + n /= 1000 + } + return sign + fmt.Sprint(n) + s +} + +// happy happy friendly time +func TF_Friendlytime(t time.Time) string { + d := time.Now().Sub(t) + if d < time.Hour*time.Duration(24) { + d -= (d % time.Second) // truncate to seconds + return d.String() + " ago" + } + return t.Format("2006-01-02 15:04") +} diff --git a/types.go b/types.go @@ -0,0 +1,59 @@ +package main + +import ( + "time" +) + +type SearchList struct { + Kind string `json:"kind"` + Etag string `json:"etag"` + Prevpagetoken string `json:"prevPageToken"` + Nextpagetoken string `json:"nextPageToken"` + Items []struct { + Kind string `json:"kind"` + Etag string `json:"etag"` + // contains "kinds" field: + // depending on type: "playlistId" or "videoId" or "channelId". + Id map[string]string + } `json:"items"` +} + +type Thumbnail struct { + Url string `json:"url"` + Width int64 `json:"width"` + Height int64 `json:"height"` +} + +type VideoList struct { + Items []struct { + Id string `json:"id"` + Snippet struct { + Publishedat time.Time `json:"publishedAt"` + Channelid string `json:"channelId"` + Title string `json:"title"` + Description string `json:"description"` + Thumbnails struct { + Default Thumbnail `json:"default"` + Medium Thumbnail `json:"medium"` + High Thumbnail `json:"high"` + Standard Thumbnail `json:"standard"` + Maxres Thumbnail `json:"maxres"` + } + Channeltitle string `json:"channelTitle"` + } `json:"snippet"` + Contentdetails struct { + Duration string `json:"duration"` + Dimension string `json:"dimension"` + Definition string `json:"definition"` + Caption string // "true" or "false" + Licensedcontent bool `json:"licensedContent"` + Projection string `json:"projection"` + } `json:"contentDetails"` + Statistics struct { + Viewcount string `json:"viewCount"` + Likecount string `json:"likeCount"` + Dislikecount string `json:"dislikeCount"` + Favoritecount string `json:"favoriteCount"` + } `json:"statistics"` + } +}