diff --git a/README.MD b/README.MD index 19fb1b95..f54cd6cb 100644 --- a/README.MD +++ b/README.MD @@ -213,6 +213,11 @@ notification: enable-email: false # 邮件 SMTP 服务器及其端口 email-smtp: smtp.example.com:25 + # SMTP 服务器加密类型 + # - none : 无加密 + # - ssl : 使用 SSL/TLS 加密 + # - tls : 使用 STARTTLS 加密 + email-smtp-encryption: tls # 通知者邮箱用户名 email-sender: noreply@example.com # 通知者邮箱密码 diff --git a/cluster.go b/cluster.go index 396f49c4..367edaca 100644 --- a/cluster.go +++ b/cluster.go @@ -230,7 +230,10 @@ func (cr *Cluster) Init(ctx context.Context) (err error) { webpushPlg := new(webpush.Plugin) cr.notifyManager.AddPlugin(webpushPlg) if config.Notification.EnableEmail { - emailPlg, err := email.NewSMTP(config.Notification.EmailSMTP, config.Notification.EmailSender, config.Notification.EmailSenderPassword) + emailPlg, err := email.NewSMTP( + config.Notification.EmailSMTP, config.Notification.EmailSMTPEncryption, + config.Notification.EmailSender, config.Notification.EmailSenderPassword, + ) if err != nil { return err } diff --git a/config.go b/config.go index 46eff455..8f11831e 100644 --- a/config.go +++ b/config.go @@ -80,6 +80,7 @@ type APIRateLimitConfig struct { type NotificationConfig struct { EnableEmail bool `yaml:"enable-email"` EmailSMTP string `yaml:"email-smtp"` + EmailSMTPEncryption string `yaml:"email-smtp-encryption"` EmailSender string `yaml:"email-sender"` EmailSenderPassword string `yaml:"email-sender-password"` EnableWebhook bool `yaml:"enable-webhook"` @@ -212,6 +213,9 @@ type Config struct { Storages []storage.StorageOption `yaml:"storages"` WebdavUsers map[string]*storage.WebDavUser `yaml:"webdav-users"` Advanced AdvancedConfig `yaml:"advanced"` + + IamTeaPot bool `yaml:"i-am-a-tea-pot"` + TeapotMessages []string `yaml:"teapot-messages"` } func (cfg *Config) applyWebManifest(manifest map[string]any) { @@ -277,6 +281,7 @@ var defaultConfig = Config{ Notification: NotificationConfig{ EnableEmail: false, EmailSMTP: "smtp.example.com:25", + EmailSMTPEncryption: "tls", EmailSender: "noreply@example.com", EmailSenderPassword: "example-password", EnableWebhook: true, @@ -326,6 +331,14 @@ var defaultConfig = Config{ NoFastEnable: false, WaitBeforeEnable: 0, }, + + IamTeaPot: false, + TeapotMessages: []string{ + "This is OpemBnclApi golang edition", + "Your internet is shutdown", + "Virus detected, removing your game data ...", + ":)", + }, } func migrateConfig(data []byte, config *Config) { diff --git a/config.yaml b/config.yaml index 894d9e43..ea91ef80 100644 --- a/config.yaml +++ b/config.yaml @@ -37,6 +37,7 @@ api-rate-limit: notification: enable-email: false email-smtp: smtp.example.com:25 + email-smtp-encryption: tls email-sender: noreply@example.com email-sender-password: example-password enable-webhook: true diff --git a/dashboard/src/api/v0.ts b/dashboard/src/api/v0.ts index c7e61e3a..b59cb319 100644 --- a/dashboard/src/api/v0.ts +++ b/dashboard/src/api/v0.ts @@ -147,17 +147,27 @@ export async function getSubscribePublicKey(token?: string): Promise { return res.data.publicKey } -export type SubscribeScope = 'enabled' | 'disabled' | 'syncdone' | 'updates' | 'dailyreport' +export type SubscribeScope = + | 'enabled' + | 'disabled' + | 'syncbegin' + | 'syncdone' + | 'updates' + | 'dailyreport' export const ALL_SUBSCRIBE_SCOPES: SubscribeScope[] = [ 'enabled', 'disabled', + 'syncbegin', 'syncdone', 'updates', 'dailyreport', ] +export type ScopeFlags = { + [key in SubscribeScope]: boolean +} export interface SubscribeSettings { - scopes: { [key in SubscribeScope]: boolean } + scopes: ScopeFlags reportAt: string } @@ -219,7 +229,7 @@ export interface EmailItemPayload { export interface EmailItemRes { user: string addr: string - scopes: SubscribeScope[] + scopes: ScopeFlags enabled: boolean } @@ -281,7 +291,7 @@ export interface WebhookItemRes { name: string endpoint: string authHash?: string - scopes: SubscribeScope[] + scopes: ScopeFlags enabled: boolean } diff --git a/dashboard/src/views/SettingsView.vue b/dashboard/src/views/SettingsView.vue index afd81314..a0dd6579 100644 --- a/dashboard/src/views/SettingsView.vue +++ b/dashboard/src/views/SettingsView.vue @@ -37,6 +37,7 @@ const settings = bindObjectToLocalStorage( { notifyWhenDisabled: false, notifyWhenEnabled: false, + notifyWhenSyncBegin: false, notifyWhenSyncFinished: false, notifyUpdates: false, dailyReport: false, @@ -68,6 +69,9 @@ function getSubscribeScopes(): SubscribeScope[] { if (settings.notifyWhenEnabled) { res.push('enabled') } + if (settings.notifyWhenSyncBegin) { + res.push('syncbegin') + } if (settings.notifyWhenSyncFinished) { res.push('syncdone') } @@ -183,6 +187,7 @@ onMounted(() => { enableNotify.value = true settings.notifyWhenDisabled = sets.scopes.disabled settings.notifyWhenEnabled = sets.scopes.enabled + settings.notifyWhenSyncBegin = sets.scopes.syncbegin settings.notifyWhenSyncFinished = sets.scopes.syncdone settings.notifyUpdates = sets.scopes.updates settings.dailyReport = sets.scopes.dailyreport diff --git a/dashboard/src/views/settings/NotificationsView.vue b/dashboard/src/views/settings/NotificationsView.vue index 5fa9351f..76e4b863 100644 --- a/dashboard/src/views/settings/NotificationsView.vue +++ b/dashboard/src/views/settings/NotificationsView.vue @@ -22,6 +22,7 @@ import { updateWebhook, removeWebhook, type SubscribeScope, + type ScopeFlags, type EmailItemPayload, type EmailItemRes, type WebhookItemPayload, @@ -57,7 +58,12 @@ const webhookEdited = computed((): boolean => { return true } } - if (ori.scopes.join() !== item.scopes.join()) { + if ( + (Object.keys(ori.scopes) as SubscribeScope[]) + .map((v) => ori.scopes[v]) + .sort() + .join() !== item.scopes.sort().join() + ) { return true } if (item.auth !== '-') { @@ -117,6 +123,7 @@ async function addEmail(item: EmailItemPayload): Promise { emails.value?.push({ user: '', ...item, + scopes: Object.fromEntries(item.scopes.map((v) => [v, true])) as ScopeFlags, }) } catch (err) { toast.add({ @@ -184,9 +191,16 @@ async function openRemoveWebhookDialog(index: number): Promise { } } +function scopeFlagsToArray(o: ScopeFlags): SubscribeScope[] { + return (Object.keys(o) as SubscribeScope[]) + .filter((v) => o[v]) + .sort() +} + function openWebhookEditDialog(item: WebhookItemRes): void { webhookEditingItem.value = { ...item, + scopes: scopeFlagsToArray(item.scopes), auth: '-', _: item, } @@ -328,7 +342,7 @@ onBeforeMount(() => { diff --git a/handler.go b/handler.go index f4eacc5f..caf9912f 100644 --- a/handler.go +++ b/handler.go @@ -33,6 +33,7 @@ import ( "os" "strconv" "strings" + "sync" "time" "github.com/LiterMC/go-openbmclapi/internal/build" @@ -327,6 +328,9 @@ var emptyHashes = func() (hashes map[string]struct{}) { var HeaderXPoweredBy = fmt.Sprintf("go-openbmclapi/%s; url=https://github.com/LiterMC/go-openbmclapi", build.BuildVersion) +var accessedTeapotMux sync.RWMutex +var accessedTeapot = make(map[string]struct{}) + func (cr *Cluster) ServeHTTP(rw http.ResponseWriter, req *http.Request) { method := req.Method u := req.URL @@ -342,6 +346,53 @@ func (cr *Cluster) ServeHTTP(rw http.ResponseWriter, req *http.Request) { return } + /** TODO: remove after 04/01 **/ + if config.IamTeaPot { + ip, _ := req.Context().Value(RealAddrCtxKey).(string) + if ip != "" { + _, month, day := time.Now().Date() + if month == time.April && day == 1 || month == time.March && day == 31 || month == time.March && day == 32 { + ua, _, _ := strings.Cut(req.Header.Get("User-Agent"), " ") + ua, _, _ = strings.Cut(ua, "/") + chance := 0 + switch ua { + case "HMCL": + chance = 512 + case "PCL": + chance = 511 + case "PojavLauncher": + chance = 512 * 10 + case "FCL": + fallthrough + case "BakaXL": + chance = 512 * 2 + } + if chance > 0 { + if randIntn(1024000) < chance/2 { + accessedTeapotMux.RLock() + _, ok := accessedTeapot[ip] + accessedTeapotMux.RUnlock() + if !ok { + accessedTeapotMux.Lock() + if _, ok = accessedTeapot[ip]; !ok { + accessedTeapot[ip] = struct{}{} + } + accessedTeapotMux.Unlock() + if !ok { + var msg string = "okay, this is a teapot, however, you will never saw this again" + if len(config.TeapotMessages) > 0 { + msg = config.TeapotMessages[randIntn(len(config.TeapotMessages))] + } + http.Error(rw, msg, http.StatusTeapot) + return + } + } + } + } + } + } + } + hash := rawpath[len("/download/"):] if !utils.IsHex(hash) { http.Error(rw, hash+" is not a valid hash", http.StatusNotFound) diff --git a/notify/email/email.go b/notify/email/email.go index 6add2948..b9d77b5e 100644 --- a/notify/email/email.go +++ b/notify/email/email.go @@ -28,6 +28,7 @@ import ( "html/template" "net" "strconv" + "strings" "time" mail "github.com/xhit/go-simple-mail/v2" @@ -64,16 +65,25 @@ type Plugin struct { var _ notify.Plugin = (*Plugin)(nil) -func NewSMTP(smtpAddr string, username string, password string) (*Plugin, error) { +func NewSMTP(smtpAddr string, smtpType string, username string, password string) (*Plugin, error) { smtp := &mail.SMTPServer{ Authentication: mail.AuthAuto, - Encryption: mail.EncryptionSTARTTLS, Helo: "localhost", ConnectTimeout: 15 * time.Second, SendTimeout: time.Minute, Username: username, Password: password, } + switch strings.ToLower(smtpType) { + case "tls", "starttls": + smtp.Encryption = mail.EncryptionSTARTTLS + case "ssl", "ssltls": + smtp.Encryption = mail.EncryptionSSLTLS + case "none": + smtp.Encryption = mail.EncryptionNone + default: + return nil, fmt.Errorf("Unknown smtp encryption type %q", smtpType) + } var ( port string err error