first-commit
This commit is contained in:
23
services/auth/source/smtp/assert_interface_test.go
Normal file
23
services/auth/source/smtp/assert_interface_test.go
Normal file
@@ -0,0 +1,23 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package smtp_test
|
||||
|
||||
import (
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
"code.gitea.io/gitea/services/auth"
|
||||
"code.gitea.io/gitea/services/auth/source/smtp"
|
||||
)
|
||||
|
||||
// This test file exists to assert that our Source exposes the interfaces that we expect
|
||||
// It tightly binds the interfaces and implementation without breaking go import cycles
|
||||
|
||||
type sourceInterface interface {
|
||||
auth.PasswordAuthenticator
|
||||
auth_model.Config
|
||||
auth_model.SkipVerifiable
|
||||
auth_model.HasTLSer
|
||||
auth_model.UseTLSer
|
||||
}
|
||||
|
||||
var _ (sourceInterface) = &smtp.Source{}
|
106
services/auth/source/smtp/auth.go
Normal file
106
services/auth/source/smtp/auth.go
Normal file
@@ -0,0 +1,106 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package smtp
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/smtp"
|
||||
"os"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// _________ __________________________
|
||||
// / _____/ / \__ ___/\______ \
|
||||
// \_____ \ / \ / \| | | ___/
|
||||
// / \/ Y \ | | |
|
||||
// /_______ /\____|__ /____| |____|
|
||||
// \/ \/
|
||||
|
||||
type loginAuthenticator struct {
|
||||
username, password string
|
||||
}
|
||||
|
||||
func (auth *loginAuthenticator) Start(server *smtp.ServerInfo) (string, []byte, error) {
|
||||
return "LOGIN", []byte(auth.username), nil
|
||||
}
|
||||
|
||||
func (auth *loginAuthenticator) Next(fromServer []byte, more bool) ([]byte, error) {
|
||||
if more {
|
||||
switch string(fromServer) {
|
||||
case "Username:":
|
||||
return []byte(auth.username), nil
|
||||
case "Password:":
|
||||
return []byte(auth.password), nil
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// SMTP authentication type names.
|
||||
const (
|
||||
PlainAuthentication = "PLAIN"
|
||||
LoginAuthentication = "LOGIN"
|
||||
CRAMMD5Authentication = "CRAM-MD5"
|
||||
)
|
||||
|
||||
// Authenticators contains available SMTP authentication type names.
|
||||
var Authenticators = []string{PlainAuthentication, LoginAuthentication, CRAMMD5Authentication}
|
||||
|
||||
// ErrUnsupportedLoginType login source is unknown error
|
||||
var ErrUnsupportedLoginType = errors.New("Login source is unknown")
|
||||
|
||||
// Authenticate performs an SMTP authentication.
|
||||
func Authenticate(a smtp.Auth, source *Source) error {
|
||||
tlsConfig := &tls.Config{
|
||||
InsecureSkipVerify: source.SkipVerify,
|
||||
ServerName: source.Host,
|
||||
}
|
||||
|
||||
conn, err := net.Dial("tcp", net.JoinHostPort(source.Host, strconv.Itoa(source.Port)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
if source.UseTLS() {
|
||||
conn = tls.Client(conn, tlsConfig)
|
||||
}
|
||||
|
||||
client, err := smtp.NewClient(conn, source.Host)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create NewClient: %w", err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
if !source.DisableHelo {
|
||||
hostname := source.HeloHostname
|
||||
if len(hostname) == 0 {
|
||||
hostname, err = os.Hostname()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to find Hostname: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err = client.Hello(hostname); err != nil {
|
||||
return fmt.Errorf("failed to send Helo: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// If not using SMTPS, always use STARTTLS if available
|
||||
hasStartTLS, _ := client.Extension("STARTTLS")
|
||||
if !source.UseTLS() && hasStartTLS {
|
||||
if err = client.StartTLS(tlsConfig); err != nil {
|
||||
return fmt.Errorf("failed to start StartTLS: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if ok, _ := client.Extension("AUTH"); ok {
|
||||
return client.Auth(a)
|
||||
}
|
||||
|
||||
return ErrUnsupportedLoginType
|
||||
}
|
59
services/auth/source/smtp/source.go
Normal file
59
services/auth/source/smtp/source.go
Normal file
@@ -0,0 +1,59 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package smtp
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/models/auth"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
)
|
||||
|
||||
// _________ __________________________
|
||||
// / _____/ / \__ ___/\______ \
|
||||
// \_____ \ / \ / \| | | ___/
|
||||
// / \/ Y \ | | |
|
||||
// /_______ /\____|__ /____| |____|
|
||||
// \/ \/
|
||||
|
||||
// Source holds configuration for the SMTP login source.
|
||||
type Source struct {
|
||||
auth.ConfigBase `json:"-"`
|
||||
|
||||
Auth string
|
||||
Host string
|
||||
Port int
|
||||
AllowedDomains string `xorm:"TEXT"`
|
||||
ForceSMTPS bool
|
||||
SkipVerify bool
|
||||
HeloHostname string
|
||||
DisableHelo bool
|
||||
}
|
||||
|
||||
// FromDB fills up an SMTPConfig from serialized format.
|
||||
func (source *Source) FromDB(bs []byte) error {
|
||||
return json.UnmarshalHandleDoubleEncode(bs, &source)
|
||||
}
|
||||
|
||||
// ToDB exports an SMTPConfig to a serialized format.
|
||||
func (source *Source) ToDB() ([]byte, error) {
|
||||
return json.Marshal(source)
|
||||
}
|
||||
|
||||
// IsSkipVerify returns if SkipVerify is set
|
||||
func (source *Source) IsSkipVerify() bool {
|
||||
return source.SkipVerify
|
||||
}
|
||||
|
||||
// HasTLS returns true for SMTP
|
||||
func (source *Source) HasTLS() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// UseTLS returns if TLS is set
|
||||
func (source *Source) UseTLS() bool {
|
||||
return source.ForceSMTPS || source.Port == 465
|
||||
}
|
||||
|
||||
func init() {
|
||||
auth.RegisterTypeConfig(auth.SMTP, &Source{})
|
||||
}
|
87
services/auth/source/smtp/source_authenticate.go
Normal file
87
services/auth/source/smtp/source_authenticate.go
Normal file
@@ -0,0 +1,87 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package smtp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/smtp"
|
||||
"net/textproto"
|
||||
"strings"
|
||||
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
// Authenticate queries if the provided login/password is authenticates against the SMTP server
|
||||
// Users will be autoregistered as required
|
||||
func (source *Source) Authenticate(ctx context.Context, user *user_model.User, userName, password string) (*user_model.User, error) {
|
||||
// Verify allowed domains.
|
||||
if len(source.AllowedDomains) > 0 {
|
||||
idx := strings.Index(userName, "@")
|
||||
if idx == -1 {
|
||||
return nil, user_model.ErrUserNotExist{Name: userName}
|
||||
} else if !util.SliceContainsString(strings.Split(source.AllowedDomains, ","), userName[idx+1:], true) {
|
||||
return nil, user_model.ErrUserNotExist{Name: userName}
|
||||
}
|
||||
}
|
||||
|
||||
var auth smtp.Auth
|
||||
switch source.Auth {
|
||||
case PlainAuthentication:
|
||||
auth = smtp.PlainAuth("", userName, password, source.Host)
|
||||
case LoginAuthentication:
|
||||
auth = &loginAuthenticator{userName, password}
|
||||
case CRAMMD5Authentication:
|
||||
auth = smtp.CRAMMD5Auth(userName, password)
|
||||
default:
|
||||
return nil, errors.New("unsupported SMTP auth type")
|
||||
}
|
||||
|
||||
if err := Authenticate(auth, source); err != nil {
|
||||
// Check standard error format first,
|
||||
// then fallback to worse case.
|
||||
tperr, ok := err.(*textproto.Error)
|
||||
if (ok && tperr.Code == 535) ||
|
||||
strings.Contains(err.Error(), "Username and Password not accepted") {
|
||||
return nil, user_model.ErrUserNotExist{Name: userName}
|
||||
}
|
||||
if (ok && tperr.Code == 534) ||
|
||||
strings.Contains(err.Error(), "Application-specific password required") {
|
||||
return nil, user_model.ErrUserNotExist{Name: userName}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if user != nil {
|
||||
return user, nil
|
||||
}
|
||||
|
||||
username := userName
|
||||
idx := strings.Index(userName, "@")
|
||||
if idx > -1 {
|
||||
username = userName[:idx]
|
||||
}
|
||||
|
||||
user = &user_model.User{
|
||||
LowerName: strings.ToLower(username),
|
||||
Name: strings.ToLower(username),
|
||||
Email: userName,
|
||||
Passwd: password,
|
||||
LoginType: auth_model.SMTP,
|
||||
LoginSource: source.AuthSource.ID,
|
||||
LoginName: userName,
|
||||
}
|
||||
overwriteDefault := &user_model.CreateUserOverwriteOptions{
|
||||
IsActive: optional.Some(true),
|
||||
}
|
||||
|
||||
if err := user_model.CreateUser(ctx, user, &user_model.Meta{}, overwriteDefault); err != nil {
|
||||
return user, err
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
Reference in New Issue
Block a user