package main import ( "bytes" "errors" "flag" "html/template" "io" "log" "net" "net/http" "net/url" "path" "sort" "strconv" "strings" "time" "github.com/kballard/go-shellquote" "contnet.org/lib/cnm-go" "contnet.org/lib/cnm-go/cnmfmt" "contnet.org/lib/cnp-go" ) var ( listen = flag.String("listen", "localhost:8080", "address for HTTP server to listen on") cnphost = flag.String("cnp-host", "", "the CNP host to proxy (disables browser mode)") nohighlight = flag.Bool("no-highlight", false, "do not include highlight.js scripts and stylesheets") nostatic = flag.Bool("no-static", false, "do not serve static files on /static") templates = template.Must(template.New("").Funcs(map[string]interface{}{ "inc": func(s string) string { return "" }, "dec": func() string { return "" }, "rst": func() string { return "" }, "depth": func() int { return 0 }, "sanchor": func() template.URL { return "" }, "lanchor": func() template.URL { return "" }, "ianchor": func() template.URL { return "" }, "anchor": func() template.URL { return "" }, "lang": func(s string) string { return strings.Map(func(r rune) rune { if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '_' || r == '-' { return r } return '_' }, s) }, "href": func(s string) template.HTMLAttr { return "href=\"" + template.HTMLAttr(template.HTMLEscapeString(s)) + "\"" }, "cnmfmt": (func(cnmfmt.Text) template.HTML)(nil), "tourl": (func(string) string)(nil), }).ParseGlob("templates/*.html")) ) const ( anchorCNPSchema = "cnp://" anchorHTTPSchema = "http://" anchorPrefix = `` ) var ( anchorTemplate = template.Must(template.New("").Parse(anchorPrefix + "{{.}}" + anchorSuffix)) ) func escapeURL(urlStr string) string { isCNP := len(urlStr) >= len(anchorCNPSchema) && strings.EqualFold(urlStr[:len(anchorCNPSchema)], anchorCNPSchema) if isCNP { urlStr = anchorHTTPSchema + urlStr[len(anchorCNPSchema):] } var buf bytes.Buffer _ = anchorTemplate.Execute(&buf, urlStr) urlStr = buf.String() urlStr = urlStr[len(anchorPrefix) : len(urlStr)-len(anchorSuffix)] if isCNP { urlStr = anchorCNPSchema + urlStr[len(anchorHTTPSchema):] } return urlStr } type server struct { host string highlighting bool } func newServer(host string, highlighting bool) *server { return &server{host, highlighting} } func (srv *server) handleError(w http.ResponseWriter, err error) { code := http.StatusInternalServerError switch err.(type) { case cnp.ErrorDenied: code = http.StatusForbidden case cnp.ErrorInvalid: code = http.StatusInternalServerError case cnp.ErrorNotFound: code = http.StatusNotFound case cnp.ErrorNotSupported: code = http.StatusInternalServerError case cnp.ErrorRejected: code = http.StatusUnprocessableEntity case cnp.ErrorServerError: code = http.StatusBadGateway case cnp.ErrorSyntax: code = http.StatusInternalServerError case cnp.ErrorTooLarge: code = http.StatusRequestEntityTooLarge case cnp.ErrorVersion: code = http.StatusInternalServerError } log.Printf("error: %T %v", err, err) http.Error(w, http.StatusText(code), code) } func (srv *server) handleIndex(w http.ResponseWriter, r *http.Request) { req, _ := cnp.NewRequest("", "/", nil) resp, _ := cnp.NewResponse(cnp.IntentOK, []byte{}) srv.cnpCNMToHTML(w, r, req, resp) } func (srv *server) handleCNP(w http.ResponseWriter, r *http.Request, path string) { host := srv.host if host == "" { ss := strings.SplitN(strings.TrimLeft(path, "/"), "/", 2) if len(ss) != 2 { if len(ss[0]) > 0 { http.Redirect(w, r, path+"/", http.StatusFound) return } srv.handleIndex(w, r) return } host = ss[0] path = "/" + ss[1] } req, err := cnp.NewRequest(host, path, nil) if err != nil { srv.handleError(w, err) return } err = r.ParseForm() if err != nil { srv.handleError(w, err) } if sel := r.FormValue("select"); sel != "" { req.SetSelect("cnm", sel) } if ims := r.Header.Get("If-Modified-Since"); ims != "" { var t time.Time t, err = http.ParseTime(ims) if err == nil && !t.IsZero() { req.SetIfModified(t) } } resp, err := cnp.Send(req) intent := "n/a" if resp != nil { intent = resp.Intent() } log.Printf("req: %q -> %q", req.Intent(), intent) if err != nil { if e, ok := err.(*net.OpError); ok && e.Op == "dial" { err = cnp.NewError(cnp.ReasonServerError) } srv.handleError(w, err) return } _, preq := r.Form["req"] _, phdr := r.Form["hdr"] _, presp := r.Form["resp"] _, praw := r.Form["raw"] if preq || phdr || presp { srv.cnpParamsToHTTP(w, resp) w.Header().Set("Content-Type", "text/plain") w.Header().Del("Content-Length") if preq { req.Write(w) } if presp { resp.Write(w) } else if phdr { resp.Header.Write(w) } } else { srv.cnpToWeb(w, r, req, resp, praw) } } func (srv *server) cnpToWeb(w http.ResponseWriter, r *http.Request, req *cnp.Request, resp *cnp.Response, raw bool) { if err := resp.Validate(); err != nil { srv.handleError(w, err) return } switch resp.ResponseIntent() { case cnp.IntentOK: srv.cnpParamsToHTTP(w, resp) if !raw && resp.Type() == "text/cnm" { srv.cnpCNMToHTML(w, r, req, resp) } else { if resp.Type() == "text/cnm" { w.Header().Set("Content-Type", "text/plain") } _, _ = io.Copy(w, resp.Body) } case cnp.IntentNotModified: w.WriteHeader(http.StatusNotModified) case cnp.IntentRedirect: host, pth, _ := resp.Location() // already validated loc, err := srv.cnpRedirectToHTTP(req, host, pth) if err != nil { srv.handleError(w, err) return } log.Println("redirecting to", loc) http.Redirect(w, r, loc, http.StatusFound) case cnp.IntentError: srv.handleError(w, cnp.NewError(resp.Reason())) default: srv.handleError(w, errors.New("unknown CNP response intent: "+resp.ResponseIntent())) } } func (srv *server) cnpRedirectToHTTP(req *cnp.Request, host, pth string) (string, error) { var loc string if host == "" || host == "." || host == srv.host { loc = pth if host == "." { loc = path.Join(path.Dir(req.Path()), loc) } if srv.host == "" { loc = path.Join("/", req.Host(), loc) } } else { rr, err := cnp.NewRequest(host, pth, nil) if err != nil { return "", err } if srv.host == "" { loc = path.Join("?", rr.Host(), rr.Path()) } else { loc = rr.URL().String() } } if strings.HasSuffix(pth, "/") { loc = loc + "/" } return loc, nil } func (srv *server) cnpParamsToHTTP(w http.ResponseWriter, resp *cnp.Response) { if l := resp.Length(); l > 0 { w.Header().Set("Content-Length", strconv.FormatInt(l, 10)) } if t := resp.Type(); t != "" { w.Header().Set("Content-Type", t) } if m := resp.Modified(); !m.IsZero() { w.Header().Set("Last-Modified", m.Format(http.TimeFormat)) } if t := resp.Time(); !t.IsZero() { w.Header().Set("Date", t.Format(http.TimeFormat)) } if n := resp.Name(); n != "" { w.Header().Set("Content-Disposition", "inline; filename=\""+strings.Map(func(r rune) rune { if r < ' ' || r == ' ' || r == '\'' { // filter out nulls, control codes, newlines and quotes return -1 } return r }, n)+"\"") } } type cnmPage struct { URL string Req string Resp string Doc *cnm.Document Site []site Netcat string Toc tocSection Highlight bool Browser bool depth int } type tocSection struct { Title string Children []tocSection } type site struct { Path string Name string Children []site } func genToc(b cnm.Block) []tocSection { var res []tocSection if v, ok := b.(*cnm.SectionBlock); ok && v != nil { if t := v.Title(); t != "" { var ch []tocSection for _, c := range v.Children() { ch = append(ch, genToc(c)...) } res = append(res, tocSection{ Title: v.Title(), Children: ch, }) } else { for _, bl := range v.Children() { res = append(res, genToc(bl)...) } } } else if v, ok := b.(cnm.ContainerBlock); ok { for _, bl := range v.Children() { res = append(res, genToc(bl)...) } } return res } func cnmSite(prefix string, s cnm.Site) (st site) { if ss := strings.Split(path.Clean(s.Path), "/"); len(ss) > 1 { s.Path = strings.Join(ss[1:], "/") return site{ Path: path.Join(prefix, ss[0]), Name: ss[0], Children: []site{ cnmSite(path.Join(prefix, ss[0]), s), }, } } st.Path = path.Join(prefix, s.Path) if s.Name == "" { st.Name = s.Path } else { st.Name = s.Name } if len(s.Children) > 0 { chs := make(map[string]site) for _, ch := range s.Children { chs[ch.Name] = cnmSite(st.Path, ch) } keys := make([]string, len(chs)) i := 0 for k := range chs { keys[i] = k i++ } sort.Slice(keys, func(i, j int) bool { a, b := chs[keys[i]], chs[keys[j]] da, db := strings.HasSuffix(a.Name, "/"), strings.HasSuffix(b.Name, "/") if da && !db { return true } if !da && db { return false } return a.Name < b.Name }) st.Children = make([]site, len(keys)) for i := range keys { st.Children[i] = chs[keys[i]] } } return } func (srv *server) cnpCNMToHTML(w http.ResponseWriter, r *http.Request, req *cnp.Request, resp *cnp.Response) { doc, err := cnm.ParseDocument(resp.Body) if err != nil { srv.handleError(w, err) return } w.Header().Set("Content-Type", "text/html") w.Header().Del("Content-Length") var breq, bresp, buf bytes.Buffer req.Header.Write(&breq) resp.Header.Write(&bresp) var st []site if len(doc.Site.Children) > 0 { st = []site{ site{"/", "/", cnmSite("/", doc.Site).Children}, } } u := req.URL() port := u.Port() if port == "" { port = strconv.Itoa(cnp.DefaultPort) } hdrs := req.Header.String() hdrs = hdrs[:len(hdrs)-1] tmpl, err := templates.Clone() if err != nil { srv.handleError(w, err) return } var toc []tocSection if doc.Content != nil { toc = genToc(doc.Content) } err = srv.addTemplateFuncs(tmpl, req, resp).ExecuteTemplate(&buf, "page.html", cnmPage{ URL: u.String(), Req: breq.String(), Resp: bresp.String(), Doc: doc, Netcat: shellquote.Join("echo", hdrs) + " | " + shellquote.Join("nc", req.Host(), port), Site: st, Toc: tocSection{Children: toc}, Highlight: srv.highlighting, Browser: srv.host == "", }) if err != nil { srv.handleError(w, err) return } _, _ = io.Copy(w, &buf) } func (srv *server) addTemplateFuncs(tmpl *template.Template, req *cnp.Request, resp *cnp.Response) *template.Template { sections := []string{} indices := []int{1} sanchors := map[string]bool{} lanchors := map[string]bool{} anchor := func() template.URL { var secs []string for _, s := range sections { if s = url.PathEscape(s); s != "" { secs = append(secs, s) } } return template.URL(strings.Join(secs, "/")) } return tmpl.Funcs(map[string]interface{}{ "inc": func(s string) string { sections = append(sections, s) indices = append(indices, 1) return "" }, "dec": func() string { sections = sections[:len(sections)-1] indices = indices[:len(indices)-1] indices[len(indices)-1]++ return "" }, "rst": func() string { sections = []string{} indices = []int{1} sanchors = map[string]bool{} lanchors = map[string]bool{} return "" }, "depth": func() int { d := len(sections) + 1 if d > 6 { return 6 } return d }, "anchor": anchor, "ianchor": func() template.URL { var buf bytes.Buffer for i, n := range indices[:len(indices)-1] { if i != 0 { buf.WriteByte('.') } buf.WriteString(strconv.Itoa(n)) } return template.URL(buf.String()) }, "sanchor": func() template.URL { s := url.PathEscape(sections[len(sections)-1]) if sanchors[s] { return "" } sanchors[s] = true return template.URL(s) }, "lanchor": func() template.URL { s := string(anchor()) if lanchors[s] { return "" } lanchors[s] = true return template.URL(s) }, "cnmfmt": func(txt cnmfmt.Text) template.HTML { return srv.fmtToHTML(req, txt) }, "tourl": func(s string) string { u, _ := srv.linkToURL(req, s) return u }, }) } func (srv *server) fmtToHTML(req *cnp.Request, text cnmfmt.Text) template.HTML { if len(text.Spans) == 0 { return "" } var last cnmfmt.Span var buf bytes.Buffer hadText := true spans := make([]cnmfmt.Span, len(text.Spans)+1) copy(spans, text.Spans) spans[len(spans)-1] = cnmfmt.Span{} // end all formats var tags []string for _, span := range spans { if last.Format.Link != span.Format.Link { if !hadText && last.Format.Link != "" { // no text in link template.HTMLEscape(&buf, []byte(last.Format.Link)) } hadText = false } srv.handleTags(req, &buf, &tags, last.Format, span.Format) if span.Text != "" { hadText = true template.HTMLEscape(&buf, []byte(span.Text)) } last = span } return template.HTML("

" + buf.String() + "

") } func (srv *server) handleTags(req *cnp.Request, buf *bytes.Buffer, tags *[]string, old, new cnmfmt.Format) { if old == new { return } close := map[string]bool{ "b": old.Emphasized && !new.Emphasized, "i": old.Alternate && !new.Alternate, "code": old.Code && !new.Code, "q": old.Quote && !new.Quote, "a": old.Link != "" && old.Link != new.Link, } open := map[string]bool{ "b": !old.Emphasized && new.Emphasized, "i": !old.Alternate && new.Alternate, "code": !old.Code && new.Code, "q": !old.Quote && new.Quote, "a": new.Link != "" && old.Link != new.Link, } t := *tags pop := len(t) for i := len(t) - 1; i >= 0; i-- { if close[t[i]] { pop = i } } for i := len(t) - 1; i >= pop; i-- { if !close[t[i]] { open[t[i]] = true } buf.WriteString("") } *tags = t[:pop] tagPush(buf, tags, open, "b", "") tagPush(buf, tags, open, "i", "") tagPush(buf, tags, open, "code", "") tagPush(buf, tags, open, "q", "") link, extern := srv.linkToURL(req, new.Link) tagPush(buf, tags, open, "a", "") } func (srv *server) linkToURL(req *cnp.Request, link string) (urlStr, extern string) { urlStr = "#ZcnpfmtZ" host := srv.host if host == "" { host = req.Host() } if u, err := url.Parse(link); err == nil { if u.Scheme == "cnp" && (srv.host == "" || u.Host == srv.host) { var lhost, lpath string lhost = u.Host lpath = u.Path if u.Host == srv.host { lhost = "" } else { extern = " class=\"cnp-external cnp-external-cnp\" data-scheme=\"cnp\"" } urlStr = path.Join("/", lhost, lpath) if strings.HasSuffix(link, "/") { urlStr += "/" } if u.Fragment != "" { urlStr += "#" + u.Fragment } } else if u.Scheme != "" { hscheme := template.HTMLEscapeString(u.Scheme) extern = " class=\"cnp-external cnp-external-" + hscheme + "\" data-scheme=\"" + hscheme + "\"" urlStr = u.String() } else { urlStr = u.Path if srv.host == "" { if !strings.HasPrefix(urlStr, "/") { urlStr = req.Path() + "/" + urlStr } urlStr = "/" + host + cnp.Clean("/"+urlStr) } if u.Fragment != "" { urlStr += "#" + u.Fragment } } } return } func tagPush(buf *bytes.Buffer, tags *[]string, ok map[string]bool, name, tag string) { if ok[name] { buf.WriteString(tag) *tags = append(*tags, name) } } func (srv *server) ServeHTTP(w http.ResponseWriter, r *http.Request) { log.Printf("access: %s %s %q", r.RemoteAddr, r.Method, r.URL) if r.Method == http.MethodPost { u := r.FormValue("url") req, err := cnp.NewRequestURL(u, nil) if err != nil { http.Error(w, err.Error(), http.StatusUnprocessableEntity) return } http.Redirect(w, r, path.Join("/", req.Host(), req.Path()), http.StatusFound) } else { srv.handleCNP(w, r, r.URL.Path) } } func main() { flag.Parse() srv := newServer(*cnphost, !*nohighlight) if !*nostatic { http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static")))) } http.Handle("/", srv) log.Printf("listening on %s", *listen) panic(http.ListenAndServe(*listen, nil)) }