move back to netoik-api repo
|
@ -1,2 +0,0 @@
|
||||||
build:
|
|
||||||
go build -o bin/server cmd/server/main.go
|
|
BIN
back/bin/server
|
@ -1,35 +0,0 @@
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
10
back/go.mod
|
@ -1,10 +0,0 @@
|
||||||
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
|
|
||||||
)
|
|
|
@ -1,8 +0,0 @@
|
||||||
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=
|
|
|
@ -1,18 +0,0 @@
|
||||||
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)
|
|
||||||
}
|
|
|
@ -1,60 +0,0 @@
|
||||||
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))
|
|
||||||
}
|
|
|
@ -1,108 +0,0 @@
|
||||||
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
|
|
||||||
}
|
|
|
@ -1,97 +0,0 @@
|
||||||
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})
|
|
||||||
}
|
|
Before Width: | Height: | Size: 8.7 KiB After Width: | Height: | Size: 8.7 KiB |
Before Width: | Height: | Size: 9.1 KiB After Width: | Height: | Size: 9.1 KiB |
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 5.6 KiB |
Before Width: | Height: | Size: 9.6 KiB After Width: | Height: | Size: 9.6 KiB |
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.5 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 5.8 KiB |
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 5.8 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 311 KiB After Width: | Height: | Size: 311 KiB |