mirror of
https://github.com/la5nta/pat
synced 2026-08-13 17:46:54 -04:00
Ref #491
CMS was recently patched to resolve the issue. The auto-generated
passwords will no longer take effect until the OTA activation message
has been downloaded.
This reverts commit e1354901a7.
352 lines
9.5 KiB
Go
352 lines
9.5 KiB
Go
// Copyright 2016 Martin Hebnes Pedersen (LA5NTA). All rights reserved.
|
|
// Use of this source code is governed by the MIT-license that can be
|
|
// found in the LICENSE file.
|
|
|
|
package app
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log"
|
|
"net"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/la5nta/pat/api/types"
|
|
"github.com/la5nta/pat/internal/buildinfo"
|
|
|
|
"github.com/la5nta/wl2k-go/fbb"
|
|
)
|
|
|
|
type ex struct {
|
|
conn net.Conn
|
|
target string
|
|
master bool
|
|
errors chan error
|
|
}
|
|
|
|
func (a *App) exchangeLoop(ctx context.Context) chan ex {
|
|
ce := make(chan ex)
|
|
go func() {
|
|
for {
|
|
select {
|
|
case ex := <-ce:
|
|
ex.errors <- a.sessionExchange(ex.conn, ex.target, ex.master)
|
|
close(ex.errors)
|
|
case <-ctx.Done():
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
return ce
|
|
}
|
|
|
|
func (a *App) exchange(conn net.Conn, targetCall string, master bool) error {
|
|
e := ex{
|
|
conn: conn,
|
|
target: targetCall,
|
|
master: master,
|
|
errors: make(chan error),
|
|
}
|
|
a.exchangeChan <- e
|
|
return <-e.errors
|
|
}
|
|
|
|
type NotifyMBox struct {
|
|
fbb.MBoxHandler
|
|
*App
|
|
}
|
|
|
|
func (m NotifyMBox) ProcessInbound(msgs ...*fbb.Message) error {
|
|
if err := m.MBoxHandler.ProcessInbound(msgs...); err != nil {
|
|
return err
|
|
}
|
|
for _, msg := range msgs {
|
|
m.websocketHub.WriteNotification(types.Notification{
|
|
Title: fmt.Sprintf("New message from %s", msg.From().Addr),
|
|
Body: msg.Subject(),
|
|
})
|
|
if isServiceMessage(msg) {
|
|
m.onServiceMessageReceived(msg)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m NotifyMBox) GetInboundAnswers(p []fbb.Proposal) []fbb.ProposalAnswer {
|
|
answers := make([]fbb.ProposalAnswer, len(p))
|
|
var outsideLimit bool
|
|
var hasAccountActivation bool
|
|
for idx, p := range p {
|
|
answers[idx] = m.GetInboundAnswer(p)
|
|
outsideLimit = outsideLimit || p.CompressedSize() >= m.config.AutoDownloadSizeLimit
|
|
if pm := p.PendingMessage(); pm != nil {
|
|
hasAccountActivation = hasAccountActivation || isAccountActivation(pm.From, pm.Subject)
|
|
}
|
|
}
|
|
if hasAccountActivation {
|
|
res := <-m.promptHub.Prompt(
|
|
context.Background(),
|
|
time.Minute,
|
|
types.PromptKindAccountActivation,
|
|
"Important: Your New Account Password",
|
|
)
|
|
if declined := res.Value != "accept"; declined {
|
|
// Defer all proposals
|
|
for idx := range answers {
|
|
answers[idx] = fbb.Defer
|
|
}
|
|
return answers
|
|
}
|
|
}
|
|
if !outsideLimit || m.config.AutoDownloadSizeLimit < 0 {
|
|
// All proposals are within the prompt limit. Go ahead.
|
|
return answers
|
|
}
|
|
|
|
// Build multi-select build options for those accepted by the mailbox handler.
|
|
var options []PromptOption
|
|
for idx, p := range p {
|
|
if answers[idx] != fbb.Accept {
|
|
continue
|
|
}
|
|
answers[idx] = fbb.Defer // Defer unless user explicitly accepts through prompt answer.
|
|
sender, subject := "Unkown sender", "Unknown subject"
|
|
if pm := p.PendingMessage(); pm != nil {
|
|
sender, subject = pm.From.String(), pm.Subject
|
|
}
|
|
desc := fmt.Sprintf("%s (%d bytes): %s", sender, p.CompressedSize(), subject)
|
|
options = append(options, PromptOption{Value: p.MID(), Desc: desc, Checked: p.CompressedSize() < m.config.AutoDownloadSizeLimit})
|
|
}
|
|
|
|
// Prompt the user
|
|
ans := <-m.promptHub.Prompt(context.Background(), time.Minute, PromptKindMultiSelect, "Select messages for download", options...)
|
|
|
|
// If timeout was reached, use our default values to fill in for the user
|
|
if ans.Err == context.DeadlineExceeded {
|
|
var checked []string
|
|
for _, opt := range options {
|
|
if opt.Checked {
|
|
checked = append(checked, opt.Value)
|
|
}
|
|
}
|
|
ans.Value = strings.Join(checked, ",")
|
|
}
|
|
|
|
// For each mid in answer, search the proposals and update answer to Accept.
|
|
for _, val := range strings.Split(ans.Value, ",") {
|
|
for idx, p := range p {
|
|
if p.MID() != val {
|
|
continue
|
|
}
|
|
answers[idx] = fbb.Accept
|
|
}
|
|
}
|
|
|
|
return answers
|
|
}
|
|
|
|
func (a *App) sessionExchange(conn net.Conn, targetCall string, master bool) error {
|
|
a.exchangeConn = conn
|
|
a.websocketHub.UpdateStatus()
|
|
defer func() { a.exchangeConn = nil; a.websocketHub.UpdateStatus() }()
|
|
|
|
// New wl2k Session
|
|
targetCall = strings.Split(targetCall, ` `)[0]
|
|
session := fbb.NewSession(
|
|
a.options.MyCall,
|
|
targetCall,
|
|
a.config.Locator,
|
|
NotifyMBox{a.mbox, a},
|
|
)
|
|
|
|
session.SetUserAgent(fbb.UserAgent{
|
|
Name: buildinfo.AppName,
|
|
Version: buildinfo.Version,
|
|
})
|
|
|
|
if len(a.config.MOTD) > 0 {
|
|
session.SetMOTD(a.config.MOTD...)
|
|
}
|
|
|
|
// Handle secure login
|
|
session.SetSecureLoginHandleFunc(func(addr fbb.Address) (string, error) {
|
|
if addr.Addr == a.options.MyCall && a.config.SecureLoginPassword != "" {
|
|
return a.config.SecureLoginPassword, nil
|
|
}
|
|
for _, aux := range a.config.AuxAddrs {
|
|
if !addr.EqualString(aux.Address) {
|
|
continue
|
|
}
|
|
switch {
|
|
case aux.Password != nil:
|
|
return *aux.Password, nil
|
|
case a.config.SecureLoginPassword != "":
|
|
return a.config.SecureLoginPassword, nil
|
|
}
|
|
}
|
|
resp := <-a.promptHub.Prompt(context.Background(), time.Minute, PromptKindPassword, "Enter secure login password for "+addr.String())
|
|
return resp.Value, resp.Err
|
|
})
|
|
|
|
for _, addr := range a.config.AuxAddrs {
|
|
session.AddAuxiliaryAddress(fbb.AddressFromString(addr.Address))
|
|
}
|
|
|
|
session.IsMaster(master)
|
|
session.SetLogger(log.New(a.termWriter, "", 0))
|
|
|
|
session.SetStatusUpdater(StatusUpdate{a.websocketHub})
|
|
|
|
if a.options.Robust {
|
|
session.SetRobustMode(fbb.RobustForced)
|
|
}
|
|
|
|
log.Printf("Connected to %s (%s)", conn.RemoteAddr(), conn.RemoteAddr().Network())
|
|
|
|
start := time.Now()
|
|
|
|
stats, err := session.Exchange(conn)
|
|
if fbb.IsLoginFailure(err) {
|
|
fmt.Println("NOTE: A new password scheme for Winlink is being implemented as of 2018-01-31.")
|
|
fmt.Println(" Users with passwords created/changed prior to January 31, 2018 should be")
|
|
fmt.Println(" aware that their password MUST be entered in ALL-UPPERCASE letters. Only")
|
|
fmt.Println(" passwords created/changed/issued after January 31, 2018 should/may contain")
|
|
fmt.Println(" lowercase letters. - https://github.com/la5nta/pat/issues/113")
|
|
}
|
|
|
|
if t, _ := strconv.ParseBool(os.Getenv("PAT_MOCK_NEW_ACCOUNT_MSG")); t {
|
|
log.Println("Mocking new account msg...")
|
|
NotifyMBox{a.mbox, a}.ProcessInbound(mockNewAccountMsg())
|
|
}
|
|
|
|
event := map[string]interface{}{
|
|
"mycall": session.Mycall(),
|
|
"targetcall": session.Targetcall(),
|
|
"remote_fw": session.RemoteForwarders(),
|
|
"remote_sid": session.RemoteSID(),
|
|
"master": master,
|
|
"local_locator": a.config.Locator,
|
|
"auxiliary_addresses": a.config.AuxAddrs,
|
|
"network": conn.RemoteAddr().Network(),
|
|
"remote_addr": conn.RemoteAddr().String(),
|
|
"local_addr": conn.LocalAddr().String(),
|
|
"sent": stats.Sent,
|
|
"received": stats.Received,
|
|
"start": start.Unix(),
|
|
"end": time.Now().Unix(),
|
|
"success": err == nil,
|
|
}
|
|
if err != nil {
|
|
event["error"] = err.Error()
|
|
}
|
|
|
|
a.eventLog.Log("exchange", event)
|
|
|
|
return err
|
|
}
|
|
|
|
func (a *App) AbortActiveConnection(dirty bool) (ok bool) {
|
|
switch {
|
|
case dirty:
|
|
// This mean we've already tried to abort, but the connection is still active.
|
|
// Fallback to the below cases to try to identify the busy modem and abort hard.
|
|
case a.dialing != nil:
|
|
// If we're currently dialing a transport, attempt to abort by cancelling the associated context.
|
|
log.Printf("Got abort signal while dialing %s, cancelling...", a.dialing.Scheme)
|
|
go a.dialCancelFunc()
|
|
return true
|
|
case a.exchangeConn != nil:
|
|
// If we have an active connection, close it gracefully.
|
|
log.Println("Got abort signal, disconnecting...")
|
|
go a.exchangeConn.Close()
|
|
return true
|
|
}
|
|
|
|
// Any connection and/or dial operation has been cancelled at this point.
|
|
// User is attempting to abort something, so try to identify any non-idling transports and abort.
|
|
// It might be a "dirty disconnect" of an already cancelled connection or dial operation which is in the
|
|
// process of gracefully terminating. It might also be an attempt to close an inbound P2P connection.
|
|
switch {
|
|
case a.ardop != nil && !a.ardop.Idle():
|
|
if dirty {
|
|
log.Println("Dirty disconnecting ardop...")
|
|
a.ardop.Abort()
|
|
return true
|
|
}
|
|
log.Println("Disconnecting ardop...")
|
|
go func() {
|
|
if err := a.ardop.Disconnect(); err != nil {
|
|
log.Println(err)
|
|
}
|
|
}()
|
|
return true
|
|
case a.varaFM != nil && !a.varaFM.Idle():
|
|
if dirty {
|
|
log.Println("Dirty disconnecting varafm...")
|
|
a.varaFM.Abort()
|
|
return true
|
|
}
|
|
log.Println("Disconnecting varafm...")
|
|
go func() {
|
|
if err := a.varaFM.Close(); err != nil {
|
|
log.Println(err)
|
|
}
|
|
}()
|
|
return true
|
|
case a.varaHF != nil && !a.varaHF.Idle():
|
|
if dirty {
|
|
log.Println("Dirty disconnecting varahf...")
|
|
a.varaHF.Abort()
|
|
return true
|
|
}
|
|
log.Println("Disconnecting varahf...")
|
|
go func() {
|
|
if err := a.varaHF.Close(); err != nil {
|
|
log.Println(err)
|
|
}
|
|
}()
|
|
return true
|
|
case a.pactor != nil:
|
|
log.Println("Disconnecting pactor...")
|
|
err := a.pactor.Close()
|
|
if err != nil {
|
|
log.Println(err)
|
|
}
|
|
return err == nil
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
type StatusUpdate struct{ WSHub }
|
|
|
|
func (s StatusUpdate) UpdateStatus(stat fbb.Status) {
|
|
var prop fbb.Proposal
|
|
switch {
|
|
case stat.Receiving != nil:
|
|
prop = *stat.Receiving
|
|
case stat.Sending != nil:
|
|
prop = *stat.Sending
|
|
}
|
|
|
|
s.WriteProgress(types.Progress{
|
|
MID: prop.MID(),
|
|
BytesTotal: stat.BytesTotal,
|
|
BytesTransferred: stat.BytesTransferred,
|
|
Subject: prop.Title(),
|
|
Receiving: stat.Receiving != nil,
|
|
Sending: stat.Sending != nil,
|
|
Done: stat.Done,
|
|
})
|
|
|
|
percent := float64(stat.BytesTransferred) / float64(stat.BytesTotal) * 100
|
|
fmt.Printf("\r%s: %3.0f%%", prop.Title(), percent)
|
|
|
|
if stat.Done {
|
|
fmt.Println("")
|
|
}
|
|
os.Stdout.Sync()
|
|
}
|