first commit
This commit is contained in:
parent
1b21b73afe
commit
6e2dc64924
|
@ -0,0 +1,4 @@
|
||||||
|
bin/server: cmd/server/main.go pkg/*/*.go go.mod go.sum
|
||||||
|
go build -o bin/server cmd/server/main.go
|
||||||
|
|
||||||
|
build: bin/server
|
Binary file not shown.
|
@ -0,0 +1,35 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"netoik.io/netoik-website/pkg/captcha"
|
||||||
|
"netoik.io/netoik-website/pkg/conf"
|
||||||
|
"netoik.io/netoik-website/pkg/contact"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Parse command line arguments
|
||||||
|
path := flag.String("c", "server.conf", "Config file")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
// Parse config file
|
||||||
|
if !conf.ParseFile(*path) {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup captcha
|
||||||
|
captcha.Setup()
|
||||||
|
|
||||||
|
// Declare api routes
|
||||||
|
http.HandleFunc("/api/contact/send", contact.HandleSend)
|
||||||
|
http.HandleFunc("/api/captcha/new", captcha.HandleNew)
|
||||||
|
|
||||||
|
// Start listening
|
||||||
|
if err := http.ListenAndServe(fmt.Sprintf("%s:%d", conf.Conf.BindHost, conf.Conf.BindPort), nil); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "cannot listen at %s:%d: %s\n", conf.Conf.BindHost, conf.Conf.BindPort, err.Error())
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
module netoik.io/netoik-website
|
||||||
|
|
||||||
|
go 1.18
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f // indirect
|
||||||
|
github.com/pelletier/go-toml v1.9.5 // indirect
|
||||||
|
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
||||||
|
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df // indirect
|
||||||
|
)
|
|
@ -0,0 +1,8 @@
|
||||||
|
github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f h1:q/DpyjJjZs94bziQ7YkBmIlpqbVP7yw179rnzoNVX1M=
|
||||||
|
github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f/go.mod h1:QGrK8vMWWHQYQ3QU9bw9Y9OPNfxccGzfb41qjvVeXtY=
|
||||||
|
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
|
||||||
|
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
||||||
|
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
|
||||||
|
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
|
||||||
|
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE=
|
||||||
|
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=
|
|
@ -0,0 +1,18 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Answer struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Id string `json:"id,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func Reply(w http.ResponseWriter, code int, answer Answer) {
|
||||||
|
a, _ := json.Marshal(answer)
|
||||||
|
w.Header().Set(http.CanonicalHeaderKey("content-type"), "application/json")
|
||||||
|
w.WriteHeader(code)
|
||||||
|
w.Write(a)
|
||||||
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
package captcha
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/dchest/captcha"
|
||||||
|
"net/http"
|
||||||
|
"netoik.io/netoik-website/pkg/api"
|
||||||
|
"netoik.io/netoik-website/pkg/conf"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func writeImage(id string) bool {
|
||||||
|
path := filepath.Join(conf.Conf.CaptchaDirectory, id+".png")
|
||||||
|
file, err := os.Create(path)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "ERROR: cannot write file %q: %s\n", path, err.Error())
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if err = captcha.WriteImage(file, id, conf.Conf.CaptchaWidth, conf.Conf.CaptchaHeight); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "ERROR: cannot write captcha into file %q: %s\n", path, err.Error())
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func HandleNew(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Check method
|
||||||
|
if r.Method != "POST" {
|
||||||
|
api.Reply(w, 405, api.Answer{})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new captcha
|
||||||
|
id := captcha.NewLen(conf.Conf.CaptchaLength)
|
||||||
|
|
||||||
|
// Write captcha image
|
||||||
|
if !writeImage(id) {
|
||||||
|
api.Reply(w, 500, api.Answer{})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove captcha image after expiration time
|
||||||
|
go func(id string) {
|
||||||
|
time.Sleep(conf.Conf.CaptchaExpiration)
|
||||||
|
path := filepath.Join(conf.Conf.CaptchaDirectory, id+".png")
|
||||||
|
if err := os.Remove(path); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||||
|
fmt.Fprintf(os.Stderr, "ERROR: cannot remove captcha image after expiration %q: %s", path, err.Error())
|
||||||
|
}
|
||||||
|
}(id)
|
||||||
|
|
||||||
|
// Return captcha id
|
||||||
|
api.Reply(w, 200, api.Answer{Success: true, Id: id})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Setup() {
|
||||||
|
captcha.SetCustomStore(captcha.NewMemoryStore(captcha.CollectNum, conf.Conf.CaptchaExpiration))
|
||||||
|
}
|
|
@ -0,0 +1,108 @@
|
||||||
|
package conf
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/pelletier/go-toml"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type conf struct {
|
||||||
|
BindHost string `toml:"bind_host"`
|
||||||
|
BindPort int `toml:"bind_port"`
|
||||||
|
|
||||||
|
SMTPHost string `toml:"smtp_host"`
|
||||||
|
SMTPPort int `toml:"smtp_port"`
|
||||||
|
SMTPUsername string `toml:"smtp_username"`
|
||||||
|
SMTPPassword string `toml:"smtp_password"`
|
||||||
|
SMTPReceiver string `toml:"smtp_receiver"`
|
||||||
|
|
||||||
|
CaptchaDirectory string `toml:"captcha_directory"`
|
||||||
|
CaptchaLength int `toml:"captcha_length"`
|
||||||
|
CaptchaWidth int `toml:"captcha_width"`
|
||||||
|
CaptchaHeight int `toml:"captcha_height"`
|
||||||
|
CaptchaExpiration time.Duration `toml:"captcha_expiration"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var Conf = conf{
|
||||||
|
BindHost: "127.0.0.1",
|
||||||
|
BindPort: 8000,
|
||||||
|
CaptchaLength: 6,
|
||||||
|
CaptchaWidth: 240,
|
||||||
|
CaptchaHeight: 80,
|
||||||
|
CaptchaExpiration: time.Hour,
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseFile(path string) bool {
|
||||||
|
var failure bool
|
||||||
|
|
||||||
|
// Open config file
|
||||||
|
file, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "ERROR: cannot open config file %q: %s\n", path, err.Error())
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read data from config file
|
||||||
|
var data = make([]byte, 10000)
|
||||||
|
n, err := file.Read(data)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "ERROR: cannot read config file %q: %s\n", path, err.Error())
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse toml from config file
|
||||||
|
if err = toml.Unmarshal(data[:n], &Conf); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "ERROR: cannot load toml config from file %q: %s\n", path, err.Error())
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check arguments in config file
|
||||||
|
if net.ParseIP(Conf.BindHost) == nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "ERROR: bad value for 'bind_host' in config file %q: should be a valid IP address\n", path)
|
||||||
|
failure = true
|
||||||
|
}
|
||||||
|
if Conf.BindPort < 1 || Conf.BindPort > 65535 {
|
||||||
|
fmt.Fprintf(os.Stderr, "ERROR: bad value for 'bind_port' in config file %q: should be an integer in 1-65535\n", path)
|
||||||
|
failure = true
|
||||||
|
}
|
||||||
|
if Conf.SMTPHost == "" {
|
||||||
|
fmt.Fprintf(os.Stderr, "ERROR: missing value for 'smtp_host' in config file %q\n", path)
|
||||||
|
failure = true
|
||||||
|
}
|
||||||
|
if Conf.SMTPPort < 1 || Conf.SMTPPort > 65535 {
|
||||||
|
fmt.Fprintf(os.Stderr, "ERROR: bad value for 'smtp_port' in config file %q: should be an integer in 1-65535\n", path)
|
||||||
|
failure = true
|
||||||
|
}
|
||||||
|
if Conf.SMTPUsername == "" {
|
||||||
|
fmt.Fprintf(os.Stderr, "ERROR: missing value for 'smtp_username' in config file %q\n", path)
|
||||||
|
failure = true
|
||||||
|
}
|
||||||
|
if Conf.SMTPPassword == "" {
|
||||||
|
fmt.Fprintf(os.Stderr, "ERROR: missing value for 'smtp_password' in config file %q\n", path)
|
||||||
|
failure = true
|
||||||
|
}
|
||||||
|
if Conf.SMTPReceiver == "" {
|
||||||
|
fmt.Fprintf(os.Stderr, "ERROR: missing value for 'smtp_receiver' in config file %q\n", path)
|
||||||
|
failure = true
|
||||||
|
}
|
||||||
|
if Conf.CaptchaDirectory == "" {
|
||||||
|
fmt.Fprintf(os.Stderr, "ERROR: missing value for 'captcha_directory' in config file %q\n", path)
|
||||||
|
failure = true
|
||||||
|
}
|
||||||
|
if Conf.CaptchaLength == 0 {
|
||||||
|
fmt.Fprintf(os.Stderr, "ERROR: missing value for 'captcha_length' in config file %q\n", path)
|
||||||
|
failure = true
|
||||||
|
}
|
||||||
|
if Conf.CaptchaWidth == 0 {
|
||||||
|
fmt.Fprintf(os.Stderr, "ERROR: missing value for 'captcha_width' in config file %q\n", path)
|
||||||
|
failure = true
|
||||||
|
}
|
||||||
|
if Conf.CaptchaHeight == 0 {
|
||||||
|
fmt.Fprintf(os.Stderr, "ERROR: missing value for 'captcha_height' in config file %q\n", path)
|
||||||
|
failure = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return !failure
|
||||||
|
}
|
|
@ -0,0 +1,97 @@
|
||||||
|
package contact
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/dchest/captcha"
|
||||||
|
"gopkg.in/gomail.v2"
|
||||||
|
"net/http"
|
||||||
|
"netoik.io/netoik-website/pkg/api"
|
||||||
|
"netoik.io/netoik-website/pkg/conf"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
type request struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
Phone string `json:"phone"`
|
||||||
|
Company string `json:"company"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
CaptchaId string `json:"captchaId"`
|
||||||
|
CaptchaDigits string `json:"captchaDigits"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func HandleSend(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Check method
|
||||||
|
if r.Method != "POST" {
|
||||||
|
api.Reply(w, 405, api.Answer{})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse json from request body
|
||||||
|
var data request
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
|
||||||
|
api.Reply(w, 400, api.Answer{})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if data.Name == "" || len(data.Name) > 200 {
|
||||||
|
api.Reply(w, 400, api.Answer{})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if data.Email == "" || len(data.Email) > 200 {
|
||||||
|
api.Reply(w, 400, api.Answer{})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(data.Phone) > 200 {
|
||||||
|
api.Reply(w, 400, api.Answer{})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(data.Company) > 200 {
|
||||||
|
api.Reply(w, 400, api.Answer{})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if data.Message == "" || len(data.Message) > 10000 {
|
||||||
|
api.Reply(w, 400, api.Answer{})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if data.CaptchaId == "" {
|
||||||
|
api.Reply(w, 400, api.Answer{})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if data.CaptchaDigits == "" {
|
||||||
|
api.Reply(w, 400, api.Answer{})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check captcha digits
|
||||||
|
if !captcha.VerifyString(data.CaptchaId, data.CaptchaDigits) {
|
||||||
|
api.Reply(w, 418, api.Answer{})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Captcha has been verified, so remove image
|
||||||
|
path := filepath.Join(conf.Conf.CaptchaDirectory, data.CaptchaId+".png")
|
||||||
|
if err := os.Remove(path); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||||
|
fmt.Fprintf(os.Stderr, "ERROR: cannot remove captcha image %q: %s", path, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build email
|
||||||
|
msg := gomail.NewMessage()
|
||||||
|
msg.SetHeader("From", conf.Conf.SMTPUsername)
|
||||||
|
msg.SetHeader("To", conf.Conf.SMTPReceiver)
|
||||||
|
msg.SetHeader("Subject", "Message from www.netoik.io")
|
||||||
|
msg.SetBody("text/plain", fmt.Sprintf(
|
||||||
|
"You have received a message from frontend.\nname: %s\nemail: %s\nphone: %s\ncompany: %s\n%s",
|
||||||
|
data.Name, data.Email, data.Phone, data.Company, data.Message))
|
||||||
|
|
||||||
|
// Configure SMTP dialer and send email
|
||||||
|
dialer := gomail.NewDialer(conf.Conf.SMTPHost, conf.Conf.SMTPPort, conf.Conf.SMTPUsername, conf.Conf.SMTPPassword)
|
||||||
|
if err := dialer.DialAndSend(msg); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "cannot send email: %s\n", err.Error())
|
||||||
|
api.Reply(w, 400, api.Answer{})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
api.Reply(w, 200, api.Answer{Success: true})
|
||||||
|
}
|
Loading…
Reference in New Issue