358 lines
8.6 KiB
Go
358 lines
8.6 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"math/rand"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"os/signal"
|
|
"path"
|
|
"strconv"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/coreos/go-systemd/activation"
|
|
"github.com/olebedev/when"
|
|
"github.com/olebedev/when/rules/common"
|
|
"github.com/olebedev/when/rules/en"
|
|
)
|
|
|
|
// HTTPServer the http server structure.
|
|
type HTTPServer struct {
|
|
}
|
|
|
|
// Common strings.
|
|
const (
|
|
APIOK = "ok"
|
|
APIERR = "error"
|
|
)
|
|
|
|
// APIGeneralResp General response to API requests.
|
|
type APIGeneralResp struct {
|
|
Status string `json:"status"`
|
|
Error string `json:"error"`
|
|
}
|
|
|
|
// JSONResponse Takes a golang structure and converts it to a JSON object for response.
|
|
func (s *HTTPServer) JSONResponse(w http.ResponseWriter, resp interface{}) {
|
|
// Encode response as json.
|
|
js, err := json.Marshal(resp)
|
|
if err != nil {
|
|
// Error should not happen normally...
|
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// If no err, we can set content type header and send response.
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.Write(js)
|
|
w.Write([]byte{'\n'})
|
|
}
|
|
|
|
// APISendGeneralResp Send a standard response.
|
|
func (s *HTTPServer) APISendGeneralResp(w http.ResponseWriter, status, err string) {
|
|
resp := APIGeneralResp{}
|
|
resp.Status = status
|
|
resp.Error = err
|
|
s.JSONResponse(w, resp)
|
|
}
|
|
|
|
// registerHandlers HTTP server handlers.
|
|
func (s *HTTPServer) registerHandlers(r *http.ServeMux) {
|
|
// For this project, we only handle requests to /.
|
|
r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
|
// Parse form data.
|
|
err := r.ParseMultipartForm(32 << 20)
|
|
if err == http.ErrNotMultipart {
|
|
err = r.ParseForm()
|
|
}
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
s.APISendGeneralResp(w, APIERR, "Bad request")
|
|
return
|
|
}
|
|
|
|
// Verify we are authorized.
|
|
if r.Form.Get("token") != app.config.APIToken {
|
|
s.APISendGeneralResp(w, APIERR, "Unauthorized")
|
|
return
|
|
}
|
|
|
|
// Get call details.
|
|
channel := r.Form.Get("channel")
|
|
if channel == "" {
|
|
channel = app.config.DefaultChannel
|
|
}
|
|
|
|
callerId := r.Form.Get("caller_id")
|
|
if callerId == "" {
|
|
callerId = app.config.DefaultCallerId
|
|
}
|
|
|
|
waitTime, err := strconv.ParseUint(r.Form.Get("wait_time"), 10, 64)
|
|
if err != nil {
|
|
waitTime = app.config.DefaultWaitTime
|
|
}
|
|
|
|
maxRetries, err := strconv.ParseUint(r.Form.Get("max_retries"), 10, 64)
|
|
if err != nil {
|
|
maxRetries = app.config.DefaultMaxRetries
|
|
}
|
|
|
|
retryTime, err := strconv.ParseUint(r.Form.Get("retry_time"), 10, 64)
|
|
if err != nil {
|
|
retryTime = app.config.DefaultRetryTime
|
|
}
|
|
|
|
account := r.Form.Get("account")
|
|
if account == "" {
|
|
account = app.config.DefaultCallerId
|
|
}
|
|
|
|
application := r.Form.Get("application")
|
|
if application == "" || app.config.PreventAPIApplication {
|
|
application = app.config.DefaultApplication
|
|
}
|
|
|
|
data := r.Form.Get("data")
|
|
if data == "" || app.config.PreventAPIApplication {
|
|
data = app.config.DefaultData
|
|
}
|
|
|
|
context := r.Form.Get("context")
|
|
if context == "" {
|
|
context = app.config.DefaultContext
|
|
}
|
|
|
|
extension := r.Form.Get("extension")
|
|
if context == "" {
|
|
extension = app.config.DefaultExtension
|
|
}
|
|
|
|
priority := r.Form.Get("priority")
|
|
if context == "" {
|
|
priority = app.config.DefaultPriority
|
|
}
|
|
|
|
setVar := make(map[string]string)
|
|
parsed, err := url.ParseQuery(r.Form.Get("set_var"))
|
|
if err != nil {
|
|
setVar = app.config.DefaultSetVar
|
|
} else {
|
|
for key, value := range parsed {
|
|
setVar[key] = value[0]
|
|
}
|
|
}
|
|
|
|
archiveVal := strings.ToLower(r.Form.Get("archive"))
|
|
archive := false
|
|
if archiveVal == "true" || archiveVal == "yes" {
|
|
archive = true
|
|
} else if archiveVal != "false" && archiveVal != "no" {
|
|
archive = app.config.DefaultArchive
|
|
}
|
|
|
|
schedule := r.Form.Get("schedule")
|
|
|
|
if channel == "" || (application == "" && context == "") {
|
|
s.APISendGeneralResp(w, APIERR, "Required options not set")
|
|
return
|
|
}
|
|
|
|
// Setup call file details.
|
|
outgoingCallName := "outgoing-call-" + strconv.Itoa(rand.Int())
|
|
spoolFileName := path.Join(app.config.AsteriskSpoolDir, outgoingCallName)
|
|
outgoingFileName := path.Join(app.config.AsteriskSpoolDir, "outgoing", outgoingCallName)
|
|
|
|
callFile, err := os.Create(spoolFileName)
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
s.APISendGeneralResp(w, APIERR, "Unable to create call file")
|
|
return
|
|
}
|
|
|
|
// Write call details.
|
|
callFile.WriteString("Channel: " + channel + "\n")
|
|
|
|
if callerId != "" {
|
|
callFile.WriteString("Callerid: " + callerId + "\n")
|
|
}
|
|
|
|
if waitTime != 0 {
|
|
callFile.WriteString("WaitTime: " + strconv.FormatUint(waitTime, 10) + "\n")
|
|
}
|
|
|
|
if maxRetries != 0 {
|
|
callFile.WriteString("MaxRetries: " + strconv.FormatUint(maxRetries, 10) + "\n")
|
|
}
|
|
|
|
if retryTime != 0 {
|
|
callFile.WriteString("RetryTime: " + strconv.FormatUint(retryTime, 10) + "\n")
|
|
}
|
|
|
|
if account != "" {
|
|
callFile.WriteString("Account: " + account + "\n")
|
|
}
|
|
|
|
if application != "" {
|
|
callFile.WriteString("Application: " + application + "\n")
|
|
}
|
|
|
|
if data != "" {
|
|
callFile.WriteString("Data: " + data + "\n")
|
|
}
|
|
|
|
if context != "" {
|
|
callFile.WriteString("Context: " + context + "\n")
|
|
}
|
|
|
|
if extension != "" {
|
|
callFile.WriteString("Extension: " + extension + "\n")
|
|
}
|
|
|
|
if priority != "" {
|
|
callFile.WriteString("Priority: " + priority + "\n")
|
|
}
|
|
|
|
for key, value := range setVar {
|
|
callFile.WriteString("Setvar: " + key + "=" + value + "\n")
|
|
}
|
|
|
|
if archive {
|
|
callFile.WriteString("Archive: yes\n")
|
|
} else {
|
|
callFile.WriteString("Archive: no\n")
|
|
}
|
|
|
|
callFile.Close()
|
|
|
|
if schedule != "" {
|
|
now := time.Now()
|
|
w := when.New(nil)
|
|
w.Add(en.All...)
|
|
w.Add(common.All...)
|
|
parsedTime, _ := w.Parse(schedule, now)
|
|
if parsedTime == nil {
|
|
parsedTime = new(when.Result)
|
|
parsedTime.Time = now
|
|
}
|
|
|
|
os.Chtimes(spoolFileName, parsedTime.Time, parsedTime.Time)
|
|
}
|
|
|
|
// Add call to the outgoing call queue.
|
|
err = os.Rename(spoolFileName, outgoingFileName)
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
s.APISendGeneralResp(w, APIERR, "Unable to move call file into outgoing directory")
|
|
return
|
|
}
|
|
|
|
// Send final response.
|
|
s.APISendGeneralResp(w, APIOK, "")
|
|
})
|
|
}
|
|
|
|
func HTTPServe() {
|
|
// Used to reset the app quit timeout for systemd sockets.
|
|
var timeoutReset chan struct{}
|
|
|
|
// Create the server.
|
|
httpServer := new(HTTPServer)
|
|
app.httpServer = httpServer
|
|
|
|
// Setup the handlers.
|
|
r := http.NewServeMux()
|
|
httpServer.registerHandlers(r)
|
|
|
|
// The http server handler will be the mux router by default.
|
|
var handler http.Handler
|
|
handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
|
if app.config.HTTPSystemDSocket {
|
|
timeoutReset <- struct{}{}
|
|
}
|
|
if app.config.HTTPDebug {
|
|
log.Println(req.Method + " " + req.URL.String())
|
|
}
|
|
r.ServeHTTP(w, req)
|
|
})
|
|
|
|
// Determine if we're using a systemd socket activation or just a standard listen.
|
|
if app.config.HTTPSystemDSocket {
|
|
done := make(chan struct{})
|
|
quit := make(chan os.Signal, 1)
|
|
timeoutReset = make(chan struct{})
|
|
|
|
// On signal, gracefully shut down the server and wait 5
|
|
// seconds for current connection to stop.
|
|
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
|
|
|
// Pull existing listener from systemd.
|
|
listeners, err := activation.Listeners()
|
|
if err != nil {
|
|
log.Panicf("Cannot retrieve listeners: %v", err)
|
|
}
|
|
|
|
// If we already have a asterisk-outgoing-call-api running, then we shouldn't start...
|
|
if len(listeners) != 1 {
|
|
log.Panicf("Unexpected number of socket activation (%d != 1)", len(listeners))
|
|
}
|
|
|
|
server := &http.Server{
|
|
Handler: handler,
|
|
}
|
|
|
|
// Upon signal, close out existing connection and quit.
|
|
go func() {
|
|
<-quit
|
|
log.Println("Server is shutting down")
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
server.SetKeepAlivesEnabled(false)
|
|
if err := server.Shutdown(ctx); err != nil {
|
|
log.Panicf("Cannot gracefully shut down the server: %v", err)
|
|
}
|
|
close(done)
|
|
}()
|
|
|
|
// 30 minute time out if no connection is received.
|
|
go func() {
|
|
for {
|
|
select {
|
|
case <-timeoutReset:
|
|
case <-time.After(30 * time.Minute):
|
|
close(quit)
|
|
}
|
|
}
|
|
}()
|
|
|
|
// Listen on existing systemd socket.
|
|
server.Serve(listeners[0])
|
|
|
|
// Wait for existing connections befor exiting.
|
|
<-done
|
|
} else {
|
|
// Get the configuration.
|
|
httpBind := app.config.HTTPBind
|
|
httpPort := app.config.HTTPPort
|
|
if app.flags.HTTPBind != "" {
|
|
httpBind = app.flags.HTTPBind
|
|
}
|
|
if app.flags.HTTPPort != 0 {
|
|
httpPort = app.flags.HTTPPort
|
|
}
|
|
|
|
// Start the server.
|
|
log.Println("Starting the http server on port", httpPort)
|
|
err := http.ListenAndServe(fmt.Sprintf("%s:%d", httpBind, httpPort), handler)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
}
|
|
}
|