mirror of
https://github.com/aykhans/slash-e.git
synced 2025-04-21 22:28:57 +00:00
chore: tweak plugin
This commit is contained in:
parent
c4b26cac38
commit
3df3405ad5
internal/mailer
plugin/httpgetter
@ -1,103 +0,0 @@
|
|||||||
package mailer
|
|
||||||
|
|
||||||
import (
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"golang.org/x/net/html"
|
|
||||||
)
|
|
||||||
|
|
||||||
var whitespaceRegex = regexp.MustCompile(`\s+`)
|
|
||||||
|
|
||||||
// Very rudimentary auto HTML to Text mail body converter.
|
|
||||||
//
|
|
||||||
// Caveats:
|
|
||||||
// - This method doesn't check for correctness of the HTML document.
|
|
||||||
// - Links will be converted to "[text](url)" format.
|
|
||||||
// - List items (<li>) are prefixed with "- ".
|
|
||||||
// - Indentation is stripped (both tabs and spaces).
|
|
||||||
// - Trailing spaces are preserved.
|
|
||||||
// - Multiple consequence newlines are collapsed as one unless multiple <br> tags are used.
|
|
||||||
func html2Text(htmlDocument string) (string, error) {
|
|
||||||
doc, err := html.Parse(strings.NewReader(htmlDocument))
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
var builder strings.Builder
|
|
||||||
var canAddNewLine bool
|
|
||||||
|
|
||||||
// see https://pkg.go.dev/golang.org/x/net/html#Parse
|
|
||||||
var f func(*html.Node, *strings.Builder)
|
|
||||||
f = func(n *html.Node, activeBuilder *strings.Builder) {
|
|
||||||
isLink := n.Type == html.ElementNode && n.Data == "a"
|
|
||||||
|
|
||||||
if isLink {
|
|
||||||
var linkBuilder strings.Builder
|
|
||||||
activeBuilder = &linkBuilder
|
|
||||||
} else if activeBuilder == nil {
|
|
||||||
activeBuilder = &builder
|
|
||||||
}
|
|
||||||
|
|
||||||
switch n.Type {
|
|
||||||
case html.TextNode:
|
|
||||||
txt := whitespaceRegex.ReplaceAllString(n.Data, " ")
|
|
||||||
|
|
||||||
// the prev node has new line so it is safe to trim the indentation
|
|
||||||
if !canAddNewLine {
|
|
||||||
txt = strings.TrimLeft(txt, " ")
|
|
||||||
}
|
|
||||||
|
|
||||||
if txt != "" {
|
|
||||||
activeBuilder.WriteString(txt)
|
|
||||||
canAddNewLine = true
|
|
||||||
}
|
|
||||||
case html.ElementNode:
|
|
||||||
if n.Data == "br" {
|
|
||||||
// always write new lines when <br> tag is used
|
|
||||||
activeBuilder.WriteString("\r\n")
|
|
||||||
canAddNewLine = false
|
|
||||||
}
|
|
||||||
// prefix list items with dash
|
|
||||||
if n.Data == "li" {
|
|
||||||
activeBuilder.WriteString("- ")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for c := n.FirstChild; c != nil; c = c.NextSibling {
|
|
||||||
if c.Type != html.ElementNode {
|
|
||||||
f(c, activeBuilder)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// format links as [label](href)
|
|
||||||
if isLink {
|
|
||||||
linkTxt := strings.TrimSpace(activeBuilder.String())
|
|
||||||
if linkTxt == "" {
|
|
||||||
linkTxt = "LINK"
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.WriteString("[")
|
|
||||||
builder.WriteString(linkTxt)
|
|
||||||
builder.WriteString("]")
|
|
||||||
|
|
||||||
// link href attr extraction
|
|
||||||
for _, a := range n.Attr {
|
|
||||||
if a.Key == "href" {
|
|
||||||
if a.Val != "" {
|
|
||||||
builder.WriteString("(")
|
|
||||||
builder.WriteString(a.Val)
|
|
||||||
builder.WriteString(")")
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
activeBuilder.Reset()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
f(doc, &builder)
|
|
||||||
|
|
||||||
return strings.TrimSpace(builder.String()), nil
|
|
||||||
}
|
|
@ -1,43 +0,0 @@
|
|||||||
package mailer
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
"net/mail"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Message defines a generic email message struct.
|
|
||||||
type Message struct {
|
|
||||||
From mail.Address `json:"from"`
|
|
||||||
To []mail.Address `json:"to"`
|
|
||||||
Bcc []mail.Address `json:"bcc"`
|
|
||||||
Cc []mail.Address `json:"cc"`
|
|
||||||
Subject string `json:"subject"`
|
|
||||||
HTML string `json:"html"`
|
|
||||||
Text string `json:"text"`
|
|
||||||
Headers map[string]string `json:"headers"`
|
|
||||||
Attachments map[string]io.Reader `json:"attachments"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mailer defines a base mail client interface.
|
|
||||||
type Mailer interface {
|
|
||||||
// Send sends an email with the provided Message.
|
|
||||||
Send(message *Message) error
|
|
||||||
}
|
|
||||||
|
|
||||||
// addressesToStrings converts the provided address to a list of serialized RFC 5322 strings.
|
|
||||||
//
|
|
||||||
// To export only the email part of mail.Address, you can set withName to false.
|
|
||||||
func addressesToStrings(addresses []mail.Address, withName bool) []string {
|
|
||||||
result := make([]string, len(addresses))
|
|
||||||
|
|
||||||
for i, addr := range addresses {
|
|
||||||
if withName && addr.Name != "" {
|
|
||||||
result[i] = addr.String()
|
|
||||||
} else {
|
|
||||||
// keep only the email part to avoid wrapping in angle-brackets
|
|
||||||
result[i] = addr.Address
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
@ -1,78 +0,0 @@
|
|||||||
package mailer
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"errors"
|
|
||||||
"mime"
|
|
||||||
"net/http"
|
|
||||||
"os/exec"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
var _ Mailer = (*Sendmail)(nil)
|
|
||||||
|
|
||||||
// Sendmail implements [mailer.Mailer] interface and defines a mail
|
|
||||||
// client that sends emails via the "sendmail" *nix command.
|
|
||||||
//
|
|
||||||
// This client is usually recommended only for development and testing.
|
|
||||||
type Sendmail struct {
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send implements `mailer.Mailer` interface.
|
|
||||||
func (c *Sendmail) Send(m *Message) error {
|
|
||||||
toAddresses := addressesToStrings(m.To, false)
|
|
||||||
|
|
||||||
headers := make(http.Header)
|
|
||||||
headers.Set("Subject", mime.QEncoding.Encode("utf-8", m.Subject))
|
|
||||||
headers.Set("From", m.From.String())
|
|
||||||
headers.Set("Content-Type", "text/html; charset=UTF-8")
|
|
||||||
headers.Set("To", strings.Join(toAddresses, ","))
|
|
||||||
|
|
||||||
cmdPath, err := findSendmailPath()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var buffer bytes.Buffer
|
|
||||||
|
|
||||||
// write
|
|
||||||
// ---
|
|
||||||
if err := headers.Write(&buffer); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if _, err := buffer.Write([]byte("\r\n")); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if m.HTML != "" {
|
|
||||||
if _, err := buffer.Write([]byte(m.HTML)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if _, err := buffer.Write([]byte(m.Text)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// ---
|
|
||||||
|
|
||||||
sendmail := exec.Command(cmdPath, strings.Join(toAddresses, ","))
|
|
||||||
sendmail.Stdin = &buffer
|
|
||||||
|
|
||||||
return sendmail.Run()
|
|
||||||
}
|
|
||||||
|
|
||||||
func findSendmailPath() (string, error) {
|
|
||||||
options := []string{
|
|
||||||
"/usr/sbin/sendmail",
|
|
||||||
"/usr/bin/sendmail",
|
|
||||||
"sendmail",
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, option := range options {
|
|
||||||
path, err := exec.LookPath(option)
|
|
||||||
if err == nil {
|
|
||||||
return path, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return "", errors.New("failed to locate a sendmail executable path")
|
|
||||||
}
|
|
@ -1,196 +0,0 @@
|
|||||||
package mailer
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net/smtp"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/domodwyer/mailyak/v3"
|
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
var _ Mailer = (*SmtpClient)(nil)
|
|
||||||
|
|
||||||
const (
|
|
||||||
SmtpAuthPlain = "PLAIN"
|
|
||||||
SmtpAuthLogin = "LOGIN"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Deprecated: Use directly the SmtpClient struct literal.
|
|
||||||
//
|
|
||||||
// NewSmtpClient creates new SmtpClient with the provided configuration.
|
|
||||||
func NewSmtpClient(
|
|
||||||
host string,
|
|
||||||
port int,
|
|
||||||
username string,
|
|
||||||
password string,
|
|
||||||
tls bool,
|
|
||||||
) *SmtpClient {
|
|
||||||
return &SmtpClient{
|
|
||||||
Host: host,
|
|
||||||
Port: port,
|
|
||||||
Username: username,
|
|
||||||
Password: password,
|
|
||||||
Tls: tls,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SmtpClient defines a SMTP mail client structure that implements
|
|
||||||
// `mailer.Mailer` interface.
|
|
||||||
type SmtpClient struct {
|
|
||||||
Host string
|
|
||||||
Port int
|
|
||||||
Username string
|
|
||||||
Password string
|
|
||||||
Tls bool
|
|
||||||
|
|
||||||
// SMTP auth method to use
|
|
||||||
// (if not explicitly set, defaults to "PLAIN")
|
|
||||||
AuthMethod string
|
|
||||||
|
|
||||||
// LocalName is optional domain name used for the EHLO/HELO exchange
|
|
||||||
// (if not explicitly set, defaults to "localhost").
|
|
||||||
//
|
|
||||||
// This is required only by some SMTP servers, such as Gmail SMTP-relay.
|
|
||||||
LocalName string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send implements `mailer.Mailer` interface.
|
|
||||||
func (c *SmtpClient) Send(m *Message) error {
|
|
||||||
var smtpAuth smtp.Auth
|
|
||||||
if c.Username != "" || c.Password != "" {
|
|
||||||
switch c.AuthMethod {
|
|
||||||
case SmtpAuthLogin:
|
|
||||||
smtpAuth = &smtpLoginAuth{c.Username, c.Password}
|
|
||||||
default:
|
|
||||||
smtpAuth = smtp.PlainAuth("", c.Username, c.Password, c.Host)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// create mail instance
|
|
||||||
var yak *mailyak.MailYak
|
|
||||||
if c.Tls {
|
|
||||||
var tlsErr error
|
|
||||||
yak, tlsErr = mailyak.NewWithTLS(fmt.Sprintf("%s:%d", c.Host, c.Port), smtpAuth, nil)
|
|
||||||
if tlsErr != nil {
|
|
||||||
return tlsErr
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
yak = mailyak.New(fmt.Sprintf("%s:%d", c.Host, c.Port), smtpAuth)
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.LocalName != "" {
|
|
||||||
yak.LocalName(c.LocalName)
|
|
||||||
}
|
|
||||||
|
|
||||||
if m.From.Name != "" {
|
|
||||||
yak.FromName(m.From.Name)
|
|
||||||
}
|
|
||||||
yak.From(m.From.Address)
|
|
||||||
yak.Subject(m.Subject)
|
|
||||||
yak.HTML().Set(m.HTML)
|
|
||||||
|
|
||||||
if m.Text == "" {
|
|
||||||
// try to generate a plain text version of the HTML
|
|
||||||
if plain, err := html2Text(m.HTML); err == nil {
|
|
||||||
yak.Plain().Set(plain)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
yak.Plain().Set(m.Text)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(m.To) > 0 {
|
|
||||||
yak.To(addressesToStrings(m.To, true)...)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(m.Bcc) > 0 {
|
|
||||||
yak.Bcc(addressesToStrings(m.Bcc, true)...)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(m.Cc) > 0 {
|
|
||||||
yak.Cc(addressesToStrings(m.Cc, true)...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// add attachements (if any)
|
|
||||||
for name, data := range m.Attachments {
|
|
||||||
yak.Attach(name, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// add custom headers (if any)
|
|
||||||
var hasMessageId bool
|
|
||||||
for k, v := range m.Headers {
|
|
||||||
if strings.EqualFold(k, "Message-ID") {
|
|
||||||
hasMessageId = true
|
|
||||||
}
|
|
||||||
yak.AddHeader(k, v)
|
|
||||||
}
|
|
||||||
if !hasMessageId {
|
|
||||||
// add a default message id if missing
|
|
||||||
fromParts := strings.Split(m.From.Address, "@")
|
|
||||||
if len(fromParts) == 2 {
|
|
||||||
yak.AddHeader("Message-ID", fmt.Sprintf("<%s@%s>",
|
|
||||||
uuid.New().String(),
|
|
||||||
fromParts[1],
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return yak.Send()
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------------------------------------------------------------------
|
|
||||||
// AUTH LOGIN
|
|
||||||
// -------------------------------------------------------------------
|
|
||||||
|
|
||||||
var _ smtp.Auth = (*smtpLoginAuth)(nil)
|
|
||||||
|
|
||||||
// smtpLoginAuth defines an AUTH that implements the LOGIN authentication mechanism.
|
|
||||||
//
|
|
||||||
// AUTH LOGIN is obsolete[1] but some mail services like outlook requires it [2].
|
|
||||||
//
|
|
||||||
// NB!
|
|
||||||
// It will only send the credentials if the connection is using TLS or is connected to localhost.
|
|
||||||
// Otherwise authentication will fail with an error, without sending the credentials.
|
|
||||||
//
|
|
||||||
// [1]: https://github.com/golang/go/issues/40817
|
|
||||||
// [2]: https://support.microsoft.com/en-us/office/outlook-com-no-longer-supports-auth-plain-authentication-07f7d5e9-1697-465f-84d2-4513d4ff0145?ui=en-us&rs=en-us&ad=us
|
|
||||||
type smtpLoginAuth struct {
|
|
||||||
username, password string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start initializes an authentication with the server.
|
|
||||||
//
|
|
||||||
// It is part of the [smtp.Auth] interface.
|
|
||||||
func (a *smtpLoginAuth) Start(server *smtp.ServerInfo) (string, []byte, error) {
|
|
||||||
// Must have TLS, or else localhost server.
|
|
||||||
// Note: If TLS is not true, then we can't trust ANYTHING in ServerInfo.
|
|
||||||
// In particular, it doesn't matter if the server advertises LOGIN auth.
|
|
||||||
// That might just be the attacker saying
|
|
||||||
// "it's ok, you can trust me with your password."
|
|
||||||
if !server.TLS && !isLocalhost(server.Name) {
|
|
||||||
return "", nil, errors.New("unencrypted connection")
|
|
||||||
}
|
|
||||||
|
|
||||||
return "LOGIN", nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Next "continues" the auth process by feeding the server with the requested data.
|
|
||||||
//
|
|
||||||
// It is part of the [smtp.Auth] interface.
|
|
||||||
func (a *smtpLoginAuth) Next(fromServer []byte, more bool) ([]byte, error) {
|
|
||||||
if more {
|
|
||||||
switch strings.ToLower(string(fromServer)) {
|
|
||||||
case "username:":
|
|
||||||
return []byte(a.username), nil
|
|
||||||
case "password:":
|
|
||||||
return []byte(a.password), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func isLocalhost(name string) bool {
|
|
||||||
return name == "localhost" || name == "127.0.0.1" || name == "::1"
|
|
||||||
}
|
|
@ -1,164 +0,0 @@
|
|||||||
package mailer
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/smtp"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestLoginAuthStart(t *testing.T) {
|
|
||||||
auth := smtpLoginAuth{username: "test", password: "123456"}
|
|
||||||
|
|
||||||
scenarios := []struct {
|
|
||||||
name string
|
|
||||||
serverInfo *smtp.ServerInfo
|
|
||||||
expectError bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
"localhost without tls",
|
|
||||||
&smtp.ServerInfo{TLS: false, Name: "localhost"},
|
|
||||||
false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"localhost with tls",
|
|
||||||
&smtp.ServerInfo{TLS: true, Name: "localhost"},
|
|
||||||
false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"127.0.0.1 without tls",
|
|
||||||
&smtp.ServerInfo{TLS: false, Name: "127.0.0.1"},
|
|
||||||
false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"127.0.0.1 with tls",
|
|
||||||
&smtp.ServerInfo{TLS: false, Name: "127.0.0.1"},
|
|
||||||
false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"::1 without tls",
|
|
||||||
&smtp.ServerInfo{TLS: false, Name: "::1"},
|
|
||||||
false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"::1 with tls",
|
|
||||||
&smtp.ServerInfo{TLS: false, Name: "::1"},
|
|
||||||
false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"non-localhost without tls",
|
|
||||||
&smtp.ServerInfo{TLS: false, Name: "example.com"},
|
|
||||||
true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"non-localhost with tls",
|
|
||||||
&smtp.ServerInfo{TLS: true, Name: "example.com"},
|
|
||||||
false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, s := range scenarios {
|
|
||||||
method, resp, err := auth.Start(s.serverInfo)
|
|
||||||
|
|
||||||
hasErr := err != nil
|
|
||||||
if hasErr != s.expectError {
|
|
||||||
t.Fatalf("[%s] Expected hasErr %v, got %v", s.name, s.expectError, hasErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
if hasErr {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(resp) != 0 {
|
|
||||||
t.Fatalf("[%s] Expected empty data response, got %v", s.name, resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
if method != "LOGIN" {
|
|
||||||
t.Fatalf("[%s] Expected LOGIN, got %v", s.name, method)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLoginAuthNext(t *testing.T) {
|
|
||||||
auth := smtpLoginAuth{username: "test", password: "123456"}
|
|
||||||
|
|
||||||
{
|
|
||||||
// example|false
|
|
||||||
r1, err := auth.Next([]byte("example:"), false)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("[example|false] Unexpected error %v", err)
|
|
||||||
}
|
|
||||||
if len(r1) != 0 {
|
|
||||||
t.Fatalf("[example|false] Expected empty part, got %v", r1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// example|true
|
|
||||||
r2, err := auth.Next([]byte("example:"), true)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("[example|true] Unexpected error %v", err)
|
|
||||||
}
|
|
||||||
if len(r2) != 0 {
|
|
||||||
t.Fatalf("[example|true] Expected empty part, got %v", r2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------
|
|
||||||
|
|
||||||
{
|
|
||||||
// username:|false
|
|
||||||
r1, err := auth.Next([]byte("username:"), false)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("[username|false] Unexpected error %v", err)
|
|
||||||
}
|
|
||||||
if len(r1) != 0 {
|
|
||||||
t.Fatalf("[username|false] Expected empty part, got %v", r1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// username:|true
|
|
||||||
r2, err := auth.Next([]byte("username:"), true)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("[username|true] Unexpected error %v", err)
|
|
||||||
}
|
|
||||||
if str := string(r2); str != auth.username {
|
|
||||||
t.Fatalf("[username|true] Expected %s, got %s", auth.username, str)
|
|
||||||
}
|
|
||||||
|
|
||||||
// uSeRnAmE:|true
|
|
||||||
r3, err := auth.Next([]byte("uSeRnAmE:"), true)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("[uSeRnAmE|true] Unexpected error %v", err)
|
|
||||||
}
|
|
||||||
if str := string(r3); str != auth.username {
|
|
||||||
t.Fatalf("[uSeRnAmE|true] Expected %s, got %s", auth.username, str)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------
|
|
||||||
|
|
||||||
{
|
|
||||||
// password:|false
|
|
||||||
r1, err := auth.Next([]byte("password:"), false)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("[password|false] Unexpected error %v", err)
|
|
||||||
}
|
|
||||||
if len(r1) != 0 {
|
|
||||||
t.Fatalf("[password|false] Expected empty part, got %v", r1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// password:|true
|
|
||||||
r2, err := auth.Next([]byte("password:"), true)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("[password|true] Unexpected error %v", err)
|
|
||||||
}
|
|
||||||
if str := string(r2); str != auth.password {
|
|
||||||
t.Fatalf("[password|true] Expected %s, got %s", auth.password, str)
|
|
||||||
}
|
|
||||||
|
|
||||||
// pAsSwOrD:|true
|
|
||||||
r3, err := auth.Next([]byte("pAsSwOrD:"), true)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("[pAsSwOrD|true] Unexpected error %v", err)
|
|
||||||
}
|
|
||||||
if str := string(r3); str != auth.password {
|
|
||||||
t.Fatalf("[pAsSwOrD|true] Expected %s, got %s", auth.password, str)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
98
plugin/httpgetter/html_meta.go
Normal file
98
plugin/httpgetter/html_meta.go
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
package httpgetter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"golang.org/x/net/html"
|
||||||
|
"golang.org/x/net/html/atom"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HTMLMeta struct {
|
||||||
|
Title string `json:"title"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Image string `json:"image"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetHTMLMeta(urlStr string) (*HTMLMeta, error) {
|
||||||
|
if _, err := url.Parse(urlStr); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := http.Get(urlStr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer response.Body.Close()
|
||||||
|
|
||||||
|
mediatype, err := getMediatype(response)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if mediatype != "text/html" {
|
||||||
|
return nil, errors.New("not a HTML page")
|
||||||
|
}
|
||||||
|
|
||||||
|
htmlMeta := extractHTMLMeta(response.Body)
|
||||||
|
return htmlMeta, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractHTMLMeta(resp io.Reader) *HTMLMeta {
|
||||||
|
tokenizer := html.NewTokenizer(resp)
|
||||||
|
htmlMeta := new(HTMLMeta)
|
||||||
|
|
||||||
|
for {
|
||||||
|
tokenType := tokenizer.Next()
|
||||||
|
if tokenType == html.ErrorToken {
|
||||||
|
break
|
||||||
|
} else if tokenType == html.StartTagToken || tokenType == html.SelfClosingTagToken {
|
||||||
|
token := tokenizer.Token()
|
||||||
|
if token.DataAtom == atom.Body {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if token.DataAtom == atom.Title {
|
||||||
|
tokenizer.Next()
|
||||||
|
token := tokenizer.Token()
|
||||||
|
htmlMeta.Title = token.Data
|
||||||
|
} else if token.DataAtom == atom.Meta {
|
||||||
|
description, ok := extractMetaProperty(token, "description")
|
||||||
|
if ok {
|
||||||
|
htmlMeta.Description = description
|
||||||
|
}
|
||||||
|
|
||||||
|
ogTitle, ok := extractMetaProperty(token, "og:title")
|
||||||
|
if ok {
|
||||||
|
htmlMeta.Title = ogTitle
|
||||||
|
}
|
||||||
|
|
||||||
|
ogDescription, ok := extractMetaProperty(token, "og:description")
|
||||||
|
if ok {
|
||||||
|
htmlMeta.Description = ogDescription
|
||||||
|
}
|
||||||
|
|
||||||
|
ogImage, ok := extractMetaProperty(token, "og:image")
|
||||||
|
if ok {
|
||||||
|
htmlMeta.Image = ogImage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return htmlMeta
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractMetaProperty(token html.Token, prop string) (content string, ok bool) {
|
||||||
|
content, ok = "", false
|
||||||
|
for _, attr := range token.Attr {
|
||||||
|
if attr.Key == "property" && attr.Val == prop {
|
||||||
|
ok = true
|
||||||
|
}
|
||||||
|
if attr.Key == "content" {
|
||||||
|
content = attr.Val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return content, ok
|
||||||
|
}
|
19
plugin/httpgetter/html_meta_test.go
Normal file
19
plugin/httpgetter/html_meta_test.go
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
package httpgetter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetHTMLMeta(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
urlStr string
|
||||||
|
htmlMeta HTMLMeta
|
||||||
|
}{}
|
||||||
|
for _, test := range tests {
|
||||||
|
metadata, err := GetHTMLMeta(test.urlStr)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, test.htmlMeta, *metadata)
|
||||||
|
}
|
||||||
|
}
|
4
plugin/httpgetter/http_getter.go
Normal file
4
plugin/httpgetter/http_getter.go
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
// Package httpgetter is using to get resources from url.
|
||||||
|
// * Get metadata for website;
|
||||||
|
// * Get image blob to avoid CORS;
|
||||||
|
package httpgetter
|
45
plugin/httpgetter/image.go
Normal file
45
plugin/httpgetter/image.go
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
package httpgetter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Image struct {
|
||||||
|
Blob []byte
|
||||||
|
Mediatype string
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetImage(urlStr string) (*Image, error) {
|
||||||
|
if _, err := url.Parse(urlStr); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := http.Get(urlStr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer response.Body.Close()
|
||||||
|
|
||||||
|
mediatype, err := getMediatype(response)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(mediatype, "image/") {
|
||||||
|
return nil, errors.New("Wrong image mediatype")
|
||||||
|
}
|
||||||
|
|
||||||
|
bodyBytes, err := io.ReadAll(response.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
image := &Image{
|
||||||
|
Blob: bodyBytes,
|
||||||
|
Mediatype: mediatype,
|
||||||
|
}
|
||||||
|
return image, nil
|
||||||
|
}
|
15
plugin/httpgetter/util.go
Normal file
15
plugin/httpgetter/util.go
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
package httpgetter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"mime"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getMediatype(response *http.Response) (string, error) {
|
||||||
|
contentType := response.Header.Get("content-type")
|
||||||
|
mediatype, _, err := mime.ParseMediaType(contentType)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return mediatype, nil
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user