Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9079be9019 | ||
|
|
79165833de | ||
|
|
8ab4064841 | ||
|
|
c44f011b15 | ||
|
|
192065d0e6 | ||
|
|
255eef9e31 |
14
README.md
14
README.md
@@ -6,9 +6,19 @@ Package gemini implements the [Gemini protocol](https://gemini.circumlunar.space
|
|||||||
|
|
||||||
It aims to provide an API similar to that of net/http to make it easy to develop Gemini clients and servers.
|
It aims to provide an API similar to that of net/http to make it easy to develop Gemini clients and servers.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
import "git.sr.ht/~adnano/go-gemini"
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
There are a few examples provided in the `examples` directory.
|
There are a few examples provided in the examples directory.
|
||||||
To run the examples:
|
To run an example:
|
||||||
|
|
||||||
go run examples/server.go
|
go run examples/server.go
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Send patches and questions to [~adnano/go-gemini-devel](https://lists.sr.ht/~adnano/go-gemini-devel).
|
||||||
|
|
||||||
|
Subscribe to release announcements on [~adnano/go-gemini-announce](https://lists.sr.ht/~adnano/go-gemini-announce).
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -76,21 +77,23 @@ func sendRequest(req *gmi.Request) error {
|
|||||||
case gmi.StatusClassInput:
|
case gmi.StatusClassInput:
|
||||||
fmt.Printf("%s: ", resp.Meta)
|
fmt.Printf("%s: ", resp.Meta)
|
||||||
scanner.Scan()
|
scanner.Scan()
|
||||||
req.URL.RawQuery = scanner.Text()
|
req.URL.RawQuery = url.QueryEscape(scanner.Text())
|
||||||
return sendRequest(req)
|
return sendRequest(req)
|
||||||
case gmi.StatusClassSuccess:
|
case gmi.StatusClassSuccess:
|
||||||
fmt.Print(string(resp.Body))
|
fmt.Print(string(resp.Body))
|
||||||
return nil
|
return nil
|
||||||
case gmi.StatusClassRedirect:
|
case gmi.StatusClassRedirect:
|
||||||
fmt.Println("Redirecting to", resp.Meta)
|
fmt.Println("Redirecting to", resp.Meta)
|
||||||
// Make the request to the same host
|
target, err := url.Parse(resp.Meta)
|
||||||
red, err := gmi.NewRequestTo(resp.Meta, req.Host)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// Handle relative redirects
|
// TODO: Prompt the user if the redirect is to another domain.
|
||||||
red.URL = req.URL.ResolveReference(red.URL)
|
redirect, err := gmi.NewRequestFromURL(req.URL.ResolveReference(target))
|
||||||
return sendRequest(red)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return sendRequest(redirect)
|
||||||
case gmi.StatusClassTemporaryFailure:
|
case gmi.StatusClassTemporaryFailure:
|
||||||
return fmt.Errorf("Temporary failure: %s", resp.Meta)
|
return fmt.Errorf("Temporary failure: %s", resp.Meta)
|
||||||
case gmi.StatusClassPermanentFailure:
|
case gmi.StatusClassPermanentFailure:
|
||||||
|
|||||||
90
examples/html.go
Normal file
90
examples/html.go
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
// +build ignore
|
||||||
|
|
||||||
|
// This example illustrates a gemtext to HTML converter.
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"html"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.sr.ht/~adnano/go-gemini"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
text := gemini.Text{
|
||||||
|
gemini.LineHeading1("Hello, world!"),
|
||||||
|
gemini.LineText("This is a gemini text document."),
|
||||||
|
}
|
||||||
|
|
||||||
|
html := textToHTML(text)
|
||||||
|
fmt.Print(html)
|
||||||
|
}
|
||||||
|
|
||||||
|
// textToHTML returns the Gemini text response as HTML.
|
||||||
|
func textToHTML(text gemini.Text) string {
|
||||||
|
var b strings.Builder
|
||||||
|
var pre bool
|
||||||
|
var list bool
|
||||||
|
for _, l := range text {
|
||||||
|
if _, ok := l.(gemini.LineListItem); ok {
|
||||||
|
if !list {
|
||||||
|
list = true
|
||||||
|
fmt.Fprint(&b, "<ul>\n")
|
||||||
|
}
|
||||||
|
} else if list {
|
||||||
|
list = false
|
||||||
|
fmt.Fprint(&b, "</ul>\n")
|
||||||
|
}
|
||||||
|
switch l.(type) {
|
||||||
|
case gemini.LineLink:
|
||||||
|
link := l.(gemini.LineLink)
|
||||||
|
url := html.EscapeString(link.URL)
|
||||||
|
name := html.EscapeString(link.Name)
|
||||||
|
if name == "" {
|
||||||
|
name = url
|
||||||
|
}
|
||||||
|
fmt.Fprintf(&b, "<p><a href='%s'>%s</a></p>\n", url, name)
|
||||||
|
case gemini.LinePreformattingToggle:
|
||||||
|
pre = !pre
|
||||||
|
if pre {
|
||||||
|
fmt.Fprint(&b, "<pre>\n")
|
||||||
|
} else {
|
||||||
|
fmt.Fprint(&b, "</pre>\n")
|
||||||
|
}
|
||||||
|
case gemini.LinePreformattedText:
|
||||||
|
text := string(l.(gemini.LinePreformattedText))
|
||||||
|
fmt.Fprintf(&b, "%s\n", html.EscapeString(text))
|
||||||
|
case gemini.LineHeading1:
|
||||||
|
text := string(l.(gemini.LineHeading1))
|
||||||
|
fmt.Fprintf(&b, "<h1>%s</h1>\n", html.EscapeString(text))
|
||||||
|
case gemini.LineHeading2:
|
||||||
|
text := string(l.(gemini.LineHeading2))
|
||||||
|
fmt.Fprintf(&b, "<h2>%s</h2>\n", html.EscapeString(text))
|
||||||
|
case gemini.LineHeading3:
|
||||||
|
text := string(l.(gemini.LineHeading3))
|
||||||
|
fmt.Fprintf(&b, "<h3>%s</h3>\n", html.EscapeString(text))
|
||||||
|
case gemini.LineListItem:
|
||||||
|
text := string(l.(gemini.LineListItem))
|
||||||
|
fmt.Fprintf(&b, "<li>%s</li>\n", html.EscapeString(text))
|
||||||
|
case gemini.LineQuote:
|
||||||
|
text := string(l.(gemini.LineQuote))
|
||||||
|
fmt.Fprintf(&b, "<blockquote>%s</blockquote>\n", html.EscapeString(text))
|
||||||
|
case gemini.LineText:
|
||||||
|
text := string(l.(gemini.LineText))
|
||||||
|
if text == "" {
|
||||||
|
fmt.Fprint(&b, "<br>\n")
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(&b, "<p>%s</p>\n", html.EscapeString(text))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if pre {
|
||||||
|
fmt.Fprint(&b, "</pre>\n")
|
||||||
|
}
|
||||||
|
if list {
|
||||||
|
fmt.Fprint(&b, "</ul>\n")
|
||||||
|
}
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
23
fs.go
23
fs.go
@@ -28,7 +28,7 @@ func (fsh fsHandler) Respond(w *ResponseWriter, r *Request) {
|
|||||||
path := path.Clean(r.URL.Path)
|
path := path.Clean(r.URL.Path)
|
||||||
f, err := fsh.Open(path)
|
f, err := fsh.Open(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
NotFound(w, r)
|
w.WriteStatus(StatusNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Detect mimetype
|
// Detect mimetype
|
||||||
@@ -58,6 +58,27 @@ type Dir string
|
|||||||
// If the file is a directory, it tries to open the index file in that directory.
|
// If the file is a directory, it tries to open the index file in that directory.
|
||||||
func (d Dir) Open(name string) (File, error) {
|
func (d Dir) Open(name string) (File, error) {
|
||||||
p := path.Join(string(d), name)
|
p := path.Join(string(d), name)
|
||||||
|
return openFile(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServeFile responds to the request with the contents of the named file
|
||||||
|
// or directory.
|
||||||
|
// TODO: Use io/fs.FS when available.
|
||||||
|
func ServeFile(w *ResponseWriter, fs FS, name string) {
|
||||||
|
f, err := fs.Open(name)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteStatus(StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Detect mimetype
|
||||||
|
ext := filepath.Ext(name)
|
||||||
|
mimetype := mime.TypeByExtension(ext)
|
||||||
|
w.SetMimetype(mimetype)
|
||||||
|
// Copy file to response writer
|
||||||
|
io.Copy(w, f)
|
||||||
|
}
|
||||||
|
|
||||||
|
func openFile(p string) (File, error) {
|
||||||
f, err := os.OpenFile(p, os.O_RDONLY, 0644)
|
f, err := os.OpenFile(p, os.O_RDONLY, 0644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
93
gemini.go
93
gemini.go
@@ -9,35 +9,80 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Status codes.
|
// Status codes.
|
||||||
|
type Status int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
StatusInput = 10
|
StatusInput Status = 10
|
||||||
StatusSensitiveInput = 11
|
StatusSensitiveInput Status = 11
|
||||||
StatusSuccess = 20
|
StatusSuccess Status = 20
|
||||||
StatusRedirect = 30
|
StatusRedirect Status = 30
|
||||||
StatusRedirectPermanent = 31
|
StatusRedirectPermanent Status = 31
|
||||||
StatusTemporaryFailure = 40
|
StatusTemporaryFailure Status = 40
|
||||||
StatusServerUnavailable = 41
|
StatusServerUnavailable Status = 41
|
||||||
StatusCGIError = 42
|
StatusCGIError Status = 42
|
||||||
StatusProxyError = 43
|
StatusProxyError Status = 43
|
||||||
StatusSlowDown = 44
|
StatusSlowDown Status = 44
|
||||||
StatusPermanentFailure = 50
|
StatusPermanentFailure Status = 50
|
||||||
StatusNotFound = 51
|
StatusNotFound Status = 51
|
||||||
StatusGone = 52
|
StatusGone Status = 52
|
||||||
StatusProxyRequestRefused = 53
|
StatusProxyRequestRefused Status = 53
|
||||||
StatusBadRequest = 59
|
StatusBadRequest Status = 59
|
||||||
StatusCertificateRequired = 60
|
StatusCertificateRequired Status = 60
|
||||||
StatusCertificateNotAuthorized = 61
|
StatusCertificateNotAuthorized Status = 61
|
||||||
StatusCertificateNotValid = 62
|
StatusCertificateNotValid Status = 62
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Class returns the status class for this status code.
|
||||||
|
func (s Status) Class() StatusClass {
|
||||||
|
return StatusClass(s / 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
// StatusMessage returns the status message corresponding to the provided
|
||||||
|
// status code.
|
||||||
|
// StatusMessage returns an empty string for input, successs, and redirect
|
||||||
|
// status codes.
|
||||||
|
func (s Status) Message() string {
|
||||||
|
switch s {
|
||||||
|
case StatusTemporaryFailure:
|
||||||
|
return "TemporaryFailure"
|
||||||
|
case StatusServerUnavailable:
|
||||||
|
return "Server unavailable"
|
||||||
|
case StatusCGIError:
|
||||||
|
return "CGI error"
|
||||||
|
case StatusProxyError:
|
||||||
|
return "Proxy error"
|
||||||
|
case StatusSlowDown:
|
||||||
|
return "Slow down"
|
||||||
|
case StatusPermanentFailure:
|
||||||
|
return "PermanentFailure"
|
||||||
|
case StatusNotFound:
|
||||||
|
return "Not found"
|
||||||
|
case StatusGone:
|
||||||
|
return "Gone"
|
||||||
|
case StatusProxyRequestRefused:
|
||||||
|
return "Proxy request refused"
|
||||||
|
case StatusBadRequest:
|
||||||
|
return "Bad request"
|
||||||
|
case StatusCertificateRequired:
|
||||||
|
return "Certificate required"
|
||||||
|
case StatusCertificateNotAuthorized:
|
||||||
|
return "Certificate not authorized"
|
||||||
|
case StatusCertificateNotValid:
|
||||||
|
return "Certificate not valid"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
// Status code categories.
|
// Status code categories.
|
||||||
|
type StatusClass int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
StatusClassInput = 1
|
StatusClassInput StatusClass = 1
|
||||||
StatusClassSuccess = 2
|
StatusClassSuccess StatusClass = 2
|
||||||
StatusClassRedirect = 3
|
StatusClassRedirect StatusClass = 3
|
||||||
StatusClassTemporaryFailure = 4
|
StatusClassTemporaryFailure StatusClass = 4
|
||||||
StatusClassPermanentFailure = 5
|
StatusClassPermanentFailure StatusClass = 5
|
||||||
StatusClassCertificateRequired = 6
|
StatusClassCertificateRequired StatusClass = 6
|
||||||
)
|
)
|
||||||
|
|
||||||
// Errors.
|
// Errors.
|
||||||
|
|||||||
13
request.go
13
request.go
@@ -42,22 +42,27 @@ func hostname(host string) string {
|
|||||||
return hostname
|
return hostname
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewRequest returns a new request. The host is inferred from the provided URL.
|
// NewRequest returns a new request. The host is inferred from the URL.
|
||||||
func NewRequest(rawurl string) (*Request, error) {
|
func NewRequest(rawurl string) (*Request, error) {
|
||||||
u, err := url.Parse(rawurl)
|
u, err := url.Parse(rawurl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
return NewRequestFromURL(u)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRequestFromURL returns a new request for the given URL.
|
||||||
|
// The host is inferred from the URL.
|
||||||
|
func NewRequestFromURL(url *url.URL) (*Request, error) {
|
||||||
// If there is no port, use the default port of 1965
|
// If there is no port, use the default port of 1965
|
||||||
host := u.Host
|
host := url.Host
|
||||||
if u.Port() == "" {
|
if url.Port() == "" {
|
||||||
host += ":1965"
|
host += ":1965"
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Request{
|
return &Request{
|
||||||
Host: host,
|
Host: host,
|
||||||
URL: u,
|
URL: url,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
// Response is a Gemini response.
|
// Response is a Gemini response.
|
||||||
type Response struct {
|
type Response struct {
|
||||||
// Status represents the response status.
|
// Status represents the response status.
|
||||||
Status int
|
Status Status
|
||||||
|
|
||||||
// Meta contains more information related to the response status.
|
// Meta contains more information related to the response status.
|
||||||
// For successful responses, Meta should contain the mimetype of the response.
|
// For successful responses, Meta should contain the mimetype of the response.
|
||||||
@@ -37,11 +37,11 @@ func (resp *Response) read(r *bufio.Reader) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
resp.Status = status
|
resp.Status = Status(status)
|
||||||
|
|
||||||
// Disregard invalid status codes
|
// Disregard invalid status codes
|
||||||
const minStatus, maxStatus = 1, 6
|
const minStatus, maxStatus = 1, 6
|
||||||
statusClass := status / 10
|
statusClass := resp.Status.Class()
|
||||||
if statusClass < minStatus || statusClass > maxStatus {
|
if statusClass < minStatus || statusClass > maxStatus {
|
||||||
return ErrInvalidResponse
|
return ErrInvalidResponse
|
||||||
}
|
}
|
||||||
@@ -74,7 +74,7 @@ func (resp *Response) read(r *bufio.Reader) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Read response body
|
// Read response body
|
||||||
if status/10 == StatusClassSuccess {
|
if resp.Status.Class() == StatusClassSuccess {
|
||||||
var err error
|
var err error
|
||||||
resp.Body, err = ioutil.ReadAll(r)
|
resp.Body, err = ioutil.ReadAll(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
54
server.go
54
server.go
@@ -168,7 +168,12 @@ func (s *Server) respond(conn net.Conn) {
|
|||||||
RemoteAddr: conn.RemoteAddr(),
|
RemoteAddr: conn.RemoteAddr(),
|
||||||
TLS: conn.(*tls.Conn).ConnectionState(),
|
TLS: conn.(*tls.Conn).ConnectionState(),
|
||||||
}
|
}
|
||||||
s.responder(req).Respond(w, req)
|
resp := s.responder(req)
|
||||||
|
if resp != nil {
|
||||||
|
resp.Respond(w, req)
|
||||||
|
} else {
|
||||||
|
w.WriteStatus(StatusNotFound)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
w.b.Flush()
|
w.b.Flush()
|
||||||
conn.Close()
|
conn.Close()
|
||||||
@@ -184,7 +189,7 @@ func (s *Server) responder(r *Request) Responder {
|
|||||||
return h
|
return h
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ResponderFunc(NotFound)
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResponseWriter is used by a Gemini handler to construct a Gemini response.
|
// ResponseWriter is used by a Gemini handler to construct a Gemini response.
|
||||||
@@ -208,22 +213,27 @@ func newResponseWriter(conn net.Conn) *ResponseWriter {
|
|||||||
// For successful responses, Meta should contain the mimetype of the response.
|
// For successful responses, Meta should contain the mimetype of the response.
|
||||||
// For failure responses, Meta should contain a short description of the failure.
|
// For failure responses, Meta should contain a short description of the failure.
|
||||||
// Meta should not be longer than 1024 bytes.
|
// Meta should not be longer than 1024 bytes.
|
||||||
func (w *ResponseWriter) WriteHeader(status int, meta string) {
|
func (w *ResponseWriter) WriteHeader(status Status, meta string) {
|
||||||
if w.wroteHeader {
|
if w.wroteHeader {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.b.WriteString(strconv.Itoa(status))
|
w.b.WriteString(strconv.Itoa(int(status)))
|
||||||
w.b.WriteByte(' ')
|
w.b.WriteByte(' ')
|
||||||
w.b.WriteString(meta)
|
w.b.WriteString(meta)
|
||||||
w.b.Write(crlf)
|
w.b.Write(crlf)
|
||||||
|
|
||||||
// Only allow body to be written on successful status codes.
|
// Only allow body to be written on successful status codes.
|
||||||
if status/10 == StatusClassSuccess {
|
if status.Class() == StatusClassSuccess {
|
||||||
w.bodyAllowed = true
|
w.bodyAllowed = true
|
||||||
}
|
}
|
||||||
w.wroteHeader = true
|
w.wroteHeader = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WriteStatus writes the response header with the given status code.
|
||||||
|
func (w *ResponseWriter) WriteStatus(status Status) {
|
||||||
|
w.WriteHeader(status, status.Message())
|
||||||
|
}
|
||||||
|
|
||||||
// SetMimetype sets the mimetype that will be written for a successful response.
|
// SetMimetype sets the mimetype that will be written for a successful response.
|
||||||
// The provided mimetype will only be used if Write is called without calling
|
// The provided mimetype will only be used if Write is called without calling
|
||||||
// WriteHeader.
|
// WriteHeader.
|
||||||
@@ -280,42 +290,20 @@ func SensitiveInput(w *ResponseWriter, r *Request, prompt string) (string, bool)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Redirect replies to the request with a redirect to the given URL.
|
// Redirect replies to the request with a redirect to the given URL.
|
||||||
func Redirect(w *ResponseWriter, r *Request, url string) {
|
func Redirect(w *ResponseWriter, url string) {
|
||||||
w.WriteHeader(StatusRedirect, url)
|
w.WriteHeader(StatusRedirect, url)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PermanentRedirect replies to the request with a permanent redirect to the given URL.
|
// PermanentRedirect replies to the request with a permanent redirect to the given URL.
|
||||||
func PermanentRedirect(w *ResponseWriter, r *Request, url string) {
|
func PermanentRedirect(w *ResponseWriter, url string) {
|
||||||
w.WriteHeader(StatusRedirectPermanent, url)
|
w.WriteHeader(StatusRedirectPermanent, url)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NotFound replies to the request with the NotFound status code.
|
|
||||||
func NotFound(w *ResponseWriter, r *Request) {
|
|
||||||
w.WriteHeader(StatusNotFound, "Not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Gone replies to the request with the Gone status code.
|
|
||||||
func Gone(w *ResponseWriter, r *Request) {
|
|
||||||
w.WriteHeader(StatusGone, "Gone")
|
|
||||||
}
|
|
||||||
|
|
||||||
// CertificateRequired responds to the request with the CertificateRequired
|
|
||||||
// status code.
|
|
||||||
func CertificateRequired(w *ResponseWriter, r *Request) {
|
|
||||||
w.WriteHeader(StatusCertificateRequired, "Certificate required")
|
|
||||||
}
|
|
||||||
|
|
||||||
// CertificateNotAuthorized responds to the request with
|
|
||||||
// the CertificateNotAuthorized status code.
|
|
||||||
func CertificateNotAuthorized(w *ResponseWriter, r *Request) {
|
|
||||||
w.WriteHeader(StatusCertificateNotAuthorized, "Certificate not authorized")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Certificate returns the request certificate. If one is not provided,
|
// Certificate returns the request certificate. If one is not provided,
|
||||||
// it returns nil and responds with StatusCertificateRequired.
|
// it returns nil and responds with StatusCertificateRequired.
|
||||||
func Certificate(w *ResponseWriter, r *Request) (*x509.Certificate, bool) {
|
func Certificate(w *ResponseWriter, r *Request) (*x509.Certificate, bool) {
|
||||||
if len(r.TLS.PeerCertificates) == 0 {
|
if len(r.TLS.PeerCertificates) == 0 {
|
||||||
CertificateRequired(w, r)
|
w.WriteStatus(StatusCertificateRequired)
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
return r.TLS.PeerCertificates[0], true
|
return r.TLS.PeerCertificates[0], true
|
||||||
@@ -458,14 +446,14 @@ func (mux *ServeMux) Respond(w *ResponseWriter, r *Request) {
|
|||||||
// If the given path is /tree and its handler is not registered,
|
// If the given path is /tree and its handler is not registered,
|
||||||
// redirect for /tree/.
|
// redirect for /tree/.
|
||||||
if u, ok := mux.redirectToPathSlash(path, r.URL); ok {
|
if u, ok := mux.redirectToPathSlash(path, r.URL); ok {
|
||||||
Redirect(w, r, u.String())
|
Redirect(w, u.String())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if path != r.URL.Path {
|
if path != r.URL.Path {
|
||||||
u := *r.URL
|
u := *r.URL
|
||||||
u.Path = path
|
u.Path = path
|
||||||
Redirect(w, r, u.String())
|
Redirect(w, u.String())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -474,7 +462,7 @@ func (mux *ServeMux) Respond(w *ResponseWriter, r *Request) {
|
|||||||
|
|
||||||
resp := mux.match(path)
|
resp := mux.match(path)
|
||||||
if resp == nil {
|
if resp == nil {
|
||||||
NotFound(w, r)
|
w.WriteStatus(StatusNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
resp.Respond(w, r)
|
resp.Respond(w, r)
|
||||||
|
|||||||
68
text.go
68
text.go
@@ -3,7 +3,6 @@ package gemini
|
|||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html"
|
|
||||||
"io"
|
"io"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
@@ -151,70 +150,3 @@ func (t Text) String() string {
|
|||||||
}
|
}
|
||||||
return b.String()
|
return b.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// HTML returns the Gemini text response as HTML.
|
|
||||||
func (t Text) HTML() string {
|
|
||||||
var b strings.Builder
|
|
||||||
var pre bool
|
|
||||||
var list bool
|
|
||||||
for _, l := range t {
|
|
||||||
if _, ok := l.(LineListItem); ok {
|
|
||||||
if !list {
|
|
||||||
list = true
|
|
||||||
fmt.Fprint(&b, "<ul>\n")
|
|
||||||
}
|
|
||||||
} else if list {
|
|
||||||
list = false
|
|
||||||
fmt.Fprint(&b, "</ul>\n")
|
|
||||||
}
|
|
||||||
switch l.(type) {
|
|
||||||
case LineLink:
|
|
||||||
link := l.(LineLink)
|
|
||||||
url := html.EscapeString(link.URL)
|
|
||||||
name := html.EscapeString(link.Name)
|
|
||||||
if name == "" {
|
|
||||||
name = url
|
|
||||||
}
|
|
||||||
fmt.Fprintf(&b, "<p><a href='%s'>%s</a></p>\n", url, name)
|
|
||||||
case LinePreformattingToggle:
|
|
||||||
pre = !pre
|
|
||||||
if pre {
|
|
||||||
fmt.Fprint(&b, "<pre>\n")
|
|
||||||
} else {
|
|
||||||
fmt.Fprint(&b, "</pre>\n")
|
|
||||||
}
|
|
||||||
case LinePreformattedText:
|
|
||||||
text := string(l.(LinePreformattedText))
|
|
||||||
fmt.Fprintf(&b, "%s\n", html.EscapeString(text))
|
|
||||||
case LineHeading1:
|
|
||||||
text := string(l.(LineHeading1))
|
|
||||||
fmt.Fprintf(&b, "<h1>%s</h1>\n", html.EscapeString(text))
|
|
||||||
case LineHeading2:
|
|
||||||
text := string(l.(LineHeading2))
|
|
||||||
fmt.Fprintf(&b, "<h2>%s</h2>\n", html.EscapeString(text))
|
|
||||||
case LineHeading3:
|
|
||||||
text := string(l.(LineHeading3))
|
|
||||||
fmt.Fprintf(&b, "<h3>%s</h3>\n", html.EscapeString(text))
|
|
||||||
case LineListItem:
|
|
||||||
text := string(l.(LineListItem))
|
|
||||||
fmt.Fprintf(&b, "<li>%s</li>\n", html.EscapeString(text))
|
|
||||||
case LineQuote:
|
|
||||||
text := string(l.(LineQuote))
|
|
||||||
fmt.Fprintf(&b, "<blockquote>%s</blockquote>\n", html.EscapeString(text))
|
|
||||||
case LineText:
|
|
||||||
text := string(l.(LineText))
|
|
||||||
if text == "" {
|
|
||||||
fmt.Fprint(&b, "<br>\n")
|
|
||||||
} else {
|
|
||||||
fmt.Fprintf(&b, "<p>%s</p>\n", html.EscapeString(text))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if pre {
|
|
||||||
fmt.Fprint(&b, "</pre>\n")
|
|
||||||
}
|
|
||||||
if list {
|
|
||||||
fmt.Fprint(&b, "</ul>\n")
|
|
||||||
}
|
|
||||||
return b.String()
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user