├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── week-01 ├── .gitignore ├── .npmignore ├── README.md ├── bin │ └── ec2-cdk.ts ├── cdk.json ├── deploy.sh ├── images │ ├── Week-01-Diagram.png │ ├── ZoiperConfig_Advanced.png │ ├── ZoiperConfig_Login.png │ ├── ZoiperConfig_STUN.png │ └── ZoiperConfig_Success.png ├── lib │ └── ec2-cdk-stack.ts ├── package.json ├── src │ ├── config.sh │ └── createVoiceConnector.py └── tsconfig.json ├── week-02 ├── .gitignore ├── .npmignore ├── README.md ├── bin │ └── asterisk-fax-server.ts ├── cdk.json ├── deploy.sh ├── images │ └── Week-02-Diagram-Overview.png ├── lib │ └── asterisk-fax-server-stack.ts ├── package.json ├── src │ ├── config.sh │ ├── createVoiceConnector.py │ └── textract.py └── tsconfig.json ├── week-03 ├── .gitignore ├── .npmignore ├── README.md ├── bin │ └── asterisk_cdr.ts ├── cdk.json ├── deploy.sh ├── images │ └── Week-03-Diagram.png ├── lib │ └── asterisk_cdr-stack.ts ├── package.json ├── src │ ├── config.sh │ ├── createVoiceConnector.py │ └── process.js └── tsconfig.json ├── week-04 ├── .gitignore ├── README.md ├── bin │ └── asterisk_parsing.ts ├── cdk.json ├── deploy.sh ├── images │ └── Week-04-Diagram.png ├── lib │ └── asterisk_parsing.ts ├── package.json ├── src │ ├── config.sh │ ├── createVoiceConnector.py │ ├── process_cdrs.js │ └── process_logs.py └── tsconfig.json ├── week-05 ├── .gitignore ├── .npmignore ├── README.md ├── SBC_Config │ ├── SBC_Template.ini │ └── buildSBCConfig.js ├── bin │ └── chime-with-sbc.ts ├── cdk.context.json ├── cdk.json ├── deploy.sh ├── images │ ├── ConnectVPN.png │ ├── ImportProfile.png │ ├── SBC_Configuration.png │ └── Week-05-Overview.png ├── lib │ └── chime-with-sbc.ts ├── package.json ├── src │ ├── config.sh │ └── createVoiceConnector.py └── tsconfig.json └── week-06 ├── .gitignore ├── .npmignore ├── README.md ├── SBC_Config ├── SBC_Template.ini └── buildSBCConfig.js ├── bin └── siprec-chime-with-sbc.ts ├── cdk.context.json ├── cdk.json ├── deploy.sh ├── images └── Week-06-Overview.png ├── lib └── siprec-chime-with-sbc.ts ├── package.json ├── src ├── config.sh └── createVoiceConnector.py └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | !jest.config.js 3 | *.d.ts 4 | node_modules 5 | 6 | # CDK asset staging directory 7 | .cdk.staging 8 | cdk.out 9 | 10 | package-lock.json 11 | 12 | !SBC_Config/buildSBCConfig.js 13 | cdk-outputs.json 14 | SBC_Config.ini 15 | 16 | *.ovpn 17 | *.pem 18 | *.drawio 19 | 20 | .DS_Store 21 | 22 | yarn.lock -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *main* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 10 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 11 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 12 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 13 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 14 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | This repository will hold the demos that have been shown on the [AWS Twitch](https://www.twitch.tv/aws) stream. Building with Chime airs every every Monday at 1 Pacific. Be sure to tune in to see the latest demos. 4 | 5 | ## Hosts 6 | 7 | - Joe Trelli - [Twitter](https://twitter.com/G_722audio) 8 | - Court Schuett - [Twitter](https://twitter.com/schuettc5061) 9 | 10 | ## Agenda 11 | 12 | ### [Week One](https://github.com/aws-samples/building-with-amazon-chime/tree/main/week-01) 13 | In this episode we will be building an Asterisk server on EC2 and connecting it to an Amazon Chime Voice Connector so that you can make a call. 14 | 15 | [Twitch Video](https://www.twitch.tv/videos/1018106494) 16 | ### [Week Two](https://github.com/aws-samples/building-with-amazon-chime/tree/main/week-02) 17 | In this episode we will be using the base Asterisk server we built in Week One and expanding it with fax support and then tying that to additional AWS services to process that fax. 18 | 19 | [Twitch Video](https://www.twitch.tv/videos/1026116160) 20 | 21 | ### [Week Three](https://github.com/aws-samples/building-with-amazon-chime/tree/main/week-03) 22 | In this epsidode we'll work through some common troubleshooting scenarios and how to get PCAPs from an EC2 instance. 23 | [Twitch Video](https://www.twitch.tv/videos/1033973484) 24 | 25 | ### [Week Four](https://github.com/aws-samples/building-with-amazon-chime/tree/main/week-04) 26 | In this epsiode we'll explore the how to process CDRs and how to extract metadata from a SIP INVITE using CLoudWatch Logs and Lambdas. 27 | [Twitch Video](https://www.twitch.tv/videos/1033973484) 28 | 29 | ### [Week Five](https://github.com/aws-samples/building-with-amazon-chime/tree/main/week-05) 30 | In this episode we'll deploy and configure an AudioCode SBC in conjunction with an Asterisk and use a VPN to connect to the Asterisk. 31 | [Twitch Video](https://www.twitch.tv/videos/1056241310) 32 | 33 | ### [Week Six](https://github.com/aws-samples/building-with-amazon-chime/tree/main/week-06) 34 | In this episode, we'll build off of the previous week by adding in a KVS streaming option to the SBC to enable SIPREC recording. 35 | [Twitch Video](https://www.twitch.tv/videos/1063314124) 36 | 37 | ### Week Seven 38 | Recap episode featuring Dino. 39 | [Twitch Video](https://www.twitch.tv/videos/1070548077) 40 | ## Important Note 41 | Amazon Chime Voice Connector phone number ordering requires an approriately aged account. If you are getting an error when trying to order a number within your account, please open a ticket to resolve. 42 | 43 | ## Questions 44 | 45 | If you have any questions about what was discussed or deployments, feel free to ask here or on Twitter. If there is something Amazon Chime related you'd like to see, please let us know. 46 | -------------------------------------------------------------------------------- /week-01/.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | *.d.ts 3 | node_modules 4 | *.drawio 5 | 6 | # CDK asset staging directory 7 | .cdk.staging 8 | cdk.out 9 | 10 | *.pem 11 | 12 | package-lock.json 13 | 14 | .DS_Store 15 | -------------------------------------------------------------------------------- /week-01/.npmignore: -------------------------------------------------------------------------------- 1 | *.ts 2 | !*.d.ts 3 | 4 | # CDK asset staging directory 5 | .cdk.staging 6 | cdk.out 7 | -------------------------------------------------------------------------------- /week-01/README.md: -------------------------------------------------------------------------------- 1 | # Chime Voice Connector with Asterisk Demo 2 | 3 | This demo will build and configure several services and servers within AWS so that you can make a phone call using Amazon Chime Voice Connector and an Asterisk server hosted in AWS EC2. 4 | ## Overview 5 | 6 | ![Diagram](images/Week-01-Diagram.png) 7 | 8 | ## Requirements 9 | - node/npm [installed](https://www.npmjs.com/get-npm) 10 | - AWS CLI [installed](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) 11 | - AWS CLI [configured](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-quickstart.html) 12 | - AWS CDK [installed](https://docs.aws.amazon.com/cdk/latest/guide/getting_started.html#getting_started_install) 13 | - `npm install -g aws-cdk` 14 | - AWS CDK [bootstrapped](https://docs.aws.amazon.com/cdk/latest/guide/bootstrapping.html) 15 | - `cdk bootstrap aws://123456789012/us-east-1` 16 | - Ability to create a VPC and EIP within that VPC (ensure your [Service Quota](https://console.aws.amazon.com/servicequotas/) for EIP is not reached) 17 | - Ability to create a Chime Voice Connector and Phone Numbers (ensure your Service Quota for VC and Phone Numbers have not been reached) 18 | 19 | ## Deployment 20 | 21 | - Clone this repo: `git clone https://github.com/aws-samples/building-with-amazon-chime.git` 22 | - `cd building-with-amazon-chime/week-01/` 23 | - `chmod +x deploy.sh` 24 | - `./deploy.sh` 25 | 26 | This script will ensure that all dependencies are installed and ready for the CDK deployment. It will also try to get your external IP address. This will be used to create a Security Group in the VPC to allow traffic from your network to reach the Asterisk server. This is similar to a Security Group with 'My IP' selected. 27 | 28 | If curl is installed and available, this will be determined automatically. If not, you will be able to input your external IP address manually. 29 | 30 | If you get a schema related error, please uninstall and re-install aws-cdk to get to the latest version. 31 | ``` 32 | npm uninstall -g aws-cdk 33 | npm install -g aws-cdk 34 | ``` 35 | 36 | ## Connecting to the Asterisk Server 37 | 38 | The output of the CDK will include several commands that you will use to connect to your Asterisk server. There is no requirement to connect to the Asterisk server, but may be of interest. 39 | 40 | The CDK will create a key-pair and store the private key in AWS SecretsManager. The DownloadKeyCommand will download this file to your local machine. The public key of this key-pair has already been loaded in the Asterisk server. The sshcommand will then ssh to your Asterisk server. 41 | 42 | # Configuring a Client 43 | 44 | For this demo, I used [Zoiper](https://www.zoiper.com/) to register to the Asterisk server and make calls. Zoiper is not required, but examples for configuring it are below. Another client could be used with similar configurations. 45 | 46 | ## Login Screen 47 | ![Login Screen](images/ZoiperConfig_Login.png) 48 | 49 | The PhoneNumber and IPAddress to be used are part of the output and should be copied exactly. The top box will look like: `+12125551234@192.0.2.23` The password is `ChimeDemo` 50 | 51 | The next screen will confirm the hostname of the server to connect to. This is the IPAddress and should be filled in already. Skip the Authentication and Outbound Proxy. 52 | 53 | ## Success 54 | 55 | At this point, a UDP connection should be found and you should see this 56 | 57 | ![Success](images/ZoiperConfig_Success.png) 58 | 59 | ## Making A Call 60 | 61 | At this point, you should be able to make a call to this number or from this number. Be sure to dial a full E.164 number when making a call. It should look like: `+12125551212`. Try it out! 62 | 63 | ## Asterisk Basics 64 | 65 | From terminal of your EC2 Instance: 66 | | Command | Use | 67 | |---|---| 68 | | `sudo bash` | Will allow you to operate as root | 69 | | `cat /var/log/cloud-init-output.log` | View output of initial install script | 70 | | `systemctl status asterisk` | Check the status of asterisk | 71 | | `asterisk -crvvvvv` | Access Asterisk console | 72 | 73 | From the Asterisk console: 74 | | Command | Use | 75 | |---|---| 76 | | `pjsip show endpoints` | List the pjsip endpoints | 77 | | `pjsip set logger on` | Enable pjsip logging | 78 | | `core reload` | Reload the configuration files | 79 | 80 | Key Asterisk Config Files in the `/etc/asterisk` directory: 81 | | File | Function | 82 | | ----| --- | 83 | | pjsip.conf | Configuration of endpoints | 84 | | extensions.conf | Configuration of call routing | 85 | 86 | # Destroying This Install 87 | 88 | To clean up after you're done with this demo, you can run `cdk destroy`. This will remove most of the components that were created, but you will need to remove the Voice Connector and Phone Number associated manually within the AWS [Chime Console](https://console.chime.aws.amazon.com/). 89 | 90 | 91 | -------------------------------------------------------------------------------- /week-01/bin/ec2-cdk.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com Inc. or its affiliates. 2 | 3 | import * as cdk from '@aws-cdk/core'; 4 | import { Ec2CdkStack } from '../lib/ec2-cdk-stack'; 5 | 6 | const app = new cdk.App(); 7 | 8 | const SecurityGroupIP = app.node.tryGetContext('SecurityGroupIP') 9 | 10 | new Ec2CdkStack(app, 'Ec2CdkStack', { 11 | SecurityGroupIP: SecurityGroupIP 12 | }); 13 | -------------------------------------------------------------------------------- /week-01/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node --prefer-ts-exts bin/ec2-cdk.ts", 3 | "context": { 4 | "@aws-cdk/core:enableStackNameDuplicates": "true", 5 | "aws-cdk:enableDiffNoFail": "true", 6 | "@aws-cdk/core:stackRelativeExports": "true", 7 | "@aws-cdk/aws-ecr-assets:dockerIgnoreSupport": true, 8 | "@aws-cdk/aws-secretsmanager:parseOwnedSecretName": true, 9 | "@aws-cdk/aws-kms:defaultKeyPolicies": true, 10 | "@aws-cdk/aws-s3:grantWriteWithoutAcl": true, 11 | "@aws-cdk/aws-ecs-patterns:removeDefaultDesiredCount": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /week-01/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ## Copyright Amazon.com Inc. or its affiliates. 3 | function valid_ip() 4 | { 5 | local ip=$1 6 | local stat=1 7 | 8 | if [[ $ip =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then 9 | OIFS=$IFS 10 | IFS='.' 11 | ip=($ip) 12 | IFS=$OIFS 13 | [[ ${ip[0]} -le 255 && ${ip[1]} -le 255 \ 14 | && ${ip[2]} -le 255 && ${ip[3]} -le 255 ]] 15 | stat=$? 16 | fi 17 | return $stat 18 | } 19 | ExternalIP='' 20 | 21 | if [ -f "cdk.context.json" ]; then 22 | echo "" 23 | echo "INFO: Removing cdk.context.json" 24 | rm cdk.context.json 25 | else 26 | echo "" 27 | echo "INFO: cdk.context.json not present, nothing to remove" 28 | fi 29 | if [ ! -f "package-lock.json" ]; then 30 | echo "" 31 | echo "Installing Packages" 32 | echo "" 33 | npm install 34 | fi 35 | echo "" 36 | echo "Getting External IP address for Security Group" 37 | echo "" 38 | if [ -x "$(which curl)" ] ; then 39 | echo "Using Curl" 40 | ExternalIP=$( curl checkip.amazonaws.com ) 41 | echo "External IP: " $ExternalIP 42 | else 43 | while true; do 44 | read -p "Enter IPv4 address to allow access in Security Group: " ExternalIP 45 | if valid_ip $ExternalIP; then 46 | break 47 | else 48 | echo "Please enter a valid IPv4 address in the form of XXX.XXX.XXX.XXX" 49 | fi 50 | done 51 | fi 52 | echo "" 53 | echo "Building CDK" 54 | echo "" 55 | npm run build 56 | echo "" 57 | echo "Deploying CDK" 58 | echo "" 59 | cdk deploy -c SecurityGroupIP=$ExternalIP -------------------------------------------------------------------------------- /week-01/images/Week-01-Diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/building-with-amazon-chime/969c1c7a36fd7e4e982922290a05ec9ad5b420fc/week-01/images/Week-01-Diagram.png -------------------------------------------------------------------------------- /week-01/images/ZoiperConfig_Advanced.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/building-with-amazon-chime/969c1c7a36fd7e4e982922290a05ec9ad5b420fc/week-01/images/ZoiperConfig_Advanced.png -------------------------------------------------------------------------------- /week-01/images/ZoiperConfig_Login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/building-with-amazon-chime/969c1c7a36fd7e4e982922290a05ec9ad5b420fc/week-01/images/ZoiperConfig_Login.png -------------------------------------------------------------------------------- /week-01/images/ZoiperConfig_STUN.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/building-with-amazon-chime/969c1c7a36fd7e4e982922290a05ec9ad5b420fc/week-01/images/ZoiperConfig_STUN.png -------------------------------------------------------------------------------- /week-01/images/ZoiperConfig_Success.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/building-with-amazon-chime/969c1c7a36fd7e4e982922290a05ec9ad5b420fc/week-01/images/ZoiperConfig_Success.png -------------------------------------------------------------------------------- /week-01/lib/ec2-cdk-stack.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com Inc. or its affiliates. 2 | 3 | import * as ec2 from "@aws-cdk/aws-ec2"; 4 | import * as cdk from '@aws-cdk/core'; 5 | import { KeyPair } from 'cdk-ec2-key-pair'; 6 | import { Asset } from '@aws-cdk/aws-s3-assets'; 7 | import * as path from 'path'; 8 | import * as ssm from '@aws-cdk/aws-ssm'; 9 | import * as iam from '@aws-cdk/aws-iam'; 10 | import { CustomResource, Duration } from '@aws-cdk/core'; 11 | import lambda = require('@aws-cdk/aws-lambda'); 12 | import custom = require('@aws-cdk/custom-resources') 13 | 14 | export interface StackProps { 15 | SecurityGroupIP: string; 16 | } 17 | export class Ec2CdkStack extends cdk.Stack { 18 | constructor(scope: cdk.Construct, id: string, props: StackProps) { 19 | super(scope, id); 20 | 21 | // Create a Key Pair to be used with this EC2 Instance 22 | const key = new KeyPair(this, 'KeyPair', { 23 | name: 'cdk-keypair', 24 | description: 'Key Pair created with CDK Deployment', 25 | }); 26 | key.grantReadOnPublicKey 27 | 28 | // Create new VPC with 2 Subnets 29 | const vpc = new ec2.Vpc(this, 'VPC', { 30 | natGateways: 0, 31 | subnetConfiguration: [ { 32 | cidrMask: 24, 33 | name: "asterisk", 34 | subnetType: ec2.SubnetType.PUBLIC 35 | }]}); 36 | 37 | // Allow SSH (TCP Port 22) access from anywhere 38 | const securityGroup = new ec2.SecurityGroup(this, 'SecurityGroup', { 39 | vpc, 40 | description: 'Security Group for Asterisk Server', 41 | allowAllOutbound: true 42 | }); 43 | securityGroup.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(22), 'Allow SSH Access') 44 | securityGroup.addIngressRule(ec2.Peer.ipv4('3.80.16.0/23'), ec2.Port.udp(5060), 'Allow Chime Voice Connector Signaling Access') 45 | securityGroup.addIngressRule(ec2.Peer.ipv4('3.80.16.0/23'), ec2.Port.tcp(5060), 'Allow Chime Voice Connector Signaling Access') 46 | securityGroup.addIngressRule(ec2.Peer.ipv4('3.80.16.0/23'), ec2.Port.tcp(5061), 'Allow Chime Voice Connector Signaling Access') 47 | securityGroup.addIngressRule(ec2.Peer.ipv4('99.77.253.0/24'), ec2.Port.udp(5060), 'Allow Chime Voice Connector Signaling Access') 48 | securityGroup.addIngressRule(ec2.Peer.ipv4('99.77.253.0/24'), ec2.Port.tcp(5060), 'Allow Chime Voice Connector Signaling Access') 49 | securityGroup.addIngressRule(ec2.Peer.ipv4('99.77.253.0/24'), ec2.Port.tcp(5061), 'Allow Chime Voice Connector Signaling Access') 50 | securityGroup.addIngressRule(ec2.Peer.ipv4('99.77.253.0/24'), ec2.Port.udpRange(5000,65000), 'Allow Chime Voice Connector Signaling Access') 51 | securityGroup.addIngressRule(ec2.Peer.ipv4('3.80.16.0/23'), ec2.Port.udpRange(5000,65000), 'Allow Chime Voice Connector Media Access') 52 | securityGroup.addIngressRule(ec2.Peer.ipv4('99.77.253.0/24'), ec2.Port.udpRange(5000,65000), 'Allow Chime Voice Connector Media Access') 53 | securityGroup.addIngressRule(ec2.Peer.ipv4('52.55.62.128/25'), ec2.Port.udpRange(1024,65535), 'Allow Chime Voice Connector Media Access') 54 | securityGroup.addIngressRule(ec2.Peer.ipv4('52.55.63.0/25'), ec2.Port.udpRange(1024,65535), 'Allow Chime Voice Connector Media Access') 55 | securityGroup.addIngressRule(ec2.Peer.ipv4('34.212.95.128/25'), ec2.Port.udpRange(1024,65535), 'Allow Chime Voice Connector Media Access') 56 | securityGroup.addIngressRule(ec2.Peer.ipv4('34.223.21.0/25'), ec2.Port.udpRange(1024,65535), 'Allow Chime Voice Connector Media Access') 57 | securityGroup.addIngressRule(ec2.Peer.ipv4(props.SecurityGroupIP + '/32'), ec2.Port.allTraffic(), 'All inbound traffic from local machine') 58 | 59 | const role = new iam.Role(this, 'ec2Role', { 60 | assumedBy: new iam.ServicePrincipal('ec2.amazonaws.com') 61 | }) 62 | 63 | role.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonSSMManagedInstanceCore')) 64 | 65 | const eip = new ec2.CfnEIP(this, 'EIP') 66 | 67 | // Use Latest Amazon Linux Image - CPU Type ARM64 68 | const ami = new ec2.AmazonLinuxImage({ 69 | generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2, 70 | cpuType: ec2.AmazonLinuxCpuType.ARM_64}); 71 | 72 | const createVoiceConnectorRole = new iam.Role(this, 'createChimeLambdaRole', { 73 | assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), 74 | inlinePolicies: { 75 | ['chimePolicy']: new iam.PolicyDocument( { statements: [new iam.PolicyStatement({ 76 | resources: ['*'], 77 | actions: ['chime:*', 78 | 'lambda:*']})]}) 79 | }, 80 | managedPolicies: [ iam.ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSLambdaBasicExecutionRole") ] 81 | }) 82 | 83 | const createVoiceConnectorLambda = new lambda.Function(this, 'createVCLambda', { 84 | code: lambda.Code.fromAsset("src", {exclude: ["**", "!createVoiceConnector.py"]}), 85 | handler: 'createVoiceConnector.on_event', 86 | runtime: lambda.Runtime.PYTHON_3_8, 87 | role: createVoiceConnectorRole, 88 | timeout: Duration.seconds(60) 89 | }); 90 | 91 | const voiceConnectorProvider = new custom.Provider(this, 'voiceConnectorProvider', { 92 | onEventHandler: createVoiceConnectorLambda 93 | }) 94 | 95 | const voiceConnectorResource = new CustomResource(this, 'voiceConnectorResource', { 96 | serviceToken: voiceConnectorProvider.serviceToken, 97 | properties: { 'region': this.region, 98 | 'eip': eip.ref } 99 | }) 100 | 101 | const phoneNumber = voiceConnectorResource.getAttString('phoneNumber') 102 | const voiceConnectorId = voiceConnectorResource.getAttString('voiceConnectorId') 103 | const outboundHostName = voiceConnectorResource.getAttString('outboundHostName') 104 | 105 | const phoneNumberParameter = new ssm.StringParameter(this, 'phoneNumber', { 106 | parameterName: '/asterisk/phoneNumber', 107 | stringValue: phoneNumber, 108 | }); 109 | 110 | const voiceConnectorParameter = new ssm.StringParameter(this, 'voiceConnector', { 111 | parameterName: '/asterisk/voiceConnector', 112 | stringValue: voiceConnectorId 113 | }) 114 | 115 | const outboundHostNameParameter = new ssm.StringParameter(this, 'outboundHostName', { 116 | parameterName: '/asterisk/outboundHostName', 117 | stringValue: outboundHostName 118 | }) 119 | 120 | const ec2UserData = ec2.UserData.forLinux(); 121 | 122 | const asteriskConfig = new Asset(this, 'AsteriskConfig', {path: path.join(__dirname, '../src/config.sh')}); 123 | 124 | const configPath = ec2UserData.addS3DownloadCommand({ 125 | bucket:asteriskConfig.bucket, 126 | bucketKey:asteriskConfig.s3ObjectKey, 127 | }); 128 | 129 | ec2UserData.addExecuteFileCommand({ 130 | filePath:configPath, 131 | arguments: '--verbose -y' 132 | }); 133 | 134 | asteriskConfig.grantRead(role); 135 | phoneNumberParameter.grantRead(role); 136 | voiceConnectorParameter.grantRead(role); 137 | outboundHostNameParameter.grantRead(role); 138 | 139 | // Create the instance using the Security Group, AMI, and KeyPair defined in the VPC created 140 | const ec2Instance = new ec2.Instance(this, 'Instance', { 141 | vpc, 142 | instanceType: ec2.InstanceType.of(ec2.InstanceClass.T4G, ec2.InstanceSize.LARGE), 143 | machineImage: ami, 144 | securityGroup: securityGroup, 145 | keyName: key.keyPairName, 146 | role: role, 147 | userData: ec2UserData, 148 | }); 149 | 150 | new ec2.CfnEIPAssociation(this, "EIP Association", { 151 | eip: eip.ref, 152 | instanceId: ec2Instance.instanceId 153 | }) 154 | 155 | 156 | // Create outputs for connecting 157 | new cdk.CfnOutput(this, 'IP Address', { value: ec2Instance.instancePublicIp }); 158 | new cdk.CfnOutput(this, 'Instance ID', { value: ec2Instance.instanceId }) 159 | new cdk.CfnOutput(this, 'DownloadKeyCommand', { value: 'aws secretsmanager get-secret-value --secret-id ec2-ssh-key/cdk-keypair/private --query SecretString --output text > cdk-key.pem && chmod 400 cdk-key.pem' }) 160 | new cdk.CfnOutput(this, 'sshCommand', { value: 'ssh -i cdk-key.pem -o IdentitiesOnly=yes ec2-user@' + ec2Instance.instancePublicIp }) 161 | new cdk.CfnOutput(this, 'PhoneNumber', { value: phoneNumber}), 162 | new cdk.CfnOutput(this, 'VoiceConnector', { value: outboundHostName}) 163 | } 164 | } -------------------------------------------------------------------------------- /week-01/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ec2-cdk", 3 | "version": "0.1.0", 4 | "bin": { 5 | "ec2-cdk": "bin/ec2-cdk.js" 6 | }, 7 | "scripts": { 8 | "build": "tsc", 9 | "watch": "tsc -w", 10 | "test": "jest", 11 | "cdk": "cdk" 12 | }, 13 | "devDependencies": { 14 | "@aws-cdk/assert": "^1.100.0", 15 | "@types/jest": "^26.0.22", 16 | "@types/node": "14.14.41", 17 | "aws-cdk": "^1.100.0", 18 | "jest": "^26.6.3", 19 | "ts-jest": "^26.5.5", 20 | "ts-node": "^9.1.1", 21 | "typescript": "~4.2.4" 22 | }, 23 | "dependencies": { 24 | "@aws-cdk/aws-ec2": "^1.100.0", 25 | "@aws-cdk/core": "^1.100.0", 26 | "@aws-cdk/custom-resources": "^1.100.0", 27 | "cdk-ec2-key-pair": "^2.1.1", 28 | "source-map-support": "^0.5.19" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /week-01/src/config.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -xe 2 | ## Copyright Amazon.com Inc. or its affiliates. 3 | HOMEDIR=/home/ec2-user 4 | yum update -y 5 | yum install net-tools -y 6 | yum install wget -y 7 | yum -y install make gcc gcc-c++ make subversion libxml2-devel ncurses-devel openssl-devel vim-enhanced man glibc-devel autoconf libnewt kernel-devel kernel-headers linux-headers openssl-devel zlib-devel libsrtp libsrtp-devel uuid libuuid-devel mariadb-server jansson-devel libsqlite3x libsqlite3x-devel epel-release.noarch bash-completion bash-completion-extras unixODBC unixODBC-devel libtool-ltdl libtool-ltdl-devel mysql-connector-odbc mlocate libiodbc sqlite sqlite-devel sql-devel.i686 sqlite-doc.noarch sqlite-tcl.x86_64 patch libedit-devel jq 8 | cd /tmp 9 | wget https://downloads.asterisk.org/pub/telephony/asterisk/asterisk-16-current.tar.gz 10 | tar xvzf asterisk-16-current.tar.gz 11 | cd asterisk-16*/ 12 | ./configure --libdir=/usr/lib64 --with-jansson-bundled 13 | make menuselect.makeopts 14 | menuselect/menuselect \ 15 | --disable BUILD_NATIVE \ 16 | --disable chan_sip \ 17 | --disable chan_skinny \ 18 | --enable cdr_csv \ 19 | --enable res_snmp \ 20 | --enable res_http_websocket \ 21 | menuselect.makeopts 22 | make 23 | make install 24 | make basic-pbx 25 | touch /etc/redhat-release 26 | make config 27 | ldconfig 28 | 29 | IP=$( curl http://169.254.169.254/latest/meta-data/public-ipv4 ) 30 | REGION=$( curl http://169.254.169.254/latest/meta-data/placement/region ) 31 | PhoneNumber=$( aws ssm get-parameter --name /asterisk/phoneNumber --region $REGION | jq -r '.Parameter.Value' ) 32 | VoiceConnectorHost=$( aws ssm get-parameter --name /asterisk/voiceConnector --region $REGION | jq -r '.Parameter.Value' ) 33 | OutboundHostName=$( aws ssm get-parameter --name /asterisk/outboundHostName --region $REGION | jq -r '.Parameter.Value' ) 34 | 35 | echo "[udp] 36 | type=transport 37 | protocol=udp 38 | bind=0.0.0.0 39 | external_media_address=$IP 40 | external_signaling_address=$IP 41 | allow_reload=yes 42 | 43 | [VoiceConnector] 44 | type=endpoint 45 | context=from-voiceConnector 46 | transport=udp 47 | disallow=all 48 | allow=ulaw 49 | aors=VoiceConnector 50 | direct_media=no 51 | ice_support=yes 52 | force_rport=yes 53 | 54 | [VoiceConnector] 55 | type=identify 56 | endpoint=VoiceConnector 57 | match=$OutboundHostName 58 | 59 | [VoiceConnector] 60 | type=aor 61 | contact=sip:$OutboundHostName 62 | 63 | [$PhoneNumber] 64 | type=endpoint 65 | context=from-phone 66 | disallow=all 67 | allow=ulaw 68 | transport=udp 69 | auth=$PhoneNumber 70 | aors=$PhoneNumber 71 | send_pai=yes 72 | direct_media=no 73 | rewrite_contact=yes 74 | ice_support=yes 75 | force_rport=yes 76 | 77 | [$PhoneNumber] 78 | type=auth 79 | auth_type=userpass 80 | password=ChimeDemo 81 | username=$PhoneNumber 82 | 83 | [$PhoneNumber] 84 | type=aor 85 | max_contacts=5" > /etc/asterisk/pjsip.conf 86 | 87 | echo "; extensions.conf - the Asterisk dial plan 88 | ; 89 | [general] 90 | static=yes 91 | writeprotect=no 92 | clearglobalvars=no 93 | 94 | [catch-all] 95 | exten => _[+0-9].,1,Answer() 96 | exten => _[+0-9].,n,Wait(1) 97 | exten => _[+0-9].,n,Playback(hello-world) 98 | exten => _[+0-9].,n,Wait(1) 99 | exten => _[+0-9].,n,echo() 100 | exten => _[+0-9].,n,Wait(1) 101 | exten => _[+0-9].,n,Hangup() 102 | 103 | [from-phone] 104 | include => outbound_phone 105 | 106 | [outbound_phone] 107 | exten => _+X.,1,NoOP(Outbound Normal) 108 | same => n,Dial(PJSIP/\${EXTEN}@VoiceConnector,20) 109 | same => n,Congestion 110 | 111 | [from-voiceConnector] 112 | include => phones 113 | include => catch-all 114 | 115 | [phones] 116 | exten => $PhoneNumber,1,Dial(PJSIP/$PhoneNumber)" > /etc/asterisk/extensions.conf 117 | 118 | echo "[options] 119 | runuser = asterisk 120 | rungroup = asterisk" > /etc/asterisk/asterisk.conf 121 | 122 | echo "[general] 123 | [logfiles] 124 | console = verbose,notice,warning,error 125 | messages = notice,warning,error" > /etc/asterisk/logger.conf 126 | 127 | groupadd asterisk 128 | useradd -r -d /var/lib/asterisk -g asterisk asterisk 129 | usermod -aG audio,dialout asterisk 130 | chown -R asterisk.asterisk /etc/asterisk 131 | chown -R asterisk.asterisk /var/{lib,log,spool}/asterisk 132 | 133 | systemctl start asterisk 134 | -------------------------------------------------------------------------------- /week-01/src/createVoiceConnector.py: -------------------------------------------------------------------------------- 1 | ## Copyright Amazon.com Inc. or its affiliates. 2 | 3 | import json 4 | import boto3 5 | import time 6 | import uuid 7 | 8 | chime = boto3.client('chime') 9 | 10 | def authorizeEIP (voiceConnectorId, elasticIP): 11 | response = chime.put_voice_connector_origination( 12 | VoiceConnectorId=voiceConnectorId, 13 | Origination={ 14 | 'Routes': [ 15 | { 16 | 'Host': elasticIP, 17 | 'Port': 5060, 18 | 'Protocol': 'UDP', 19 | 'Priority': 1, 20 | 'Weight': 1 21 | }, 22 | ], 23 | 'Disabled': False 24 | } 25 | ) 26 | print(response) 27 | 28 | response = chime.put_voice_connector_termination( 29 | VoiceConnectorId=voiceConnectorId, 30 | Termination={ 31 | 'CpsLimit': 1, 32 | 'CallingRegions': [ 33 | 'US', 34 | ], 35 | 'CidrAllowedList': [ 36 | elasticIP + '/32', 37 | ], 38 | 'Disabled': False 39 | } 40 | ) 41 | print(response) 42 | 43 | 44 | def getPhoneNumber (): 45 | search_response = chime.search_available_phone_numbers( 46 | # AreaCode='string', 47 | # City='string', 48 | # Country='string', 49 | State='IL', 50 | # TollFreePrefix='string', 51 | MaxResults=1 52 | ) 53 | phoneNumberToOrder = search_response['E164PhoneNumbers'][0] 54 | print ('Phone Number: ' + phoneNumberToOrder) 55 | phone_order = chime.create_phone_number_order( 56 | ProductType='VoiceConnector', 57 | E164PhoneNumbers=[ 58 | phoneNumberToOrder, 59 | ] 60 | ) 61 | print ('Phone Order: ' + str(phone_order)) 62 | 63 | check_phone_order = chime.get_phone_number_order( 64 | PhoneNumberOrderId=phone_order['PhoneNumberOrder']['PhoneNumberOrderId'] 65 | ) 66 | order_status = check_phone_order['PhoneNumberOrder']['Status'] 67 | timeout = 0 68 | 69 | while not order_status == 'Successful': 70 | timeout += 1 71 | print('Checking status: ' + str(order_status)) 72 | time.sleep(5) 73 | check_phone_order = chime.get_phone_number_order( 74 | PhoneNumberOrderId=phone_order['PhoneNumberOrder']['PhoneNumberOrderId'] 75 | ) 76 | order_status = check_phone_order['PhoneNumberOrder']['Status'] 77 | if timeout == 5: 78 | return 'Could not get phone number' 79 | 80 | return phoneNumberToOrder 81 | 82 | def createVoiceConnector (region, phoneNumber): 83 | print(str(uuid.uuid1())) 84 | print(region) 85 | response = chime.create_voice_connector( 86 | Name='Trunk' + str(uuid.uuid1()), 87 | AwsRegion=region, 88 | RequireEncryption=False 89 | ) 90 | 91 | voiceConnectorId = response['VoiceConnector']['VoiceConnectorId'] 92 | outboundHostName = response['VoiceConnector']['OutboundHostName'] 93 | 94 | response = chime.associate_phone_numbers_with_voice_connector( 95 | VoiceConnectorId=voiceConnectorId, 96 | E164PhoneNumbers=[ 97 | phoneNumber, 98 | ], 99 | ForceAssociate=True 100 | ) 101 | 102 | voiceConnector = { 'voiceConnectorId': voiceConnectorId, 'outboundHostName': outboundHostName, 'phoneNumber': phoneNumber} 103 | return voiceConnector 104 | 105 | def on_event(event, context): 106 | print(event) 107 | request_type = event['RequestType'] 108 | if request_type == 'Create': return on_create(event) 109 | if request_type == 'Update': return on_update(event) 110 | if request_type == 'Delete': return on_delete(event) 111 | raise Exception("Invalid request type: %s" % request_type) 112 | 113 | def on_create(event): 114 | physical_id = 'VoiceConnectorResources' 115 | region = event['ResourceProperties']['region'] 116 | elasticIP = event['ResourceProperties']['eip'] 117 | 118 | newPhoneNumber = getPhoneNumber() 119 | voiceConnector = createVoiceConnector(region, newPhoneNumber) 120 | authorizeEIP(voiceConnector['voiceConnectorId'], elasticIP) 121 | 122 | return { 'PhysicalResourceId': physical_id, 'Data': voiceConnector } 123 | 124 | def on_update(event): 125 | physical_id = event["PhysicalResourceId"] 126 | props = event["ResourceProperties"] 127 | print("update resource %s with props %s" % (physical_id, props)) 128 | return { 'PhysicalResourceId': physical_id } 129 | 130 | 131 | def on_delete(event): 132 | physical_id = event["PhysicalResourceId"] 133 | print("delete resource %s" % physical_id) 134 | return { 'PhysicalResourceId': physical_id } 135 | -------------------------------------------------------------------------------- /week-01/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2018", 4 | "module": "commonjs", 5 | "lib": ["es2018"], 6 | "declaration": true, 7 | "strict": true, 8 | "noImplicitAny": true, 9 | "strictNullChecks": true, 10 | "noImplicitThis": true, 11 | "alwaysStrict": true, 12 | "noUnusedLocals": false, 13 | "noUnusedParameters": false, 14 | "noImplicitReturns": true, 15 | "noFallthroughCasesInSwitch": false, 16 | "inlineSourceMap": true, 17 | "inlineSources": true, 18 | "experimentalDecorators": true, 19 | "strictPropertyInitialization": false, 20 | "typeRoots": ["./node_modules/@types"] 21 | }, 22 | "exclude": ["cdk.out"] 23 | } 24 | -------------------------------------------------------------------------------- /week-02/.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | *.d.ts 3 | node_modules 4 | 5 | # CDK asset staging directory 6 | .cdk.staging 7 | cdk.out 8 | 9 | .DS_Store 10 | package-lock.json 11 | 12 | *.pem 13 | *.drawio 14 | -------------------------------------------------------------------------------- /week-02/.npmignore: -------------------------------------------------------------------------------- 1 | *.ts 2 | !*.d.ts 3 | 4 | # CDK asset staging directory 5 | .cdk.staging 6 | cdk.out 7 | -------------------------------------------------------------------------------- /week-02/README.md: -------------------------------------------------------------------------------- 1 | # Chime Voice Connector with Faxing Demo 2 | 3 | This demo will build and configure several services and servers within AWS to receive and process a fax. 4 | 5 | ## Requirements 6 | - AWS CLI [installed](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2-linux.html) 7 | - node/npm [installed](https://github.com/nodesource/distributions/blob/master/README.md) 8 | - AWS CDK [installed](https://docs.aws.amazon.com/cdk/latest/guide/getting_started.html) and [bootstrapped](https://docs.aws.amazon.com/cdk/latest/guide/bootstrapping.html) 9 | - Ability to create a VPC and EIP within that VPC (ensure your [Service Quota](https://console.aws.amazon.com/servicequotas/) for EIP is not reached) 10 | - Ability to create a Chime Voice Connector (ensure your Service Quota for VC is not reached) 11 | ## Deployment 12 | 13 | - Clone this repo 14 | - `chmod +x deploy.sh` 15 | - `./deploy.sh` 16 | 17 | This script will ensure that all dependencies are installed and ready for the CDK deployment. If you get a schema related error, please uninstall and re-install aws-cdk to get to the latest version. 18 | ``` 19 | npm uninstall -g aws-cdk 20 | npm install -g aws-cdk 21 | ``` 22 | ## Connecting to the Asterisk Server 23 | 24 | The output of the CDK will include several commands that you will use to connect to your Asterisk server. There is no requirement to connect to the Asterisk server, but may be of interest. 25 | 26 | The CDK will create a key-pair and store the private key in AWS SecretsManager. The DownloadKeyCommand will download this file to your local machine. The public key of this key-pair has already been loaded in the Asterisk server. The sshcommand will then ssh to your Asterisk server. 27 | 28 | ## Overview 29 | ![Week-02-Overview](images/Week-02-Diagram-Overview.png) 30 | ## Changes from Previous Asterisk 31 | 32 | In the previous demo, we created a simple Asterisk server to be used to make phone calls to the PSTN. In this demo, we'll be using many of the same components but making some changes to the Asterisk configuration and adding some new components. 33 | 34 | ``` 35 | yum install inotify-tools -y 36 | yum install ImageMagick ImageMagick-devel -y 37 | wget https://www.soft-switch.org/downloads/spandsp/spandsp-0.0.6.tar.gz 38 | ``` 39 | 40 | In the config.sh deployment script, these three parts were added. 41 | - [inotify-tools](https://github.com/inotify-tools/inotify-tools) is used to monitor a directory for changes. This will be used to monitor for new fax images and upload them to S3 when they arrive 42 | - [ImageMagick](https://imagemagick.org/index.php) is used to convert the .tif file output from the Asterisk server to a .jpg file that can be processed by Amazon Textract 43 | - [spandsp](https://github.com/freeswitch/spandsp) is the library used by Asterisk to process the faxes 44 | 45 | These additions are combined to create a server that will recieve a fax, save the fax to a directory, convert the file, and then upload the file to S3. From there, other services will take over to complete the processing of the fax. 46 | 47 | ``` 48 | [faxnumber] 49 | exten => $PhoneNumber,1,NoOp(Receiving Fax) 50 | same => n,Set(FAXDIRECTORY=/etc/asterisk/incoming_faxes) 51 | same => n,Set(FAXNAME=\${STRFTIME(,,%s)}) 52 | same => n,ReceiveFax(\${FAXDIRECTORY}/\${FAXNAME}.tif) 53 | same => n,NoOp(Fax Finished: \${FAXSTATUS}) 54 | ``` 55 | 56 | In the `extensions.conf` file, these lines will answer the inbound call and save the file to the `/etc/asterisk/incoming_faxes` directory as a .tif file with the filename of the epoch seconds. 57 | 58 | ``` 59 | inotifywait /etc/asterisk/incoming_faxes -e moved_to -e close -e close_write --format '%f' | while read file; do 60 | filename=\"\${file%.*}\" 61 | convert /etc/asterisk/incoming_faxes/\$filename.tif /etc/asterisk/converted_faxes/\$filename.jpg 62 | aws s3 cp /etc/asterisk/converted_faxes/\$filename.jpg s3://$IncomingFaxBucket 63 | done 64 | ``` 65 | 66 | This script will monitor that directory, convert the file from a .tif to a .jpg and then upload it to an S3 bucket. 67 | 68 | ### Sending a Fax 69 | 70 | While not covered in this demo, the Asterisk server has also been configured to send a fax from the Asterisk console: 71 | 72 | ``` 73 | asterisk -crvvvvvv 74 | channel originate LOCAL/@outboundfax extension s@sendfax 75 | ``` 76 | 77 | This will send a fax to the number you replace with from the file `/tmp/faxfile.tif`. 78 | 79 | ## Processing the Fax on AWS 80 | 81 | Once the image of the fax has been uploaded to an S3 bucket, it must still be processed. This is done by using a [Lambda function](https://docs.aws.amazon.com/lambda/latest/dg/welcome.html). When the file is uploaded to S3, the Lambda is automatically [triggered](https://docs.aws.amazon.com/lambda/latest/dg/with-s3.html). 82 | 83 | When this happens, the Lambda will make a request to [Amazon Textract](https://aws.amazon.com/textract/) with this code: 84 | 85 | ``` 86 | bucket = event['Records'][0]['s3']['bucket']['name'] 87 | key = urllib.parse.unquote_plus(event['Records'][0]['s3']['object']['key'], encoding='utf-8') 88 | client = boto3.client('textract') 89 | 90 | #process using S3 object 91 | response = client.detect_document_text( 92 | Document={'S3Object': {'Bucket': bucket, 'Name': key}}) 93 | ``` 94 | 95 | The output from this request can be seen in the [CloudWatch](https://aws.amazon.com/cloudwatch/) logs but could easily be used with other services as needed. 96 | # Destroying This Install 97 | 98 | To clean up after you're done with this demo, you can run `cdk destroy`. This will remove most of the components that were created, but you will need to remove the Voice Connector and Phone Number associated manually within the AWS [Chime Console](https://console.chime.aws.amazon.com/). 99 | 100 | 101 | -------------------------------------------------------------------------------- /week-02/bin/asterisk-fax-server.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import 'source-map-support/register'; 3 | import * as cdk from '@aws-cdk/core'; 4 | import { AsteriskFaxServerStack } from '../lib/asterisk-fax-server-stack'; 5 | 6 | const app = new cdk.App(); 7 | 8 | 9 | new AsteriskFaxServerStack(app, 'AsteriskFaxServerStack'); 10 | -------------------------------------------------------------------------------- /week-02/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node --prefer-ts-exts bin/asterisk-fax-server.ts", 3 | "context": { 4 | "@aws-cdk/core:enableStackNameDuplicates": "true", 5 | "aws-cdk:enableDiffNoFail": "true", 6 | "@aws-cdk/core:stackRelativeExports": "true", 7 | "@aws-cdk/aws-ecr-assets:dockerIgnoreSupport": true, 8 | "@aws-cdk/aws-secretsmanager:parseOwnedSecretName": true, 9 | "@aws-cdk/aws-kms:defaultKeyPolicies": true, 10 | "@aws-cdk/aws-s3:grantWriteWithoutAcl": true, 11 | "@aws-cdk/aws-ecs-patterns:removeDefaultDesiredCount": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /week-02/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | if [ -f "cdk.context.json" ]; then 3 | echo "" 4 | echo "INFO: Removing cdk.context.json" 5 | rm cdk.context.json 6 | else 7 | echo "" 8 | echo "INFO: cdk.context.json not present, nothing to remove" 9 | fi 10 | if [ ! -f "package-lock.json" ]; then 11 | echo "" 12 | echo "Installing Packages" 13 | echo "" 14 | npm install 15 | fi 16 | echo "" 17 | echo "Building CDK" 18 | echo "" 19 | npm run build 20 | echo "" 21 | echo "Deploying CDK" 22 | echo "" 23 | cdk deploy 24 | -------------------------------------------------------------------------------- /week-02/images/Week-02-Diagram-Overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/building-with-amazon-chime/969c1c7a36fd7e4e982922290a05ec9ad5b420fc/week-02/images/Week-02-Diagram-Overview.png -------------------------------------------------------------------------------- /week-02/lib/asterisk-fax-server-stack.ts: -------------------------------------------------------------------------------- 1 | import * as ec2 from "@aws-cdk/aws-ec2"; 2 | import * as cdk from '@aws-cdk/core'; 3 | import { KeyPair } from 'cdk-ec2-key-pair'; 4 | import { Asset } from '@aws-cdk/aws-s3-assets'; 5 | import * as path from 'path'; 6 | import * as ssm from '@aws-cdk/aws-ssm'; 7 | import * as iam from '@aws-cdk/aws-iam'; 8 | import { CustomResource, Duration } from '@aws-cdk/core'; 9 | import lambda = require('@aws-cdk/aws-lambda'); 10 | import custom = require('@aws-cdk/custom-resources') 11 | import s3 = require('@aws-cdk/aws-s3'); 12 | import { S3EventSource } from '@aws-cdk/aws-lambda-event-sources'; 13 | 14 | export class AsteriskFaxServerStack extends cdk.Stack { 15 | constructor(scope: cdk.Construct, id: string) { 16 | super(scope, id); 17 | 18 | // Create a Key Pair to be used with this EC2 Instance 19 | const key = new KeyPair(this, 'KeyPair', { 20 | name: 'cdk-keypair', 21 | description: 'Key Pair created with CDK Deployment', 22 | }); 23 | key.grantReadOnPublicKey 24 | 25 | // Create new VPC with 2 Subnets 26 | const vpc = new ec2.Vpc(this, 'VPC', { 27 | natGateways: 0, 28 | subnetConfiguration: [ { 29 | cidrMask: 24, 30 | name: "asterisk", 31 | subnetType: ec2.SubnetType.PUBLIC 32 | }]}); 33 | 34 | // Allow SSH (TCP Port 22) access from anywhere 35 | const securityGroup = new ec2.SecurityGroup(this, 'SecurityGroup', { 36 | vpc, 37 | description: 'Security Group for Asterisk Server', 38 | allowAllOutbound: true 39 | }); 40 | securityGroup.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(22), 'Allow SSH Access') 41 | securityGroup.addIngressRule(ec2.Peer.ipv4('3.80.16.0/23'), ec2.Port.udp(5060), 'Allow Chime Voice Connector Signaling Access') 42 | securityGroup.addIngressRule(ec2.Peer.ipv4('3.80.16.0/23'), ec2.Port.tcp(5060), 'Allow Chime Voice Connector Signaling Access') 43 | securityGroup.addIngressRule(ec2.Peer.ipv4('3.80.16.0/23'), ec2.Port.tcp(5061), 'Allow Chime Voice Connector Signaling Access') 44 | securityGroup.addIngressRule(ec2.Peer.ipv4('99.77.253.0/24'), ec2.Port.udp(5060), 'Allow Chime Voice Connector Signaling Access') 45 | securityGroup.addIngressRule(ec2.Peer.ipv4('99.77.253.0/24'), ec2.Port.tcp(5060), 'Allow Chime Voice Connector Signaling Access') 46 | securityGroup.addIngressRule(ec2.Peer.ipv4('99.77.253.0/24'), ec2.Port.tcp(5061), 'Allow Chime Voice Connector Signaling Access') 47 | securityGroup.addIngressRule(ec2.Peer.ipv4('99.77.253.0/24'), ec2.Port.udpRange(5000,65000), 'Allow Chime Voice Connector Signaling Access') 48 | securityGroup.addIngressRule(ec2.Peer.ipv4('3.80.16.0/23'), ec2.Port.udpRange(5000,65000), 'Allow Chime Voice Connector Media Access') 49 | securityGroup.addIngressRule(ec2.Peer.ipv4('99.77.253.0/24'), ec2.Port.udpRange(5000,65000), 'Allow Chime Voice Connector Media Access') 50 | securityGroup.addIngressRule(ec2.Peer.ipv4('52.55.62.128/25'), ec2.Port.udpRange(1024,65535), 'Allow Chime Voice Connector Media Access') 51 | securityGroup.addIngressRule(ec2.Peer.ipv4('52.55.63.0/25'), ec2.Port.udpRange(1024,65535), 'Allow Chime Voice Connector Media Access') 52 | securityGroup.addIngressRule(ec2.Peer.ipv4('34.212.95.128/25'), ec2.Port.udpRange(1024,65535), 'Allow Chime Voice Connector Media Access') 53 | securityGroup.addIngressRule(ec2.Peer.ipv4('34.223.21.0/25'), ec2.Port.udpRange(1024,65535), 'Allow Chime Voice Connector Media Access') 54 | 55 | const incomingFaxes = new s3.Bucket(this, 'IncomingFaxes', { 56 | publicReadAccess: false, 57 | removalPolicy: cdk.RemovalPolicy.DESTROY, 58 | autoDeleteObjects: true // NOT recommended for production code 59 | }); 60 | 61 | 62 | const role = new iam.Role(this, 'ec2Role', { 63 | assumedBy: new iam.ServicePrincipal('ec2.amazonaws.com') 64 | }) 65 | 66 | role.addToPolicy(new iam.PolicyStatement({ 67 | resources: [ 68 | incomingFaxes.bucketArn, 69 | `${incomingFaxes.bucketArn}/*` 70 | ], 71 | actions: ['*']})) 72 | 73 | role.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonSSMManagedInstanceCore')) 74 | 75 | const eip = new ec2.CfnEIP(this, 'EIP') 76 | 77 | // Use Latest Amazon Linux Image - CPU Type ARM64 78 | const ami = new ec2.AmazonLinuxImage({ 79 | generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2, 80 | cpuType: ec2.AmazonLinuxCpuType.X86_64}); 81 | 82 | const createVoiceConnectorRole = new iam.Role(this, 'createChimeLambdaRole', { 83 | assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), 84 | inlinePolicies: { 85 | ['chimePolicy']: new iam.PolicyDocument( { statements: [new iam.PolicyStatement({ 86 | resources: ['*'], 87 | actions: ['chime:*', 88 | 'lambda:*']})]}) 89 | }, 90 | managedPolicies: [ iam.ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSLambdaBasicExecutionRole") ] 91 | }) 92 | 93 | const createVoiceConnectorLambda = new lambda.Function(this, 'createVCLambda', { 94 | code: lambda.Code.fromAsset("src", {exclude: ["**", "!createVoiceConnector.py"]}), 95 | handler: 'createVoiceConnector.on_event', 96 | runtime: lambda.Runtime.PYTHON_3_8, 97 | role: createVoiceConnectorRole, 98 | timeout: Duration.seconds(60) 99 | }); 100 | 101 | const voiceConnectorProvider = new custom.Provider(this, 'voiceConnectorProvider', { 102 | onEventHandler: createVoiceConnectorLambda 103 | }) 104 | 105 | const textractRole = new iam.Role(this, 'textractLambdaRole', { 106 | assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), 107 | inlinePolicies: { 108 | ['chimePolicy']: new iam.PolicyDocument( { statements: [new iam.PolicyStatement({ 109 | resources: ['*'], 110 | actions: ['textract:*']})]}) 111 | }, 112 | managedPolicies: [ iam.ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSLambdaBasicExecutionRole") ] 113 | }) 114 | 115 | const textractLambda = new lambda.Function(this, 'textractLambda', { 116 | code: lambda.Code.fromAsset("src", {exclude: ["**", "!textract.py"]}), 117 | handler: 'textract.lambda_handler', 118 | runtime: lambda.Runtime.PYTHON_3_8, 119 | timeout: Duration.seconds(60), 120 | role: textractRole 121 | }); 122 | 123 | textractLambda.addEventSource(new S3EventSource(incomingFaxes, { 124 | events: [ s3.EventType.OBJECT_CREATED] 125 | })); 126 | 127 | incomingFaxes.grantReadWrite(textractLambda) 128 | 129 | const voiceConnectorResource = new CustomResource(this, 'voiceConnectorResource', { 130 | serviceToken: voiceConnectorProvider.serviceToken, 131 | properties: { 'region': this.region, 132 | 'eip': eip.ref } 133 | }) 134 | 135 | const phoneNumber = voiceConnectorResource.getAttString('phoneNumber') 136 | const voiceConnectorId = voiceConnectorResource.getAttString('voiceConnectorId') 137 | const outboundHostName = voiceConnectorResource.getAttString('outboundHostName') 138 | 139 | const phoneNumberParameter = new ssm.StringParameter(this, 'phoneNumber', { 140 | parameterName: '/asterisk/phoneNumber', 141 | stringValue: phoneNumber, 142 | }); 143 | 144 | const voiceConnectorParameter = new ssm.StringParameter(this, 'voiceConnector', { 145 | parameterName: '/asterisk/voiceConnector', 146 | stringValue: voiceConnectorId 147 | }) 148 | 149 | const outboundHostNameParameter = new ssm.StringParameter(this, 'outboundHostName', { 150 | parameterName: '/asterisk/outboundHostName', 151 | stringValue: outboundHostName 152 | }) 153 | 154 | const incomingFaxBucketParameter = new ssm.StringParameter(this, 'incomingFaxBucket', { 155 | parameterName: '/asterisk/incomingFaxBucket', 156 | stringValue: incomingFaxes.bucketName 157 | }) 158 | 159 | const ec2UserData = ec2.UserData.forLinux(); 160 | 161 | const asteriskConfig = new Asset(this, 'AsteriskConfig', {path: path.join(__dirname, '../src/config.sh')}); 162 | 163 | const configPath = ec2UserData.addS3DownloadCommand({ 164 | bucket:asteriskConfig.bucket, 165 | bucketKey:asteriskConfig.s3ObjectKey, 166 | }); 167 | 168 | ec2UserData.addExecuteFileCommand({ 169 | filePath:configPath, 170 | arguments: '--verbose -y' 171 | }); 172 | 173 | asteriskConfig.grantRead(role); 174 | phoneNumberParameter.grantRead(role); 175 | voiceConnectorParameter.grantRead(role); 176 | outboundHostNameParameter.grantRead(role); 177 | incomingFaxBucketParameter.grantRead(role); 178 | 179 | // Create the instance using the Security Group, AMI, and KeyPair defined in the VPC created 180 | const ec2Instance = new ec2.Instance(this, 'Instance', { 181 | vpc, 182 | instanceType: ec2.InstanceType.of(ec2.InstanceClass.T3, ec2.InstanceSize.LARGE), 183 | machineImage: ami, 184 | securityGroup: securityGroup, 185 | keyName: key.keyPairName, 186 | role: role, 187 | userData: ec2UserData, 188 | }); 189 | 190 | new ec2.CfnEIPAssociation(this, "EIP Association", { 191 | eip: eip.ref, 192 | instanceId: ec2Instance.instanceId 193 | }) 194 | 195 | // Create outputs for connecting 196 | new cdk.CfnOutput(this, 'IP Address', { value: ec2Instance.instancePublicIp }); 197 | new cdk.CfnOutput(this, 'Instance ID', {value: ec2Instance.instanceId}) 198 | new cdk.CfnOutput(this, 'Download Key Command', { value: 'aws secretsmanager get-secret-value --secret-id ec2-ssh-key/cdk-keypair/private --query SecretString --output text > cdk-key.pem && chmod 400 cdk-key.pem' }) 199 | new cdk.CfnOutput(this, 'ssh command', { value: 'ssh -i cdk-key.pem -o IdentitiesOnly=yes ec2-user@' + ec2Instance.instancePublicIp }) 200 | new cdk.CfnOutput(this, 'PhoneNumber', { value: phoneNumber}), 201 | new cdk.CfnOutput(this, 'VoiceConnector', { value: outboundHostName}) 202 | new cdk.CfnOutput(this, 'S3 Bucket', { value: incomingFaxes.bucketName} ) 203 | new cdk.CfnOutput(this, 'Textract Lambda', { value: textractLambda.functionName}) 204 | } 205 | } -------------------------------------------------------------------------------- /week-02/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "building-with-amazon-chime-week-02", 3 | "version": "0.1.0", 4 | "bin": { 5 | "ec2-cdk": "bin/asterisk-fax-server.js" 6 | }, 7 | "scripts": { 8 | "build": "tsc", 9 | "watch": "tsc -w", 10 | "cdk": "cdk" 11 | }, 12 | "devDependencies": { 13 | "@aws-cdk/assert": "^1.100.0", 14 | "@types/jest": "^26.0.22", 15 | "@types/node": "14.14.41", 16 | "aws-cdk": "^1.100.0", 17 | "ts-node": "^9.1.1", 18 | "typescript": "~4.2.4" 19 | }, 20 | "dependencies": { 21 | "@aws-cdk/aws-ec2": "^1.100.0", 22 | "@aws-cdk/aws-lambda-event-sources": "^1.104.0", 23 | "@aws-cdk/core": "^1.100.0", 24 | "@aws-cdk/custom-resources": "^1.100.0", 25 | "cdk-ec2-key-pair": "^2.1.1", 26 | "source-map-support": "^0.5.19" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /week-02/src/config.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -xe 2 | HOMEDIR=/home/ec2-user 3 | yum update -y 4 | yum install net-tools -y 5 | yum install wget -y 6 | yum -y install make gcc gcc-c++ make subversion libxml2-devel ncurses-devel openssl-devel vim-enhanced man glibc-devel autoconf libnewt kernel-devel kernel-headers linux-headers openssl-devel zlib-devel libsrtp libsrtp-devel uuid libuuid-devel mariadb-server jansson-devel libsqlite3x libsqlite3x-devel epel-release.noarch bash-completion bash-completion-extras unixODBC unixODBC-devel libtool-ltdl libtool-ltdl-devel mysql-connector-odbc mlocate libiodbc sqlite sqlite-devel sql-devel.i686 sqlite-doc.noarch sqlite-tcl.x86_64 patch libedit-devel libtiff-devel jq 7 | amazon-linux-extras install epel -y 8 | yum install inotify-tools -y 9 | yum install ImageMagick ImageMagick-devel -y 10 | cd /tmp 11 | wget https://www.soft-switch.org/downloads/spandsp/spandsp-0.0.6.tar.gz 12 | tar xvzf spandsp-0.0.6.tar.gz 13 | cd spandsp-0.0.6/ 14 | ./configure 15 | make 16 | make install 17 | echo "/usr/local/lib" >> /etc/ld.so.conf.d/usrlocallib.conf 18 | ldconfig 19 | cd - 20 | wget https://downloads.asterisk.org/pub/telephony/asterisk/asterisk-16-current.tar.gz 21 | tar xvzf asterisk-16-current.tar.gz 22 | cd asterisk-16*/ 23 | ./configure --libdir=/usr/lib64 --with-jansson-bundled 24 | make -j$(nproc) menuselect.makeopts 25 | menuselect/menuselect \ 26 | --disable BUILD_NATIVE \ 27 | --disable chan_sip \ 28 | --disable chan_skinny \ 29 | --enable cdr_csv \ 30 | --enable res_snmp \ 31 | --enable res_fax_spandsp \ 32 | --enable res_http_websocket \ 33 | menuselect.makeopts \ 34 | make 35 | make install 36 | make basic-pbx 37 | touch /etc/redhat-release 38 | make config 39 | ldconfig 40 | 41 | IP=$( curl http://169.254.169.254/latest/meta-data/public-ipv4 ) 42 | REGION=$( curl http://169.254.169.254/latest/meta-data/placement/region ) 43 | PhoneNumber=$( aws ssm get-parameter --name /asterisk/phoneNumber --region $REGION | jq -r '.Parameter.Value' ) 44 | VoiceConnectorHost=$( aws ssm get-parameter --name /asterisk/voiceConnector --region $REGION | jq -r '.Parameter.Value' ) 45 | OutboundHostName=$( aws ssm get-parameter --name /asterisk/outboundHostName --region $REGION | jq -r '.Parameter.Value' ) 46 | IncomingFaxBucket=$( aws ssm get-parameter --name /asterisk/incomingFaxBucket --region $REGION | jq -r '.Parameter.Value' ) 47 | 48 | echo "[udp] 49 | type=transport 50 | protocol=udp 51 | bind=0.0.0.0 52 | external_media_address=$IP 53 | external_signaling_address=$IP 54 | allow_reload=yes 55 | 56 | [VoiceConnector] 57 | type=endpoint 58 | context=from-voiceConnector 59 | transport=udp 60 | disallow=all 61 | allow=ulaw 62 | aors=VoiceConnector 63 | direct_media=no 64 | ice_support=yes 65 | force_rport=yes 66 | 67 | [VoiceConnector] 68 | type=identify 69 | endpoint=VoiceConnector 70 | match=$OutboundHostName 71 | 72 | [VoiceConnector] 73 | type=aor 74 | contact=sip:$OutboundHostName 75 | 76 | [$PhoneNumber] 77 | type=endpoint 78 | context=from-phone 79 | disallow=all 80 | allow=ulaw 81 | transport=udp 82 | auth=$PhoneNumber 83 | aors=$PhoneNumber 84 | send_pai=yes 85 | direct_media=no 86 | rewrite_contact=yes 87 | ice_support=yes 88 | force_rport=yes 89 | 90 | [$PhoneNumber] 91 | type=auth 92 | auth_type=userpass 93 | password=ChimeDemo 94 | username=$PhoneNumber 95 | 96 | [$PhoneNumber] 97 | type=aor 98 | max_contacts=5" > /etc/asterisk/pjsip.conf 99 | 100 | echo "; extensions.conf - the Asterisk dial plan 101 | ; 102 | [general] 103 | static=yes 104 | writeprotect=no 105 | clearglobalvars=no 106 | 107 | [from-voiceConnector] 108 | include => faxnumber 109 | 110 | [faxnumber] 111 | exten => $PhoneNumber,1,NoOp(Receiving Fax) 112 | same => n,Set(FAXDIRECTORY=/etc/asterisk/incoming_faxes) 113 | same => n,Set(FAXNAME=\${STRFTIME(,,%s)}) 114 | same => n,ReceiveFax(\${FAXDIRECTORY}/\${FAXNAME}.tif) 115 | same => n,NoOp(Fax Finished: \${FAXSTATUS}) 116 | 117 | [sendfax] 118 | exten => s,1,NoOP() 119 | same => n,SendFax(/tmp/faxfile.tif,fs) 120 | same => n,Hangup() 121 | 122 | [outboundfax] 123 | exten => _+X.,1,NoOP(Sending Fax) 124 | same => n,Set(CALLERID(num)=$PhoneNumber) 125 | same => n,Dial(PJSIP/\${EXTEN}@VoiceConnector,30)" > /etc/asterisk/extensions.conf 126 | 127 | echo "load = res_fax.so 128 | load = res_fax_spandsp.so 129 | load = pbx_spool.so 130 | load = res_clioriginate.so" >> /etc/asterisk/modules.conf 131 | 132 | echo "[options] 133 | runuser = asterisk 134 | rungroup = asterisk" > /etc/asterisk/asterisk.conf 135 | 136 | echo "[general] 137 | [logfiles] 138 | console = verbose,notice,warning,error 139 | messages = notice,warning,error" > /etc/asterisk/logger.conf 140 | 141 | mkdir /etc/asterisk/incoming_faxes 142 | mkdir /etc/asterisk/converted_faxes 143 | 144 | echo " 145 | #!/usr/bin/env bash 146 | while true; do 147 | inotifywait /etc/asterisk/incoming_faxes -e moved_to -e close -e close_write --format '%f' | while read file; do 148 | filename=\"\${file%.*}\" 149 | convert /etc/asterisk/incoming_faxes/\$filename.tif /etc/asterisk/converted_faxes/\$filename.jpg 150 | aws s3 cp /etc/asterisk/converted_faxes/\$filename.jpg s3://$IncomingFaxBucket 151 | done 152 | done" > /usr/bin/watch_dir.sh 153 | 154 | chmod +x /usr/bin/watch_dir.sh 155 | /usr/bin/watch_dir.sh & 156 | 157 | (crontab -l ; echo "@reboot /usr/bin/watch_dir.sh") | crontab - 158 | 159 | groupadd asterisk 160 | useradd -r -d /var/lib/asterisk -g asterisk asterisk 161 | usermod -aG audio,dialout asterisk 162 | chown -R asterisk.asterisk /etc/asterisk 163 | chown -R asterisk.asterisk /etc/asterisk/incoming_faxes 164 | chown -R asterisk.asterisk /etc/asterisk/converted_faxes 165 | chown -R asterisk.asterisk /var/{lib,log,spool}/asterisk 166 | 167 | systemctl start asterisk -------------------------------------------------------------------------------- /week-02/src/createVoiceConnector.py: -------------------------------------------------------------------------------- 1 | import json 2 | import boto3 3 | import time 4 | import uuid 5 | 6 | chime = boto3.client('chime') 7 | 8 | def authorizeEIP (voiceConnectorId, elasticIP): 9 | response = chime.put_voice_connector_origination( 10 | VoiceConnectorId=voiceConnectorId, 11 | Origination={ 12 | 'Routes': [ 13 | { 14 | 'Host': elasticIP, 15 | 'Port': 5060, 16 | 'Protocol': 'UDP', 17 | 'Priority': 1, 18 | 'Weight': 1 19 | }, 20 | ], 21 | 'Disabled': False 22 | } 23 | ) 24 | print(response) 25 | 26 | response = chime.put_voice_connector_termination( 27 | VoiceConnectorId=voiceConnectorId, 28 | Termination={ 29 | 'CpsLimit': 1, 30 | 'CallingRegions': [ 31 | 'US', 32 | ], 33 | 'CidrAllowedList': [ 34 | elasticIP + '/32', 35 | ], 36 | 'Disabled': False 37 | } 38 | ) 39 | print(response) 40 | 41 | 42 | def getPhoneNumber (): 43 | search_response = chime.search_available_phone_numbers( 44 | # AreaCode='string', 45 | # City='string', 46 | # Country='string', 47 | State='IL', 48 | # TollFreePrefix='string', 49 | MaxResults=1 50 | ) 51 | phoneNumberToOrder = search_response['E164PhoneNumbers'][0] 52 | print ('Phone Number: ' + phoneNumberToOrder) 53 | phone_order = chime.create_phone_number_order( 54 | ProductType='VoiceConnector', 55 | E164PhoneNumbers=[ 56 | phoneNumberToOrder, 57 | ] 58 | ) 59 | print ('Phone Order: ' + str(phone_order)) 60 | 61 | check_phone_order = chime.get_phone_number_order( 62 | PhoneNumberOrderId=phone_order['PhoneNumberOrder']['PhoneNumberOrderId'] 63 | ) 64 | order_status = check_phone_order['PhoneNumberOrder']['Status'] 65 | timeout = 0 66 | 67 | while not order_status == 'Successful': 68 | timeout += 1 69 | print('Checking status: ' + str(order_status)) 70 | time.sleep(5) 71 | check_phone_order = chime.get_phone_number_order( 72 | PhoneNumberOrderId=phone_order['PhoneNumberOrder']['PhoneNumberOrderId'] 73 | ) 74 | order_status = check_phone_order['PhoneNumberOrder']['Status'] 75 | if timeout == 5: 76 | return 'Could not get phone number' 77 | 78 | return phoneNumberToOrder 79 | 80 | def createVoiceConnector (region, phoneNumber): 81 | print(str(uuid.uuid1())) 82 | print(region) 83 | response = chime.create_voice_connector( 84 | Name='Trunk' + str(uuid.uuid1()), 85 | AwsRegion=region, 86 | RequireEncryption=False 87 | ) 88 | 89 | voiceConnectorId = response['VoiceConnector']['VoiceConnectorId'] 90 | outboundHostName = response['VoiceConnector']['OutboundHostName'] 91 | 92 | response = chime.associate_phone_numbers_with_voice_connector( 93 | VoiceConnectorId=voiceConnectorId, 94 | E164PhoneNumbers=[ 95 | phoneNumber, 96 | ], 97 | ForceAssociate=True 98 | ) 99 | 100 | voiceConnector = { 'voiceConnectorId': voiceConnectorId, 'outboundHostName': outboundHostName, 'phoneNumber': phoneNumber} 101 | return voiceConnector 102 | 103 | def on_event(event, context): 104 | print(event) 105 | request_type = event['RequestType'] 106 | if request_type == 'Create': return on_create(event) 107 | if request_type == 'Update': return on_update(event) 108 | if request_type == 'Delete': return on_delete(event) 109 | raise Exception("Invalid request type: %s" % request_type) 110 | 111 | def on_create(event): 112 | physical_id = 'VoiceConnectorResources' 113 | region = event['ResourceProperties']['region'] 114 | elasticIP = event['ResourceProperties']['eip'] 115 | 116 | newPhoneNumber = getPhoneNumber() 117 | voiceConnector = createVoiceConnector(region, newPhoneNumber) 118 | authorizeEIP(voiceConnector['voiceConnectorId'], elasticIP) 119 | 120 | return { 'PhysicalResourceId': physical_id, 'Data': voiceConnector } 121 | 122 | def on_update(event): 123 | physical_id = event["PhysicalResourceId"] 124 | props = event["ResourceProperties"] 125 | print("update resource %s with props %s" % (physical_id, props)) 126 | return { 'PhysicalResourceId': physical_id } 127 | 128 | 129 | def on_delete(event): 130 | physical_id = event["PhysicalResourceId"] 131 | print("delete resource %s" % physical_id) 132 | return { 'PhysicalResourceId': physical_id } 133 | -------------------------------------------------------------------------------- /week-02/src/textract.py: -------------------------------------------------------------------------------- 1 | import json 2 | import urllib.parse 3 | import boto3 4 | 5 | def lambda_handler(event, context): 6 | bucket = event['Records'][0]['s3']['bucket']['name'] 7 | key = urllib.parse.unquote_plus(event['Records'][0]['s3']['object']['key'], encoding='utf-8') 8 | client = boto3.client('textract') 9 | 10 | #process using S3 object 11 | response = client.detect_document_text( 12 | Document={'S3Object': {'Bucket': bucket, 'Name': key}}) 13 | 14 | #Get the text blocks 15 | blocks=response['Blocks'] 16 | 17 | for item in response["Blocks"]: 18 | if item["BlockType"] == "LINE": 19 | print (item["Text"]) 20 | 21 | return { 22 | 'statusCode': 200, 23 | 'body': json.dumps(blocks) 24 | } 25 | -------------------------------------------------------------------------------- /week-02/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2018", 4 | "module": "commonjs", 5 | "lib": ["es2018"], 6 | "declaration": true, 7 | "strict": true, 8 | "noImplicitAny": true, 9 | "strictNullChecks": true, 10 | "noImplicitThis": true, 11 | "alwaysStrict": true, 12 | "noUnusedLocals": false, 13 | "noUnusedParameters": false, 14 | "noImplicitReturns": true, 15 | "noFallthroughCasesInSwitch": false, 16 | "inlineSourceMap": true, 17 | "inlineSources": true, 18 | "experimentalDecorators": true, 19 | "strictPropertyInitialization": false, 20 | "typeRoots": ["./node_modules/@types"] 21 | }, 22 | "exclude": ["cdk.out"] 23 | } 24 | -------------------------------------------------------------------------------- /week-03/.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | *.d.ts 3 | node_modules 4 | !src/*.js 5 | 6 | # CDK asset staging directory 7 | .cdk.staging 8 | cdk.out 9 | 10 | package-lock.json 11 | *.pcap 12 | *.pem -------------------------------------------------------------------------------- /week-03/.npmignore: -------------------------------------------------------------------------------- 1 | *.ts 2 | !*.d.ts 3 | 4 | # CDK asset staging directory 5 | .cdk.staging 6 | cdk.out 7 | -------------------------------------------------------------------------------- /week-03/README.md: -------------------------------------------------------------------------------- 1 | # Chime Voice Connector Troubleshooting 2 | 3 | This demo will build and configure several services to explore troubleshooting VoIP on AWS. 4 | 5 | ## Requirements 6 | - AWS CLI [installed](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2-linux.html) 7 | - node/npm [installed](https://github.com/nodesource/distributions/blob/master/README.md) 8 | - AWS CDK [installed](https://docs.aws.amazon.com/cdk/latest/guide/getting_started.html) and [bootstrapped](https://docs.aws.amazon.com/cdk/latest/guide/bootstrapping.html) 9 | - Ability to create a VPC and EIP within that VPC (ensure your [Service Quota](https://console.aws.amazon.com/servicequotas/) for EIP is not reached) 10 | - Ability to create a Chime Voice Connector (ensure your Service Quota for VC is not reached) 11 | ## Deployment 12 | 13 | - Clone this repo 14 | - `chmod +x deploy.sh` 15 | - `./deploy.sh` 16 | 17 | This script will ensure that all dependencies are installed and ready for the CDK deployment. If you get a schema related error, please uninstall and re-install aws-cdk to get to the latest version. 18 | ``` 19 | npm uninstall -g aws-cdk 20 | npm install -g aws-cdk 21 | ``` 22 | ## Connecting to the Asterisk Server 23 | 24 | The output of the CDK will include several commands that you will use to connect to your Asterisk server. There is no requirement to connect to the Asterisk server, but may be of interest. 25 | 26 | The CDK will create a key-pair and store the private key in AWS SecretsManager. The DownloadKeyCommand will download this file to your local machine. The public key of this key-pair has already been loaded in the Asterisk server. The sshcommand will then ssh to your Asterisk server. 27 | 28 | ## Overview 29 | 30 | ![Diagram](images/Week-03-Diagram.png) 31 | 32 | ### Logging in to Server 33 | This week we'll be exploring trouble shooting on the Asterisk so logging on will be more useful than in pervious weeks. 34 | 35 | - ssh to Asterisk: `ssh -i cdk-key.pem -o IdentitiesOnly=yes ec2-user@` 36 | - get access to root: `sudo bash` 37 | - check to see if config completed: `tail -f /var/log/cloud-init-output.log` 38 | 39 | You should see something like: 40 | ``` 41 | chown -R asterisk.asterisk /var/lib/asterisk /var/log/asterisk /var/spool/asterisk systemctl start asterisk 42 | ``` 43 | 44 | ### Capturing traffic 45 | 46 | - install Wireshark: `yum install wireshark -y` 47 | - capture SIP messages to screen: `tshark -f "udp port 5060"` 48 | - capture SIP messages to file: `tshark -f "udp port 5060" -w /tmp/capture.pcap` 49 | - capture SIP messages and audio to file: `tshark -f "udp" -w /tmp/capture.pcap` 50 | 51 | ### Transfering pcap to local machine 52 | - change ownership of file to ec2-user: `chown ec2-user /tmp/capture.pcap` 53 | - copy file down to local machine: `scp -i cdk-key.pem -o IdentitiesOnly=yes ec2-user@:/tmp/capture.pcap ./` 54 | 55 | # Destroying This Install 56 | 57 | To clean up after you're done with this demo, you can run `cdk destroy`. This will remove most of the components that were created, but you will need to remove the Voice Connector and Phone Number associated manually within the AWS [Chime Console](https://console.chime.aws.amazon.com/). 58 | -------------------------------------------------------------------------------- /week-03/bin/asterisk_cdr.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from '@aws-cdk/core'; 2 | import { AsteriskCdrStack } from '../lib/asterisk_cdr-stack'; 3 | 4 | const app = new cdk.App(); 5 | 6 | const SecurityGroupIP = app.node.tryGetContext('SecurityGroupIP') 7 | 8 | new AsteriskCdrStack(app, 'AsteriskCdrStack', { 9 | SecurityGroupIP: SecurityGroupIP 10 | }); 11 | -------------------------------------------------------------------------------- /week-03/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node --prefer-ts-exts bin/asterisk_cdr.ts", 3 | "context": { 4 | "@aws-cdk/aws-apigateway:usagePlanKeyOrderInsensitiveId": true, 5 | "@aws-cdk/core:enableStackNameDuplicates": "true", 6 | "aws-cdk:enableDiffNoFail": "true", 7 | "@aws-cdk/core:stackRelativeExports": "true", 8 | "@aws-cdk/aws-ecr-assets:dockerIgnoreSupport": true, 9 | "@aws-cdk/aws-secretsmanager:parseOwnedSecretName": true, 10 | "@aws-cdk/aws-kms:defaultKeyPolicies": true, 11 | "@aws-cdk/aws-s3:grantWriteWithoutAcl": true, 12 | "@aws-cdk/aws-ecs-patterns:removeDefaultDesiredCount": true, 13 | "@aws-cdk/aws-rds:lowercaseDbIdentifier": true, 14 | "@aws-cdk/aws-efs:defaultEncryptionAtRest": true 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /week-03/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | function valid_ip() 3 | { 4 | local ip=$1 5 | local stat=1 6 | 7 | if [[ $ip =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then 8 | OIFS=$IFS 9 | IFS='.' 10 | ip=($ip) 11 | IFS=$OIFS 12 | [[ ${ip[0]} -le 255 && ${ip[1]} -le 255 \ 13 | && ${ip[2]} -le 255 && ${ip[3]} -le 255 ]] 14 | stat=$? 15 | fi 16 | return $stat 17 | } 18 | ExternalIP='' 19 | 20 | if [ -f "cdk.context.json" ]; then 21 | echo "" 22 | echo "INFO: Removing cdk.context.json" 23 | rm cdk.context.json 24 | else 25 | echo "" 26 | echo "INFO: cdk.context.json not present, nothing to remove" 27 | fi 28 | if [ ! -f "package-lock.json" ]; then 29 | echo "" 30 | echo "Installing Packages" 31 | echo "" 32 | npm install 33 | fi 34 | echo "" 35 | echo "Getting External IP address for Security Group" 36 | echo "" 37 | if [ -x "$(which curl)" ] ; then 38 | echo "Using Curl" 39 | ExternalIP=$( curl checkip.amazonaws.com ) 40 | echo "External IP: " $ExternalIP 41 | else 42 | while true; do 43 | read -p "Enter IPv4 address to allow access in Security Group: " ExternalIP 44 | if valid_ip $ExternalIP; then 45 | break 46 | else 47 | echo "Please enter a valid IPv4 address in the form of XXX.XXX.XXX.XXX" 48 | fi 49 | done 50 | fi 51 | echo "" 52 | echo "Building CDK" 53 | echo "" 54 | npm run build 55 | echo "" 56 | echo "Deploying CDK" 57 | echo "" 58 | cdk deploy -c SecurityGroupIP=$ExternalIP -------------------------------------------------------------------------------- /week-03/images/Week-03-Diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/building-with-amazon-chime/969c1c7a36fd7e4e982922290a05ec9ad5b420fc/week-03/images/Week-03-Diagram.png -------------------------------------------------------------------------------- /week-03/lib/asterisk_cdr-stack.ts: -------------------------------------------------------------------------------- 1 | import * as ec2 from "@aws-cdk/aws-ec2"; 2 | import * as cdk from '@aws-cdk/core'; 3 | import { KeyPair } from 'cdk-ec2-key-pair'; 4 | import { Asset } from '@aws-cdk/aws-s3-assets'; 5 | import * as path from 'path'; 6 | import * as ssm from '@aws-cdk/aws-ssm'; 7 | import * as iam from '@aws-cdk/aws-iam'; 8 | import { CustomResource, Duration } from '@aws-cdk/core'; 9 | import lambda = require('@aws-cdk/aws-lambda'); 10 | import custom = require('@aws-cdk/custom-resources') 11 | import dynamodb = require('@aws-cdk/aws-dynamodb'); 12 | import s3 = require('@aws-cdk/aws-s3'); 13 | import { S3EventSource } from '@aws-cdk/aws-lambda-event-sources'; 14 | 15 | export interface StackProps { 16 | SecurityGroupIP: string; 17 | } 18 | export class AsteriskCdrStack extends cdk.Stack { 19 | constructor(scope: cdk.Construct, id: string, props: StackProps) { 20 | super(scope, id); 21 | 22 | const cdrTable = new dynamodb.Table(this, 'meetings', { 23 | partitionKey: { 24 | name: 'callId', 25 | type: dynamodb.AttributeType.STRING 26 | }, 27 | removalPolicy: cdk.RemovalPolicy.DESTROY, 28 | billingMode: dynamodb.BillingMode.PAY_PER_REQUEST, 29 | }); 30 | 31 | const cdrBucket = new s3.Bucket(this, 'cdrBucket', { 32 | }); 33 | 34 | const processCDRsLambda = new lambda.Function(this, 'process', { 35 | code: lambda.Code.fromAsset("src", {exclude: ["**", "!process.js"]}), 36 | handler: 'process.handler', 37 | runtime: lambda.Runtime.NODEJS_14_X, 38 | timeout: Duration.seconds(60), 39 | role: new iam.Role(this, 'lambdaRole', { 40 | assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), 41 | managedPolicies: [ 42 | iam.ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSLambdaBasicExecutionRole"), 43 | iam.ManagedPolicy.fromAwsManagedPolicyName("AWSPriceListServiceFullAccess")] 44 | }), 45 | environment: { 46 | CDR_TABLE: cdrTable.tableName 47 | }, 48 | }); 49 | 50 | processCDRsLambda.addEventSource(new S3EventSource(cdrBucket, { 51 | events: [ s3.EventType.OBJECT_CREATED] 52 | })); 53 | 54 | cdrTable.grantReadWriteData(processCDRsLambda) 55 | cdrBucket.grantRead(processCDRsLambda) 56 | 57 | 58 | // Create a Key Pair to be used with this EC2 Instance 59 | const key = new KeyPair(this, 'KeyPair', { 60 | name: 'cdk-keypair', 61 | description: 'Key Pair created with CDK Deployment', 62 | }); 63 | key.grantReadOnPublicKey 64 | 65 | // Create new VPC with 2 Subnets 66 | const vpc = new ec2.Vpc(this, 'VPC', { 67 | natGateways: 0, 68 | subnetConfiguration: [ { 69 | cidrMask: 24, 70 | name: "asterisk", 71 | subnetType: ec2.SubnetType.PUBLIC 72 | }]}); 73 | 74 | // Allow SSH (TCP Port 22) access from anywhere 75 | const securityGroup = new ec2.SecurityGroup(this, 'SecurityGroup', { 76 | vpc, 77 | description: 'Security Group for Asterisk Server', 78 | allowAllOutbound: true 79 | }); 80 | securityGroup.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(22), 'Allow SSH Access') 81 | securityGroup.addIngressRule(ec2.Peer.ipv4('3.80.16.0/23'), ec2.Port.udp(5060), 'Allow Chime Voice Connector Signaling Access') 82 | securityGroup.addIngressRule(ec2.Peer.ipv4('3.80.16.0/23'), ec2.Port.tcp(5060), 'Allow Chime Voice Connector Signaling Access') 83 | securityGroup.addIngressRule(ec2.Peer.ipv4('3.80.16.0/23'), ec2.Port.tcp(5061), 'Allow Chime Voice Connector Signaling Access') 84 | securityGroup.addIngressRule(ec2.Peer.ipv4('99.77.253.0/24'), ec2.Port.udp(5060), 'Allow Chime Voice Connector Signaling Access') 85 | securityGroup.addIngressRule(ec2.Peer.ipv4('99.77.253.0/24'), ec2.Port.tcp(5060), 'Allow Chime Voice Connector Signaling Access') 86 | securityGroup.addIngressRule(ec2.Peer.ipv4('99.77.253.0/24'), ec2.Port.tcp(5061), 'Allow Chime Voice Connector Signaling Access') 87 | securityGroup.addIngressRule(ec2.Peer.ipv4('99.77.253.0/24'), ec2.Port.udpRange(5000,65000), 'Allow Chime Voice Connector Signaling Access') 88 | securityGroup.addIngressRule(ec2.Peer.ipv4('3.80.16.0/23'), ec2.Port.udpRange(5000,65000), 'Allow Chime Voice Connector Media Access') 89 | securityGroup.addIngressRule(ec2.Peer.ipv4('99.77.253.0/24'), ec2.Port.udpRange(5000,65000), 'Allow Chime Voice Connector Media Access') 90 | securityGroup.addIngressRule(ec2.Peer.ipv4('52.55.62.128/25'), ec2.Port.udpRange(1024,65535), 'Allow Chime Voice Connector Media Access') 91 | securityGroup.addIngressRule(ec2.Peer.ipv4('52.55.63.0/25'), ec2.Port.udpRange(1024,65535), 'Allow Chime Voice Connector Media Access') 92 | securityGroup.addIngressRule(ec2.Peer.ipv4('34.212.95.128/25'), ec2.Port.udpRange(1024,65535), 'Allow Chime Voice Connector Media Access') 93 | securityGroup.addIngressRule(ec2.Peer.ipv4('34.223.21.0/25'), ec2.Port.udpRange(1024,65535), 'Allow Chime Voice Connector Media Access') 94 | securityGroup.addIngressRule(ec2.Peer.ipv4(props.SecurityGroupIP + '/32'), ec2.Port.allTraffic(), 'All inbound traffic from local machine') 95 | 96 | const role = new iam.Role(this, 'ec2Role', { 97 | assumedBy: new iam.ServicePrincipal('ec2.amazonaws.com') 98 | }) 99 | 100 | role.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonSSMManagedInstanceCore')) 101 | 102 | const eip = new ec2.CfnEIP(this, 'EIP') 103 | 104 | // Use Latest Amazon Linux Image - CPU Type ARM64 105 | const ami = new ec2.AmazonLinuxImage({ 106 | generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2, 107 | cpuType: ec2.AmazonLinuxCpuType.ARM_64}); 108 | 109 | const createVoiceConnectorRole = new iam.Role(this, 'createChimeLambdaRole', { 110 | assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), 111 | inlinePolicies: { 112 | ['chimePolicy']: new iam.PolicyDocument( { statements: [new iam.PolicyStatement({ 113 | resources: ['*'], 114 | actions: ['chime:*', 115 | 'lambda:*']})]}) 116 | }, 117 | managedPolicies: [ iam.ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSLambdaBasicExecutionRole") ] 118 | }) 119 | 120 | const createVoiceConnectorLambda = new lambda.Function(this, 'createVCLambda', { 121 | code: lambda.Code.fromAsset("src", {exclude: ["**", "!createVoiceConnector.py"]}), 122 | handler: 'createVoiceConnector.on_event', 123 | runtime: lambda.Runtime.PYTHON_3_8, 124 | role: createVoiceConnectorRole, 125 | timeout: Duration.seconds(60) 126 | }); 127 | 128 | const voiceConnectorProvider = new custom.Provider(this, 'voiceConnectorProvider', { 129 | onEventHandler: createVoiceConnectorLambda 130 | }) 131 | 132 | const voiceConnectorResource = new CustomResource(this, 'voiceConnectorResource', { 133 | serviceToken: voiceConnectorProvider.serviceToken, 134 | properties: { 'region': this.region, 135 | 'eip': eip.ref } 136 | }) 137 | 138 | const phoneNumber = voiceConnectorResource.getAttString('phoneNumber') 139 | const voiceConnectorId = voiceConnectorResource.getAttString('voiceConnectorId') 140 | const outboundHostName = voiceConnectorResource.getAttString('outboundHostName') 141 | 142 | const phoneNumberParameter = new ssm.StringParameter(this, 'phoneNumber', { 143 | parameterName: '/asterisk/phoneNumber', 144 | stringValue: phoneNumber, 145 | }); 146 | 147 | const voiceConnectorParameter = new ssm.StringParameter(this, 'voiceConnector', { 148 | parameterName: '/asterisk/voiceConnector', 149 | stringValue: voiceConnectorId 150 | }) 151 | 152 | const outboundHostNameParameter = new ssm.StringParameter(this, 'outboundHostName', { 153 | parameterName: '/asterisk/outboundHostName', 154 | stringValue: outboundHostName 155 | }) 156 | 157 | const ec2UserData = ec2.UserData.forLinux(); 158 | 159 | const asteriskConfig = new Asset(this, 'AsteriskConfig', {path: path.join(__dirname, '../src/config.sh')}); 160 | 161 | const configPath = ec2UserData.addS3DownloadCommand({ 162 | bucket:asteriskConfig.bucket, 163 | bucketKey:asteriskConfig.s3ObjectKey, 164 | }); 165 | 166 | ec2UserData.addExecuteFileCommand({ 167 | filePath:configPath, 168 | arguments: '--verbose -y' 169 | }); 170 | 171 | asteriskConfig.grantRead(role); 172 | phoneNumberParameter.grantRead(role); 173 | voiceConnectorParameter.grantRead(role); 174 | outboundHostNameParameter.grantRead(role); 175 | 176 | // Create the instance using the Security Group, AMI, and KeyPair defined in the VPC created 177 | const ec2Instance = new ec2.Instance(this, 'Instance', { 178 | vpc, 179 | instanceType: ec2.InstanceType.of(ec2.InstanceClass.T4G, ec2.InstanceSize.LARGE), 180 | machineImage: ami, 181 | securityGroup: securityGroup, 182 | keyName: key.keyPairName, 183 | role: role, 184 | userData: ec2UserData, 185 | }); 186 | 187 | new ec2.CfnEIPAssociation(this, "EIP Association", { 188 | eip: eip.ref, 189 | instanceId: ec2Instance.instanceId 190 | }) 191 | 192 | 193 | // Create outputs for connecting 194 | new cdk.CfnOutput(this, 'IP Address', { value: ec2Instance.instancePublicIp }); 195 | new cdk.CfnOutput(this, 'Download Key Command', { value: 'aws secretsmanager get-secret-value --secret-id ec2-ssh-key/cdk-keypair/private --query SecretString --output text > cdk-key.pem && chmod 400 cdk-key.pem' }) 196 | new cdk.CfnOutput(this, 'ssh command', { value: 'ssh -i cdk-key.pem -o IdentitiesOnly=yes ec2-user@' + ec2Instance.instancePublicIp }) 197 | new cdk.CfnOutput(this, 'PhoneNumber', { value: phoneNumber}), 198 | new cdk.CfnOutput(this, 'VoiceConnector', { value: outboundHostName}) 199 | new cdk.CfnOutput(this, 'VoiceConnectorLogs', { value: '/aws/ChimeVoiceConnectorLogs/' + voiceConnectorId}) 200 | new cdk.CfnOutput(this, 'VoiceConnectorSipMessages', { value: '/aws/ChimeVoiceConnectorSipMessages/' + voiceConnectorId}) 201 | new cdk.CfnOutput(this, 'Update Security Group', { value: 'aws ec2 authorize-security-group-ingress --group-id ' + securityGroup.securityGroupId + ' --protocol -1 --cidr YOUR_PUBLIC_IP/32'}) 202 | } 203 | } -------------------------------------------------------------------------------- /week-03/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "asterisk_cdr", 3 | "version": "0.1.0", 4 | "bin": { 5 | "asterisk_cdr": "bin/asterisk_cdr.js" 6 | }, 7 | "scripts": { 8 | "build": "tsc", 9 | "watch": "tsc -w", 10 | "cdk": "cdk" 11 | }, 12 | "devDependencies": { 13 | "@aws-cdk/assert": "^1.100.0", 14 | "@types/jest": "^26.0.22", 15 | "@types/node": "14.14.41", 16 | "aws-cdk": "^1.100.0", 17 | "ts-node": "^9.1.1", 18 | "typescript": "~4.2.4" 19 | }, 20 | "dependencies": { 21 | "@aws-cdk/aws-dynamodb": "^1.105.0", 22 | "@aws-cdk/aws-ec2": "^1.100.0", 23 | "@aws-cdk/aws-lambda-event-sources": "^1.105.0", 24 | "@aws-cdk/core": "^1.100.0", 25 | "@aws-cdk/custom-resources": "^1.100.0", 26 | "cdk-ec2-key-pair": "^2.1.1", 27 | "source-map-support": "^0.5.19" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /week-03/src/config.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -xe 2 | HOMEDIR=/home/ec2-user 3 | yum update -y 4 | yum install net-tools -y 5 | yum install wget -y 6 | yum -y install make gcc gcc-c++ make subversion libxml2-devel ncurses-devel openssl-devel vim-enhanced man glibc-devel autoconf libnewt kernel-devel kernel-headers linux-headers openssl-devel zlib-devel libsrtp libsrtp-devel uuid libuuid-devel mariadb-server jansson-devel libsqlite3x libsqlite3x-devel epel-release.noarch bash-completion bash-completion-extras unixODBC unixODBC-devel libtool-ltdl libtool-ltdl-devel mysql-connector-odbc mlocate libiodbc sqlite sqlite-devel sql-devel.i686 sqlite-doc.noarch sqlite-tcl.x86_64 patch libedit-devel jq 7 | cd /tmp 8 | wget https://downloads.asterisk.org/pub/telephony/asterisk/asterisk-16-current.tar.gz 9 | tar xvzf asterisk-16-current.tar.gz 10 | cd asterisk-16*/ 11 | ./configure --libdir=/usr/lib64 --with-jansson-bundled 12 | make menuselect.makeopts 13 | menuselect/menuselect \ 14 | --disable BUILD_NATIVE \ 15 | --disable chan_sip \ 16 | --disable chan_skinny \ 17 | --enable cdr_csv \ 18 | --enable res_snmp \ 19 | --enable res_http_websocket \ 20 | menuselect.makeopts 21 | make 22 | make install 23 | make basic-pbx 24 | touch /etc/redhat-release 25 | make config 26 | ldconfig 27 | 28 | IP=$( curl http://169.254.169.254/latest/meta-data/public-ipv4 ) 29 | REGION=$( curl http://169.254.169.254/latest/meta-data/placement/region ) 30 | PhoneNumber=$( aws ssm get-parameter --name /asterisk/phoneNumber --region $REGION | jq -r '.Parameter.Value' ) 31 | VoiceConnectorHost=$( aws ssm get-parameter --name /asterisk/voiceConnector --region $REGION | jq -r '.Parameter.Value' ) 32 | OutboundHostName=$( aws ssm get-parameter --name /asterisk/outboundHostName --region $REGION | jq -r '.Parameter.Value' ) 33 | 34 | echo "[udp] 35 | type=transport 36 | protocol=udp 37 | bind=0.0.0.0 38 | external_media_address=$IP 39 | external_signaling_address=$IP 40 | allow_reload=yes 41 | 42 | [VoiceConnector] 43 | type=endpoint 44 | context=from-voiceConnector 45 | transport=udp 46 | disallow=all 47 | allow=ulaw 48 | aors=VoiceConnector 49 | direct_media=no 50 | ice_support=yes 51 | force_rport=yes 52 | 53 | [VoiceConnector] 54 | type=identify 55 | endpoint=VoiceConnector 56 | match=$OutboundHostName 57 | 58 | [VoiceConnector] 59 | type=aor 60 | contact=sip:$OutboundHostName 61 | 62 | [$PhoneNumber] 63 | type=endpoint 64 | context=from-phone 65 | disallow=all 66 | allow=ulaw 67 | transport=udp 68 | auth=$PhoneNumber 69 | aors=$PhoneNumber 70 | send_pai=yes 71 | direct_media=no 72 | rewrite_contact=yes 73 | ice_support=yes 74 | force_rport=yes 75 | 76 | [$PhoneNumber] 77 | type=auth 78 | auth_type=userpass 79 | password=ChimeDemo 80 | username=$PhoneNumber 81 | 82 | [$PhoneNumber] 83 | type=aor 84 | max_contacts=5" > /etc/asterisk/pjsip.conf 85 | 86 | echo "; extensions.conf - the Asterisk dial plan 87 | ; 88 | [general] 89 | static=yes 90 | writeprotect=no 91 | clearglobalvars=no 92 | 93 | [catch-all] 94 | exten => _[+0-9].,1,Answer() 95 | exten => _[+0-9].,n,Wait(1) 96 | exten => _[+0-9].,n,Playback(hello-world) 97 | exten => _[+0-9].,n,Wait(1) 98 | exten => _[+0-9].,n,echo() 99 | exten => _[+0-9].,n,Wait(1) 100 | exten => _[+0-9].,n,Hangup() 101 | 102 | [from-phone] 103 | include => outbound_phone 104 | 105 | [outbound_phone] 106 | exten => _+X.,1,NoOP(Outbound Normal) 107 | same => n,Dial(PJSIP/\${EXTEN}@VoiceConnector,20) 108 | same => n,Congestion 109 | 110 | [from-voiceConnector] 111 | include => phones 112 | include => catch-all 113 | 114 | [phones] 115 | exten => $PhoneNumber,1,Dial(PJSIP/$PhoneNumber)" > /etc/asterisk/extensions.conf 116 | 117 | echo "[options] 118 | runuser = asterisk 119 | rungroup = asterisk" > /etc/asterisk/asterisk.conf 120 | 121 | echo "[general] 122 | [logfiles] 123 | console = verbose,notice,warning,error 124 | messages = notice,warning,error" > /etc/asterisk/logger.conf 125 | 126 | groupadd asterisk 127 | useradd -r -d /var/lib/asterisk -g asterisk asterisk 128 | usermod -aG audio,dialout asterisk 129 | chown -R asterisk.asterisk /etc/asterisk 130 | chown -R asterisk.asterisk /var/{lib,log,spool}/asterisk 131 | 132 | systemctl start asterisk 133 | -------------------------------------------------------------------------------- /week-03/src/createVoiceConnector.py: -------------------------------------------------------------------------------- 1 | 2 | import json 3 | import boto3 4 | import time 5 | import uuid 6 | 7 | chime = boto3.client('chime') 8 | 9 | def authorizeEIP (voiceConnectorId, elasticIP): 10 | response = chime.put_voice_connector_origination( 11 | VoiceConnectorId=voiceConnectorId, 12 | Origination={ 13 | 'Routes': [ 14 | { 15 | 'Host': elasticIP, 16 | 'Port': 5060, 17 | 'Protocol': 'UDP', 18 | 'Priority': 1, 19 | 'Weight': 1 20 | }, 21 | ], 22 | 'Disabled': False 23 | } 24 | ) 25 | print(response) 26 | 27 | response = chime.put_voice_connector_termination( 28 | VoiceConnectorId=voiceConnectorId, 29 | Termination={ 30 | 'CpsLimit': 1, 31 | 'CallingRegions': [ 32 | 'US', 33 | ], 34 | 'CidrAllowedList': [ 35 | elasticIP + '/32', 36 | ], 37 | 'Disabled': False 38 | } 39 | ) 40 | print(response) 41 | 42 | 43 | def getPhoneNumber (): 44 | search_response = chime.search_available_phone_numbers( 45 | # AreaCode='string', 46 | # City='string', 47 | # Country='string', 48 | State='IL', 49 | # TollFreePrefix='string', 50 | MaxResults=1 51 | ) 52 | phoneNumberToOrder = search_response['E164PhoneNumbers'][0] 53 | print ('Phone Number: ' + phoneNumberToOrder) 54 | phone_order = chime.create_phone_number_order( 55 | ProductType='VoiceConnector', 56 | E164PhoneNumbers=[ 57 | phoneNumberToOrder, 58 | ] 59 | ) 60 | print ('Phone Order: ' + str(phone_order)) 61 | 62 | check_phone_order = chime.get_phone_number_order( 63 | PhoneNumberOrderId=phone_order['PhoneNumberOrder']['PhoneNumberOrderId'] 64 | ) 65 | order_status = check_phone_order['PhoneNumberOrder']['Status'] 66 | timeout = 0 67 | 68 | while not order_status == 'Successful': 69 | timeout += 1 70 | print('Checking status: ' + str(order_status)) 71 | time.sleep(5) 72 | check_phone_order = chime.get_phone_number_order( 73 | PhoneNumberOrderId=phone_order['PhoneNumberOrder']['PhoneNumberOrderId'] 74 | ) 75 | order_status = check_phone_order['PhoneNumberOrder']['Status'] 76 | if timeout == 5: 77 | return 'Could not get phone number' 78 | 79 | return phoneNumberToOrder 80 | 81 | def createVoiceConnector (region, phoneNumber): 82 | print(str(uuid.uuid1())) 83 | print(region) 84 | response = chime.create_voice_connector( 85 | Name='Trunk' + str(uuid.uuid1()), 86 | AwsRegion=region, 87 | RequireEncryption=False 88 | ) 89 | 90 | voiceConnectorId = response['VoiceConnector']['VoiceConnectorId'] 91 | outboundHostName = response['VoiceConnector']['OutboundHostName'] 92 | 93 | response = chime.associate_phone_numbers_with_voice_connector( 94 | VoiceConnectorId=voiceConnectorId, 95 | E164PhoneNumbers=[ 96 | phoneNumber, 97 | ], 98 | ForceAssociate=True 99 | ) 100 | 101 | voiceConnector = { 'voiceConnectorId': voiceConnectorId, 'outboundHostName': outboundHostName, 'phoneNumber': phoneNumber} 102 | return voiceConnector 103 | 104 | def on_event(event, context): 105 | print(event) 106 | request_type = event['RequestType'] 107 | if request_type == 'Create': return on_create(event) 108 | if request_type == 'Update': return on_update(event) 109 | if request_type == 'Delete': return on_delete(event) 110 | raise Exception("Invalid request type: %s" % request_type) 111 | 112 | def on_create(event): 113 | physical_id = 'VoiceConnectorResources' 114 | region = event['ResourceProperties']['region'] 115 | elasticIP = event['ResourceProperties']['eip'] 116 | 117 | newPhoneNumber = getPhoneNumber() 118 | voiceConnector = createVoiceConnector(region, newPhoneNumber) 119 | authorizeEIP(voiceConnector['voiceConnectorId'], elasticIP) 120 | 121 | return { 'PhysicalResourceId': physical_id, 'Data': voiceConnector } 122 | 123 | def on_update(event): 124 | physical_id = event["PhysicalResourceId"] 125 | props = event["ResourceProperties"] 126 | print("update resource %s with props %s" % (physical_id, props)) 127 | return { 'PhysicalResourceId': physical_id } 128 | 129 | 130 | def on_delete(event): 131 | physical_id = event["PhysicalResourceId"] 132 | print("delete resource %s" % physical_id) 133 | return { 'PhysicalResourceId': physical_id } 134 | -------------------------------------------------------------------------------- /week-03/src/process.js: -------------------------------------------------------------------------------- 1 | 2 | const AWS = require('aws-sdk'); 3 | const s3 = new AWS.S3(); 4 | var docClient = new AWS.DynamoDB.DocumentClient(); 5 | const cdrTable = process.env.CDR_TABLE 6 | 7 | async function downloadCDR(event) { 8 | const srcBucket = event.Records[0].s3.bucket.name; 9 | const srcKey = decodeURIComponent(event.Records[0].s3.object.key.replace(/\+/g, " ")); 10 | try { 11 | const params = { 12 | Bucket: srcBucket, 13 | Key: srcKey 14 | }; 15 | var callDetailRecord = await s3.getObject(params).promise(); 16 | } catch (error) { 17 | console.log(error); 18 | } 19 | return callDetailRecord; 20 | } 21 | 22 | async function uploadCDR(callDetailRecord, priceComponents) { 23 | const parsedCallDetailRecord = JSON.parse(callDetailRecord.Body.toString()); 24 | console.log(parsedCallDetailRecord) 25 | console.log(priceComponents) 26 | var params = { 27 | TableName: cdrTable, 28 | Item: { 29 | "callId": parsedCallDetailRecord.CallId, 30 | "transactionId": parsedCallDetailRecord.TransactionId, 31 | "AwsAccountId": parsedCallDetailRecord.AwsAccountId, 32 | "voiceConectorId": parsedCallDetailRecord.VoiceConnectorId, 33 | "status": parsedCallDetailRecord.Status, 34 | "StatusMessage": parsedCallDetailRecord.StatusMessage, 35 | "BillableDurationSeconds": parsedCallDetailRecord.BillableDurationSeconds, 36 | "DestinationPhoneNumber": parsedCallDetailRecord.DestinationPhoneNumber, 37 | "DestinationCountry": parsedCallDetailRecord.DestinationCountry, 38 | "SourcePhoneNumber": parsedCallDetailRecord.SourcePhoneNumber, 39 | "SourceCountry": parsedCallDetailRecord.SourceCountry, 40 | "UsageType": parsedCallDetailRecord.UsageType, 41 | "ServiceCode": parsedCallDetailRecord.ServiceCode, 42 | "Direction": parsedCallDetailRecord.Direction, 43 | "StartTimeEpochSeconds": parsedCallDetailRecord.StartTimeEpochSeconds, 44 | "EndTimeEpochSeconds": parsedCallDetailRecord.EndTimeEpochSeconds, 45 | "Region": parsedCallDetailRecord.Region, 46 | "Streaming": parsedCallDetailRecord.Streaming, 47 | "callCost": priceComponents.callCost, 48 | "pricePerMinute": priceComponents.pricePerMinute, 49 | "currency": priceComponents.currency 50 | } 51 | } 52 | console.log(params) 53 | try { 54 | await docClient.put(params).promise() 55 | console.log("Inserted CDR") 56 | } catch (err) { 57 | console.log(err) 58 | return err 59 | } 60 | } 61 | 62 | async function getPrices(callDetailRecord) { 63 | var parsedCallDetailRecord = JSON.parse(callDetailRecord.Body.toString()); 64 | var params = { 65 | Filters: [ 66 | { 67 | Field: "ServiceCode", 68 | Type: "TERM_MATCH", 69 | Value: "AmazonChimeVoiceConnector" 70 | }, 71 | { 72 | Field: "usagetype", 73 | Type: "TERM_MATCH", 74 | Value: parsedCallDetailRecord.UsageType 75 | } 76 | ], 77 | MaxResults: 1, 78 | ServiceCode: "AmazonChimeVoiceConnector" 79 | }; 80 | var pricing = new AWS.Pricing({ 81 | apiVersion: '2017-10-15', 82 | region: 'us-east-1', 83 | }).getProducts(params); 84 | 85 | var promise = pricing.promise(); 86 | 87 | promise.then( 88 | function(data) { 89 | return data; 90 | }, 91 | function(error) { 92 | console.log(error); 93 | } 94 | ); 95 | return promise; 96 | } 97 | 98 | exports.handler = async function(event, context, callback) { 99 | console.log(event) 100 | const callDetailRecord = await downloadCDR(event); 101 | const pricing = await getPrices(callDetailRecord); 102 | console.log("main pricing: ", pricing); 103 | 104 | var parsedCallDetailRecord = JSON.parse(callDetailRecord.Body.toString()); 105 | var sku = Object.keys(pricing.PriceList[0].terms.OnDemand); 106 | var rateCode = Object.keys(pricing.PriceList[0].terms.OnDemand[sku].priceDimensions); 107 | var currency = Object.keys(pricing.PriceList[0].terms.OnDemand[sku].priceDimensions[rateCode].pricePerUnit); 108 | var pricePerMinute = pricing.PriceList[0].terms.OnDemand[sku].priceDimensions[rateCode].pricePerUnit[currency]; 109 | const callCost = pricePerMinute * parsedCallDetailRecord.BillableDurationSeconds; 110 | var priceComponents = {"currency": currency, "pricePerMinute": pricePerMinute, "callCost": callCost}; 111 | console.log("priceComponents: ", priceComponents); 112 | 113 | const uploadStatus = await uploadCDR(callDetailRecord, priceComponents); 114 | return uploadStatus; 115 | }; -------------------------------------------------------------------------------- /week-03/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2018", 4 | "module": "commonjs", 5 | "lib": ["es2018"], 6 | "declaration": true, 7 | "strict": true, 8 | "noImplicitAny": true, 9 | "strictNullChecks": true, 10 | "noImplicitThis": true, 11 | "alwaysStrict": true, 12 | "noUnusedLocals": false, 13 | "noUnusedParameters": false, 14 | "noImplicitReturns": true, 15 | "noFallthroughCasesInSwitch": false, 16 | "inlineSourceMap": true, 17 | "inlineSources": true, 18 | "experimentalDecorators": true, 19 | "strictPropertyInitialization": false, 20 | "typeRoots": ["./node_modules/@types"] 21 | }, 22 | "exclude": ["cdk.out"] 23 | } 24 | -------------------------------------------------------------------------------- /week-04/.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | *.d.ts 3 | node_modules 4 | !src/*.js 5 | 6 | # CDK asset staging directory 7 | .cdk.staging 8 | cdk.out 9 | 10 | 11 | *.pcap 12 | *.pem -------------------------------------------------------------------------------- /week-04/README.md: -------------------------------------------------------------------------------- 1 | # Chime Voice Connector CDR Analysis and Metadata Parsing 2 | 3 | This demo will build and configure several services to explore CDR analysis and metadata parsing from SIP INVITEs using VoIP on AWS. 4 | 5 | ## Requirements 6 | - AWS CLI [installed](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2-linux.html) 7 | - node/npm [installed](https://github.com/nodesource/distributions/blob/master/README.md) 8 | - AWS CDK [installed](https://docs.aws.amazon.com/cdk/latest/guide/getting_started.html) and [bootstrapped](https://docs.aws.amazon.com/cdk/latest/guide/bootstrapping.html) 9 | - Ability to create a VPC and EIP within that VPC (ensure your [Service Quota](https://console.aws.amazon.com/servicequotas/) for EIP is not reached) 10 | - Ability to create a Chime Voice Connector (ensure your Service Quota for VC is not reached) 11 | ## Deployment 12 | 13 | - Clone this repo 14 | - `chmod +x deploy.sh` 15 | - `./deploy.sh` 16 | 17 | This script will ensure that all dependencies are installed and ready for the CDK deployment. If you get a schema related error, please uninstall and re-install aws-cdk to get to the latest version. 18 | ``` 19 | npm uninstall -g aws-cdk 20 | npm install -g aws-cdk 21 | ``` 22 | 23 | ## Making a Call 24 | 25 | Please see the directions outlined in [Week-01](https://github.com/aws-samples/building-with-amazon-chime/tree/main/week-01#configuring-a-client) for configuring the client and placing a call. An outbound call from the Asterisk to a PSTN phone will be required to see the results of the parsing function. 26 | 27 | ## Overview 28 | 29 | ![Diagram](images/Week-04-Diagram.png) 30 | 31 | 32 | ## Functions 33 | 34 | Two Lambda functions are used in this demo. The process_cdrs.js function will trigger on updates to the S3 bucket used by Amazon Chime Voice Connector. This bucket is created as part of the CDK deployment and assocaited in the global settings in the Amazon Chime console. When new CDRs are generated, they will be put in the S3 bucket in JSON format. This will trigger the Lambda to run and process the contents, look up the cost of this call, and store the results in a Dynamo DB. 35 | 36 | The second function operates in a similar fashion. SIP message logs have been enabled for this Voice Connector. When a call is placed and SIP messages are generated, these messages will be stored in a CloudWatch log group associated with the Voice Connector. A CloudWatc subscription filter has been enabled that will send logs that contain "INVITE sip" to the Lambda. This lambda will then parse the log and look for a specific header. 37 | 38 | In this case, the Asterisk has been configured with: 39 | ``` 40 | [HeaderSupport] 41 | exten => addheader,1,Set(PJSIP_HEADER(add,X-Header-Support)=${UNIQUEID}) 42 | 43 | [outbound_phone] 44 | exten => _+X.,1,NoOP(Outbound Normal) 45 | same => n,Dial(PJSIP/${EXTEN}@VoiceConnector,20,b(HeaderSupport^addheader^1)) 46 | same => n,Congestion 47 | ``` 48 | 49 | This will cause the INVITE sent from the Asterisk to add an X-Header with a unique value. 50 | 51 | When this INVITE is captured in the CloudWatch logs, the process_log.py Lambda will parse the log looking for `X-Header-Support` with the following code: 52 | 53 | ``` 54 | sip_message = str(x['message']) 55 | x_header_start = sip_message.find('X-Header-Support') 56 | if not x_header_start == -1: 57 | x_header = sip_message[(x_header_start+18):(x_header_start+30)] 58 | print("X-Header: " + x_header) 59 | ``` 60 | This lambda is just capturing the output in the logs of the Lambda, but this data could be used in many other AWS services. 61 | 62 | # Destroying This Install 63 | 64 | To clean up after you're done with this demo, you can run `cdk destroy`. This will remove most of the components that were created, but you will need to remove the Voice Connector and Phone Number associated manually within the AWS [Chime Console](https://console.chime.aws.amazon.com/). Additionally, the S3 bucket created to store the CDRs will need to be emptied and deleted. -------------------------------------------------------------------------------- /week-04/bin/asterisk_parsing.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from '@aws-cdk/core'; 2 | import { AsteriskParsing } from '../lib/asterisk_parsing'; 3 | 4 | const app = new cdk.App(); 5 | 6 | const SecurityGroupIP = app.node.tryGetContext('SecurityGroupIP') || '10.10.10.10' 7 | 8 | new AsteriskParsing(app, 'AsteriskParsing', { 9 | SecurityGroupIP: SecurityGroupIP 10 | }); 11 | -------------------------------------------------------------------------------- /week-04/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node --prefer-ts-exts bin/asterisk_parsing.ts", 3 | "context": { 4 | "@aws-cdk/aws-apigateway:usagePlanKeyOrderInsensitiveId": true, 5 | "@aws-cdk/core:enableStackNameDuplicates": "true", 6 | "aws-cdk:enableDiffNoFail": "true", 7 | "@aws-cdk/core:stackRelativeExports": "true", 8 | "@aws-cdk/aws-ecr-assets:dockerIgnoreSupport": true, 9 | "@aws-cdk/aws-secretsmanager:parseOwnedSecretName": true, 10 | "@aws-cdk/aws-kms:defaultKeyPolicies": true, 11 | "@aws-cdk/aws-s3:grantWriteWithoutAcl": true, 12 | "@aws-cdk/aws-ecs-patterns:removeDefaultDesiredCount": true, 13 | "@aws-cdk/aws-rds:lowercaseDbIdentifier": true, 14 | "@aws-cdk/aws-efs:defaultEncryptionAtRest": true 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /week-04/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | function valid_ip() 3 | { 4 | local ip=$1 5 | local stat=1 6 | 7 | if [[ $ip =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then 8 | OIFS=$IFS 9 | IFS='.' 10 | ip=($ip) 11 | IFS=$OIFS 12 | [[ ${ip[0]} -le 255 && ${ip[1]} -le 255 \ 13 | && ${ip[2]} -le 255 && ${ip[3]} -le 255 ]] 14 | stat=$? 15 | fi 16 | return $stat 17 | } 18 | ExternalIP='' 19 | 20 | if [ -f "cdk.context.json" ]; then 21 | echo "" 22 | echo "INFO: Removing cdk.context.json" 23 | rm cdk.context.json 24 | else 25 | echo "" 26 | echo "INFO: cdk.context.json not present, nothing to remove" 27 | fi 28 | if [ ! -f "package-lock.json" ]; then 29 | echo "" 30 | echo "Installing Packages" 31 | echo "" 32 | npm install 33 | fi 34 | echo "" 35 | echo "Getting External IP address for Security Group" 36 | echo "" 37 | if [ -x "$(which curl)" ] ; then 38 | echo "Using Curl" 39 | ExternalIP=$( curl checkip.amazonaws.com ) 40 | echo "External IP: " $ExternalIP 41 | else 42 | while true; do 43 | read -p "Enter IPv4 address to allow access in Security Group: " ExternalIP 44 | if valid_ip $ExternalIP; then 45 | break 46 | else 47 | echo "Please enter a valid IPv4 address in the form of XXX.XXX.XXX.XXX" 48 | fi 49 | done 50 | fi 51 | echo "" 52 | echo "Building CDK" 53 | echo "" 54 | npm run build 55 | echo "" 56 | echo "Deploying CDK" 57 | echo "" 58 | cdk deploy -c SecurityGroupIP=$ExternalIP 59 | -------------------------------------------------------------------------------- /week-04/images/Week-04-Diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/building-with-amazon-chime/969c1c7a36fd7e4e982922290a05ec9ad5b420fc/week-04/images/Week-04-Diagram.png -------------------------------------------------------------------------------- /week-04/lib/asterisk_parsing.ts: -------------------------------------------------------------------------------- 1 | import * as ec2 from "@aws-cdk/aws-ec2"; 2 | import * as cdk from '@aws-cdk/core'; 3 | import { KeyPair } from 'cdk-ec2-key-pair'; 4 | import { Asset } from '@aws-cdk/aws-s3-assets'; 5 | import * as path from 'path'; 6 | import * as ssm from '@aws-cdk/aws-ssm'; 7 | import * as iam from '@aws-cdk/aws-iam'; 8 | import { CustomResource, Duration } from '@aws-cdk/core'; 9 | import lambda = require('@aws-cdk/aws-lambda'); 10 | import custom = require('@aws-cdk/custom-resources') 11 | import dynamodb = require('@aws-cdk/aws-dynamodb'); 12 | import s3 = require('@aws-cdk/aws-s3'); 13 | import { S3EventSource } from '@aws-cdk/aws-lambda-event-sources'; 14 | import * as logs from '@aws-cdk/aws-logs' 15 | import { LambdaDestination } from '@aws-cdk/aws-logs-destinations' 16 | export interface StackProps { 17 | SecurityGroupIP: string; 18 | } 19 | export class AsteriskParsing extends cdk.Stack { 20 | constructor(scope: cdk.Construct, id: string, props: StackProps) { 21 | super(scope, id); 22 | 23 | const cdrTable = new dynamodb.Table(this, 'cdrTable', { 24 | partitionKey: { 25 | name: 'callId', 26 | type: dynamodb.AttributeType.STRING 27 | }, 28 | removalPolicy: cdk.RemovalPolicy.DESTROY, 29 | billingMode: dynamodb.BillingMode.PAY_PER_REQUEST, 30 | }); 31 | 32 | const cdrBucket = new s3.Bucket(this, 'cdrBucket', { 33 | }); 34 | 35 | const processCDRsLambda = new lambda.Function(this, 'process_cdrs', { 36 | code: lambda.Code.fromAsset("src", {exclude: ["**", "!process_cdrs.js"]}), 37 | handler: 'process_cdrs.handler', 38 | runtime: lambda.Runtime.NODEJS_14_X, 39 | timeout: Duration.seconds(60), 40 | role: new iam.Role(this, 'lambdaRole', { 41 | assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), 42 | managedPolicies: [ 43 | iam.ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSLambdaBasicExecutionRole"), 44 | iam.ManagedPolicy.fromAwsManagedPolicyName("AWSPriceListServiceFullAccess")] 45 | }), 46 | environment: { 47 | CDR_TABLE: cdrTable.tableName 48 | }, 49 | }); 50 | 51 | processCDRsLambda.addEventSource(new S3EventSource(cdrBucket, { 52 | events: [ s3.EventType.OBJECT_CREATED] 53 | })); 54 | 55 | cdrTable.grantReadWriteData(processCDRsLambda) 56 | cdrBucket.grantRead(processCDRsLambda) 57 | 58 | // Create a Key Pair to be used with this EC2 Instance 59 | const key = new KeyPair(this, 'KeyPair', { 60 | name: 'cdk-keypair', 61 | description: 'Key Pair created with CDK Deployment', 62 | }); 63 | key.grantReadOnPublicKey 64 | 65 | // Create new VPC with 2 Subnets 66 | const vpc = new ec2.Vpc(this, 'VPC', { 67 | natGateways: 0, 68 | subnetConfiguration: [ { 69 | cidrMask: 24, 70 | name: "asterisk", 71 | subnetType: ec2.SubnetType.PUBLIC 72 | }]}); 73 | 74 | // Allow SSH (TCP Port 22) access from anywhere 75 | const securityGroup = new ec2.SecurityGroup(this, 'SecurityGroup', { 76 | vpc, 77 | description: 'Security Group for Asterisk Server', 78 | allowAllOutbound: true 79 | }); 80 | securityGroup.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(22), 'Allow SSH Access') 81 | securityGroup.addIngressRule(ec2.Peer.ipv4('3.80.16.0/23'), ec2.Port.udp(5060), 'Allow Chime Voice Connector Signaling Access') 82 | securityGroup.addIngressRule(ec2.Peer.ipv4('3.80.16.0/23'), ec2.Port.tcp(5060), 'Allow Chime Voice Connector Signaling Access') 83 | securityGroup.addIngressRule(ec2.Peer.ipv4('3.80.16.0/23'), ec2.Port.tcp(5061), 'Allow Chime Voice Connector Signaling Access') 84 | securityGroup.addIngressRule(ec2.Peer.ipv4('99.77.253.0/24'), ec2.Port.udp(5060), 'Allow Chime Voice Connector Signaling Access') 85 | securityGroup.addIngressRule(ec2.Peer.ipv4('99.77.253.0/24'), ec2.Port.tcp(5060), 'Allow Chime Voice Connector Signaling Access') 86 | securityGroup.addIngressRule(ec2.Peer.ipv4('99.77.253.0/24'), ec2.Port.tcp(5061), 'Allow Chime Voice Connector Signaling Access') 87 | securityGroup.addIngressRule(ec2.Peer.ipv4('99.77.253.0/24'), ec2.Port.udpRange(5000,65000), 'Allow Chime Voice Connector Signaling Access') 88 | securityGroup.addIngressRule(ec2.Peer.ipv4('3.80.16.0/23'), ec2.Port.udpRange(5000,65000), 'Allow Chime Voice Connector Media Access') 89 | securityGroup.addIngressRule(ec2.Peer.ipv4('99.77.253.0/24'), ec2.Port.udpRange(5000,65000), 'Allow Chime Voice Connector Media Access') 90 | securityGroup.addIngressRule(ec2.Peer.ipv4('52.55.62.128/25'), ec2.Port.udpRange(1024,65535), 'Allow Chime Voice Connector Media Access') 91 | securityGroup.addIngressRule(ec2.Peer.ipv4('52.55.63.0/25'), ec2.Port.udpRange(1024,65535), 'Allow Chime Voice Connector Media Access') 92 | securityGroup.addIngressRule(ec2.Peer.ipv4('34.212.95.128/25'), ec2.Port.udpRange(1024,65535), 'Allow Chime Voice Connector Media Access') 93 | securityGroup.addIngressRule(ec2.Peer.ipv4('34.223.21.0/25'), ec2.Port.udpRange(1024,65535), 'Allow Chime Voice Connector Media Access') 94 | securityGroup.addIngressRule(ec2.Peer.ipv4(props.SecurityGroupIP + '/32'), ec2.Port.allTraffic(), 'All inbound traffic from local machine') 95 | 96 | const role = new iam.Role(this, 'ec2Role', { 97 | assumedBy: new iam.ServicePrincipal('ec2.amazonaws.com') 98 | }) 99 | 100 | role.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonSSMManagedInstanceCore')) 101 | 102 | const eip = new ec2.CfnEIP(this, 'EIP') 103 | 104 | // Use Latest Amazon Linux Image - CPU Type ARM64 105 | const ami = new ec2.AmazonLinuxImage({ 106 | generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2, 107 | cpuType: ec2.AmazonLinuxCpuType.ARM_64}); 108 | 109 | const createVoiceConnectorRole = new iam.Role(this, 'createChimeLambdaRole', { 110 | assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), 111 | inlinePolicies: { 112 | ['chimePolicy']: new iam.PolicyDocument( { statements: [new iam.PolicyStatement({ 113 | resources: ['*'], 114 | actions: ['chime:*', 115 | 's3:*', 116 | 'cloudwatch:*', 117 | 'logs:*']})]}) 118 | }, 119 | managedPolicies: [ iam.ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSLambdaBasicExecutionRole") ] 120 | }) 121 | 122 | const createVoiceConnectorLambda = new lambda.Function(this, 'createVCLambda', { 123 | code: lambda.Code.fromAsset("src", {exclude: ["**", "!createVoiceConnector.py"]}), 124 | handler: 'createVoiceConnector.on_event', 125 | runtime: lambda.Runtime.PYTHON_3_8, 126 | role: createVoiceConnectorRole, 127 | timeout: Duration.seconds(60) 128 | }); 129 | 130 | const voiceConnectorProvider = new custom.Provider(this, 'voiceConnectorProvider', { 131 | onEventHandler: createVoiceConnectorLambda 132 | }) 133 | 134 | const voiceConnectorResource = new CustomResource(this, 'voiceConnectorResource', { 135 | serviceToken: voiceConnectorProvider.serviceToken, 136 | properties: { 'region': this.region, 137 | 'eip': eip.ref, 138 | 'cdr_bucket': cdrBucket.bucketName } 139 | }) 140 | 141 | const phoneNumber = voiceConnectorResource.getAttString('phoneNumber') 142 | const voiceConnectorId = voiceConnectorResource.getAttString('voiceConnectorId') 143 | const outboundHostName = voiceConnectorResource.getAttString('outboundHostName') 144 | 145 | const phoneNumberParameter = new ssm.StringParameter(this, 'phoneNumber', { 146 | parameterName: '/asterisk/phoneNumber', 147 | stringValue: phoneNumber, 148 | }); 149 | 150 | const voiceConnectorParameter = new ssm.StringParameter(this, 'voiceConnector', { 151 | parameterName: '/asterisk/voiceConnector', 152 | stringValue: voiceConnectorId 153 | }) 154 | 155 | const outboundHostNameParameter = new ssm.StringParameter(this, 'outboundHostName', { 156 | parameterName: '/asterisk/outboundHostName', 157 | stringValue: outboundHostName 158 | }) 159 | 160 | const voiceConnectorLogGroupName = '/aws/ChimeVoiceConnectorSipMessages/' + voiceConnectorId 161 | const sip_logs = logs.LogGroup.fromLogGroupName(this, 'VoiceConnectorLog', voiceConnectorLogGroupName ) 162 | 163 | const processRole = new iam.Role(this, 'processRole', { 164 | assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com')} 165 | ) 166 | 167 | const processRolePolicy = new iam.PolicyStatement({ 168 | resources: [ sip_logs.logGroupArn ], 169 | actions: ['cloudwatatch:*', 170 | 'logs:*']}) 171 | processRole.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSLambdaBasicExecutionRole")) 172 | processRole.addToPolicy(processRolePolicy) 173 | 174 | const processLogsLambda = new lambda.Function(this, 'processLogsLambda', { 175 | code: lambda.Code.fromAsset("src", {exclude: ["**", "!process_logs.py"]}), 176 | handler: 'process_logs.lambda_handler', 177 | runtime: lambda.Runtime.PYTHON_3_8, 178 | role: processRole, 179 | timeout: Duration.seconds(60) 180 | }); 181 | 182 | sip_logs.addSubscriptionFilter('SubLogs', { 183 | destination: new LambdaDestination(processLogsLambda), 184 | filterPattern: logs.FilterPattern.allTerms("INVITE sip") 185 | }) 186 | 187 | processLogsLambda.addPermission('logsPermissions', { 188 | principal: new iam.ServicePrincipal('logs.us-east-1.amazonaws.com'), 189 | action: 'lambda:invokeFunction', 190 | sourceArn: sip_logs.logGroupArn 191 | }) 192 | 193 | const ec2UserData = ec2.UserData.forLinux(); 194 | 195 | const asteriskConfig = new Asset(this, 'AsteriskConfig', {path: path.join(__dirname, '../src/config.sh')}); 196 | 197 | const configPath = ec2UserData.addS3DownloadCommand({ 198 | bucket:asteriskConfig.bucket, 199 | bucketKey:asteriskConfig.s3ObjectKey, 200 | }); 201 | 202 | ec2UserData.addExecuteFileCommand({ 203 | filePath:configPath, 204 | arguments: '--verbose -y' 205 | }); 206 | 207 | asteriskConfig.grantRead(role); 208 | phoneNumberParameter.grantRead(role); 209 | voiceConnectorParameter.grantRead(role); 210 | outboundHostNameParameter.grantRead(role); 211 | 212 | // Create the instance using the Security Group, AMI, and KeyPair defined in the VPC created 213 | const ec2Instance = new ec2.Instance(this, 'Instance', { 214 | vpc, 215 | instanceType: ec2.InstanceType.of(ec2.InstanceClass.T4G, ec2.InstanceSize.LARGE), 216 | machineImage: ami, 217 | securityGroup: securityGroup, 218 | keyName: key.keyPairName, 219 | role: role, 220 | userData: ec2UserData, 221 | }); 222 | 223 | new ec2.CfnEIPAssociation(this, "EIP Association", { 224 | eip: eip.ref, 225 | instanceId: ec2Instance.instanceId 226 | }) 227 | 228 | 229 | // Create outputs for connecting 230 | new cdk.CfnOutput(this, 'IP Address', { value: ec2Instance.instancePublicIp }); 231 | new cdk.CfnOutput(this, 'Download Key Command', { value: 'aws secretsmanager get-secret-value --secret-id ec2-ssh-key/cdk-keypair/private --query SecretString --output text > cdk-key.pem && chmod 400 cdk-key.pem' }) 232 | new cdk.CfnOutput(this, 'ssh command', { value: 'ssh -i cdk-key.pem -o IdentitiesOnly=yes ec2-user@' + ec2Instance.instancePublicIp }) 233 | new cdk.CfnOutput(this, 'PhoneNumber', { value: phoneNumber}), 234 | new cdk.CfnOutput(this, 'VoiceConnector', { value: outboundHostName}) 235 | new cdk.CfnOutput(this, 'VoiceConnectorLogs', { value: '/aws/ChimeVoiceConnectorLogs/' + voiceConnectorId}) 236 | new cdk.CfnOutput(this, 'VoiceConnectorSipMessages', { value: '/aws/ChimeVoiceConnectorSipMessages/' + voiceConnectorId}) 237 | new cdk.CfnOutput(this, 'Update Security Group', { value: 'aws ec2 authorize-security-group-ingress --group-id ' + securityGroup.securityGroupId + ' --protocol -1 --cidr YOUR_PUBLIC_IP/32'}) 238 | } 239 | } -------------------------------------------------------------------------------- /week-04/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "asterisk_cdr", 3 | "version": "0.1.0", 4 | "bin": { 5 | "asterisk_cdr": "bin/asterisk_cdr.js" 6 | }, 7 | "scripts": { 8 | "build": "tsc", 9 | "watch": "tsc -w", 10 | "cdk": "cdk" 11 | }, 12 | "devDependencies": { 13 | "@aws-cdk/assert": "1.107.0", 14 | "@types/jest": "^26.0.23", 15 | "@types/node": "15.12.1", 16 | "ts-node": "^10.0.0", 17 | "typescript": "~4.3.2" 18 | }, 19 | "dependencies": { 20 | "@aws-cdk/aws-dynamodb": "1.107.0", 21 | "@aws-cdk/aws-ec2": "1.107.0", 22 | "@aws-cdk/aws-lambda-event-sources": "1.107.0", 23 | "@aws-cdk/aws-logs-destinations": "1.107.0", 24 | "@aws-cdk/core": "1.107.0", 25 | "@aws-cdk/custom-resources": "1.107.0", 26 | "aws-cdk": "^1.107.0", 27 | "cdk-ec2-key-pair": "^2.2.1", 28 | "source-map-support": "^0.5.19" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /week-04/src/config.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -xe 2 | HOMEDIR=/home/ec2-user 3 | yum update -y 4 | yum install net-tools -y 5 | yum install wget -y 6 | yum -y install make gcc gcc-c++ make subversion libxml2-devel ncurses-devel openssl-devel vim-enhanced man glibc-devel autoconf libnewt kernel-devel kernel-headers linux-headers openssl-devel zlib-devel libsrtp libsrtp-devel uuid libuuid-devel mariadb-server jansson-devel libsqlite3x libsqlite3x-devel epel-release.noarch bash-completion bash-completion-extras unixODBC unixODBC-devel libtool-ltdl libtool-ltdl-devel mysql-connector-odbc mlocate libiodbc sqlite sqlite-devel sql-devel.i686 sqlite-doc.noarch sqlite-tcl.x86_64 patch libedit-devel jq 7 | cd /tmp 8 | wget https://downloads.asterisk.org/pub/telephony/asterisk/asterisk-16-current.tar.gz 9 | tar xvzf asterisk-16-current.tar.gz 10 | cd asterisk-16*/ 11 | ./configure --libdir=/usr/lib64 --with-jansson-bundled 12 | make menuselect.makeopts 13 | menuselect/menuselect \ 14 | --disable BUILD_NATIVE \ 15 | --disable chan_sip \ 16 | --disable chan_skinny \ 17 | --enable cdr_csv \ 18 | --enable res_snmp \ 19 | --enable res_http_websocket \ 20 | menuselect.makeopts 21 | make 22 | make install 23 | make basic-pbx 24 | touch /etc/redhat-release 25 | make config 26 | ldconfig 27 | 28 | IP=$( curl http://169.254.169.254/latest/meta-data/public-ipv4 ) 29 | REGION=$( curl http://169.254.169.254/latest/meta-data/placement/region ) 30 | PhoneNumber=$( aws ssm get-parameter --name /asterisk/phoneNumber --region $REGION | jq -r '.Parameter.Value' ) 31 | VoiceConnectorHost=$( aws ssm get-parameter --name /asterisk/voiceConnector --region $REGION | jq -r '.Parameter.Value' ) 32 | OutboundHostName=$( aws ssm get-parameter --name /asterisk/outboundHostName --region $REGION | jq -r '.Parameter.Value' ) 33 | 34 | echo "[udp] 35 | type=transport 36 | protocol=udp 37 | bind=0.0.0.0 38 | external_media_address=$IP 39 | external_signaling_address=$IP 40 | allow_reload=yes 41 | 42 | [VoiceConnector] 43 | type=endpoint 44 | context=from-voiceConnector 45 | transport=udp 46 | disallow=all 47 | allow=ulaw 48 | aors=VoiceConnector 49 | direct_media=no 50 | ice_support=yes 51 | force_rport=yes 52 | 53 | [VoiceConnector] 54 | type=identify 55 | endpoint=VoiceConnector 56 | match=$OutboundHostName 57 | 58 | [VoiceConnector] 59 | type=aor 60 | contact=sip:$OutboundHostName 61 | 62 | [$PhoneNumber] 63 | type=endpoint 64 | context=from-phone 65 | disallow=all 66 | allow=ulaw 67 | transport=udp 68 | auth=$PhoneNumber 69 | aors=$PhoneNumber 70 | send_pai=yes 71 | direct_media=no 72 | rewrite_contact=yes 73 | ice_support=yes 74 | force_rport=yes 75 | 76 | [$PhoneNumber] 77 | type=auth 78 | auth_type=userpass 79 | password=ChimeDemo 80 | username=$PhoneNumber 81 | 82 | [$PhoneNumber] 83 | type=aor 84 | max_contacts=5" > /etc/asterisk/pjsip.conf 85 | 86 | echo "; extensions.conf - the Asterisk dial plan 87 | ; 88 | [general] 89 | static=yes 90 | writeprotect=no 91 | clearglobalvars=no 92 | 93 | [catch-all] 94 | exten => _[+0-9].,1,Answer() 95 | exten => _[+0-9].,n,Wait(1) 96 | exten => _[+0-9].,n,Playback(hello-world) 97 | exten => _[+0-9].,n,Wait(1) 98 | exten => _[+0-9].,n,echo() 99 | exten => _[+0-9].,n,Wait(1) 100 | exten => _[+0-9].,n,Hangup() 101 | 102 | [from-phone] 103 | include => outbound_phone 104 | 105 | [HeaderSupport] 106 | exten => addheader,1,Set(PJSIP_HEADER(add,X-Header-Support)=\${UNIQUEID}) 107 | 108 | [outbound_phone] 109 | exten => _+X.,1,NoOP(Outbound Normal) 110 | same => n,Dial(PJSIP/\${EXTEN}@VoiceConnector,20,b(HeaderSupport^addheader^1)) 111 | same => n,Congestion 112 | 113 | [from-voiceConnector] 114 | include => phones 115 | include => catch-all 116 | 117 | [phones] 118 | exten => $PhoneNumber,1,Dial(PJSIP/$PhoneNumber)" > /etc/asterisk/extensions.conf 119 | 120 | echo "[options] 121 | runuser = asterisk 122 | rungroup = asterisk" > /etc/asterisk/asterisk.conf 123 | 124 | echo "[general] 125 | [logfiles] 126 | console = verbose,notice,warning,error 127 | messages = notice,warning,error" > /etc/asterisk/logger.conf 128 | 129 | groupadd asterisk 130 | useradd -r -d /var/lib/asterisk -g asterisk asterisk 131 | usermod -aG audio,dialout asterisk 132 | chown -R asterisk.asterisk /etc/asterisk 133 | chown -R asterisk.asterisk /var/{lib,log,spool}/asterisk 134 | 135 | systemctl start asterisk 136 | -------------------------------------------------------------------------------- /week-04/src/createVoiceConnector.py: -------------------------------------------------------------------------------- 1 | 2 | import json 3 | import boto3 4 | import time 5 | import uuid 6 | 7 | chime = boto3.client('chime') 8 | 9 | def authorizeEIP (voiceConnectorId, elasticIP): 10 | response = chime.put_voice_connector_origination( 11 | VoiceConnectorId=voiceConnectorId, 12 | Origination={ 13 | 'Routes': [ 14 | { 15 | 'Host': elasticIP, 16 | 'Port': 5060, 17 | 'Protocol': 'UDP', 18 | 'Priority': 1, 19 | 'Weight': 1 20 | }, 21 | ], 22 | 'Disabled': False 23 | } 24 | ) 25 | print(response) 26 | 27 | response = chime.put_voice_connector_termination( 28 | VoiceConnectorId=voiceConnectorId, 29 | Termination={ 30 | 'CpsLimit': 1, 31 | 'CallingRegions': [ 32 | 'US', 33 | ], 34 | 'CidrAllowedList': [ 35 | elasticIP + '/32', 36 | ], 37 | 'Disabled': False 38 | } 39 | ) 40 | print(response) 41 | 42 | 43 | def getPhoneNumber (): 44 | search_response = chime.search_available_phone_numbers( 45 | # AreaCode='string', 46 | # City='string', 47 | # Country='string', 48 | State='IL', 49 | # TollFreePrefix='string', 50 | MaxResults=1 51 | ) 52 | phoneNumberToOrder = search_response['E164PhoneNumbers'][0] 53 | print ('Phone Number: ' + phoneNumberToOrder) 54 | phone_order = chime.create_phone_number_order( 55 | ProductType='VoiceConnector', 56 | E164PhoneNumbers=[ 57 | phoneNumberToOrder, 58 | ] 59 | ) 60 | print ('Phone Order: ' + str(phone_order)) 61 | 62 | check_phone_order = chime.get_phone_number_order( 63 | PhoneNumberOrderId=phone_order['PhoneNumberOrder']['PhoneNumberOrderId'] 64 | ) 65 | order_status = check_phone_order['PhoneNumberOrder']['Status'] 66 | timeout = 0 67 | 68 | while not order_status == 'Successful': 69 | timeout += 1 70 | print('Checking status: ' + str(order_status)) 71 | time.sleep(5) 72 | check_phone_order = chime.get_phone_number_order( 73 | PhoneNumberOrderId=phone_order['PhoneNumberOrder']['PhoneNumberOrderId'] 74 | ) 75 | order_status = check_phone_order['PhoneNumberOrder']['Status'] 76 | if timeout == 5: 77 | return 'Could not get phone number' 78 | 79 | return phoneNumberToOrder 80 | 81 | def createVoiceConnector (region, phoneNumber, cdr_bucket): 82 | print(str(uuid.uuid1())) 83 | print(region) 84 | response = chime.create_voice_connector( 85 | Name='Trunk' + str(uuid.uuid1()), 86 | AwsRegion=region, 87 | RequireEncryption=False 88 | ) 89 | 90 | voiceConnectorId = response['VoiceConnector']['VoiceConnectorId'] 91 | outboundHostName = response['VoiceConnector']['OutboundHostName'] 92 | 93 | chime.put_voice_connector_logging_configuration( 94 | VoiceConnectorId=voiceConnectorId, 95 | LoggingConfiguration={ 96 | 'EnableSIPLogs': True 97 | }) 98 | 99 | chime.update_global_settings( 100 | BusinessCalling={ 101 | 'CdrBucket': cdr_bucket 102 | }, 103 | VoiceConnector={ 104 | 'CdrBucket': cdr_bucket 105 | } 106 | ) 107 | 108 | 109 | response = chime.associate_phone_numbers_with_voice_connector( 110 | VoiceConnectorId=voiceConnectorId, 111 | E164PhoneNumbers=[ 112 | phoneNumber, 113 | ], 114 | ForceAssociate=True 115 | ) 116 | 117 | voiceConnector = { 'voiceConnectorId': voiceConnectorId, 'outboundHostName': outboundHostName, 'phoneNumber': phoneNumber} 118 | return voiceConnector 119 | 120 | def on_event(event, context): 121 | print(event) 122 | request_type = event['RequestType'] 123 | if request_type == 'Create': return on_create(event) 124 | if request_type == 'Update': return on_update(event) 125 | if request_type == 'Delete': return on_delete(event) 126 | raise Exception("Invalid request type: %s" % request_type) 127 | 128 | def on_create(event): 129 | physical_id = 'VoiceConnectorResources' 130 | region = event['ResourceProperties']['region'] 131 | elasticIP = event['ResourceProperties']['eip'] 132 | cdr_bucket = event['ResourceProperties']['cdr_bucket'] 133 | 134 | newPhoneNumber = getPhoneNumber() 135 | voiceConnector = createVoiceConnector(region, newPhoneNumber, cdr_bucket) 136 | authorizeEIP(voiceConnector['voiceConnectorId'], elasticIP) 137 | 138 | return { 'PhysicalResourceId': physical_id, 'Data': voiceConnector } 139 | 140 | def on_update(event): 141 | physical_id = event["PhysicalResourceId"] 142 | props = event["ResourceProperties"] 143 | print("update resource %s with props %s" % (physical_id, props)) 144 | return { 'PhysicalResourceId': physical_id } 145 | 146 | 147 | def on_delete(event): 148 | physical_id = event["PhysicalResourceId"] 149 | print("delete resource %s" % physical_id) 150 | return { 'PhysicalResourceId': physical_id } 151 | -------------------------------------------------------------------------------- /week-04/src/process_cdrs.js: -------------------------------------------------------------------------------- 1 | 2 | const AWS = require('aws-sdk'); 3 | const s3 = new AWS.S3(); 4 | var docClient = new AWS.DynamoDB.DocumentClient(); 5 | const cdrTable = process.env.CDR_TABLE 6 | 7 | async function downloadCDR(event) { 8 | const srcBucket = event.Records[0].s3.bucket.name; 9 | const srcKey = decodeURIComponent(event.Records[0].s3.object.key.replace(/\+/g, " ")); 10 | try { 11 | const params = { 12 | Bucket: srcBucket, 13 | Key: srcKey 14 | }; 15 | var callDetailRecord = await s3.getObject(params).promise(); 16 | } catch (error) { 17 | console.log(error); 18 | } 19 | return callDetailRecord; 20 | } 21 | 22 | async function uploadCDR(callDetailRecord, priceComponents) { 23 | const parsedCallDetailRecord = JSON.parse(callDetailRecord.Body.toString()); 24 | console.log(parsedCallDetailRecord) 25 | console.log(priceComponents) 26 | var params = { 27 | TableName: cdrTable, 28 | Item: { 29 | "callId": parsedCallDetailRecord.CallId, 30 | "transactionId": parsedCallDetailRecord.TransactionId, 31 | "AwsAccountId": parsedCallDetailRecord.AwsAccountId, 32 | "voiceConectorId": parsedCallDetailRecord.VoiceConnectorId, 33 | "status": parsedCallDetailRecord.Status, 34 | "StatusMessage": parsedCallDetailRecord.StatusMessage, 35 | "BillableDurationSeconds": parsedCallDetailRecord.BillableDurationSeconds, 36 | "DestinationPhoneNumber": parsedCallDetailRecord.DestinationPhoneNumber, 37 | "DestinationCountry": parsedCallDetailRecord.DestinationCountry, 38 | "SourcePhoneNumber": parsedCallDetailRecord.SourcePhoneNumber, 39 | "SourceCountry": parsedCallDetailRecord.SourceCountry, 40 | "UsageType": parsedCallDetailRecord.UsageType, 41 | "ServiceCode": parsedCallDetailRecord.ServiceCode, 42 | "Direction": parsedCallDetailRecord.Direction, 43 | "StartTimeEpochSeconds": parsedCallDetailRecord.StartTimeEpochSeconds, 44 | "EndTimeEpochSeconds": parsedCallDetailRecord.EndTimeEpochSeconds, 45 | "Region": parsedCallDetailRecord.Region, 46 | "Streaming": parsedCallDetailRecord.Streaming, 47 | "callCost": priceComponents.callCost, 48 | "pricePerMinute": priceComponents.pricePerMinute, 49 | "currency": priceComponents.currency 50 | } 51 | } 52 | console.log(params) 53 | try { 54 | await docClient.put(params).promise() 55 | console.log("Inserted CDR") 56 | } catch (err) { 57 | console.log(err) 58 | return err 59 | } 60 | } 61 | 62 | async function getPrices(callDetailRecord) { 63 | var parsedCallDetailRecord = JSON.parse(callDetailRecord.Body.toString()); 64 | var params = { 65 | Filters: [ 66 | { 67 | Field: "ServiceCode", 68 | Type: "TERM_MATCH", 69 | Value: "AmazonChimeVoiceConnector" 70 | }, 71 | { 72 | Field: "usagetype", 73 | Type: "TERM_MATCH", 74 | Value: parsedCallDetailRecord.UsageType 75 | } 76 | ], 77 | MaxResults: 1, 78 | ServiceCode: "AmazonChimeVoiceConnector" 79 | }; 80 | var pricing = new AWS.Pricing({ 81 | apiVersion: '2017-10-15', 82 | region: 'us-east-1', 83 | }).getProducts(params); 84 | 85 | var promise = pricing.promise(); 86 | 87 | promise.then( 88 | function(data) { 89 | return data; 90 | }, 91 | function(error) { 92 | console.log(error); 93 | } 94 | ); 95 | return promise; 96 | } 97 | 98 | exports.handler = async function(event, context, callback) { 99 | console.log(event) 100 | const callDetailRecord = await downloadCDR(event); 101 | const pricing = await getPrices(callDetailRecord); 102 | console.log("main pricing: ", pricing); 103 | 104 | var parsedCallDetailRecord = JSON.parse(callDetailRecord.Body.toString()); 105 | var sku = Object.keys(pricing.PriceList[0].terms.OnDemand); 106 | var rateCode = Object.keys(pricing.PriceList[0].terms.OnDemand[sku].priceDimensions); 107 | var currency = Object.keys(pricing.PriceList[0].terms.OnDemand[sku].priceDimensions[rateCode].pricePerUnit); 108 | var pricePerMinute = pricing.PriceList[0].terms.OnDemand[sku].priceDimensions[rateCode].pricePerUnit[currency]; 109 | const callCost = pricePerMinute * parsedCallDetailRecord.BillableDurationSeconds; 110 | var priceComponents = {"currency": currency, "pricePerMinute": pricePerMinute, "callCost": callCost}; 111 | console.log("priceComponents: ", priceComponents); 112 | 113 | const uploadStatus = await uploadCDR(callDetailRecord, priceComponents); 114 | return uploadStatus; 115 | }; -------------------------------------------------------------------------------- /week-04/src/process_logs.py: -------------------------------------------------------------------------------- 1 | import gzip 2 | import json 3 | import base64 4 | 5 | 6 | def lambda_handler(event, context): 7 | log_data = event['awslogs']['data'] 8 | compressed_data = base64.b64decode(log_data) 9 | uncompressed_data = gzip.decompress(compressed_data) 10 | logs = json.loads(uncompressed_data) 11 | log_events = logs['logEvents'] 12 | for x in log_events: 13 | print(x) 14 | sip_message = str(x['message']) 15 | x_header_start = sip_message.find('X-Header-Support') 16 | if not x_header_start == -1: 17 | x_header = sip_message[(x_header_start+18):(x_header_start+30)] 18 | print("X-Header: " + x_header) -------------------------------------------------------------------------------- /week-04/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2018", 4 | "module": "commonjs", 5 | "lib": ["es2018"], 6 | "declaration": true, 7 | "strict": true, 8 | "noImplicitAny": true, 9 | "strictNullChecks": true, 10 | "noImplicitThis": true, 11 | "alwaysStrict": true, 12 | "noUnusedLocals": false, 13 | "noUnusedParameters": false, 14 | "noImplicitReturns": true, 15 | "noFallthroughCasesInSwitch": false, 16 | "inlineSourceMap": true, 17 | "inlineSources": true, 18 | "experimentalDecorators": true, 19 | "strictPropertyInitialization": false, 20 | "typeRoots": ["./node_modules/@types"] 21 | }, 22 | "exclude": ["cdk.out"] 23 | } 24 | -------------------------------------------------------------------------------- /week-05/.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | !jest.config.js 3 | *.d.ts 4 | node_modules 5 | 6 | # CDK asset staging directory 7 | .cdk.staging 8 | cdk.out 9 | 10 | package-lock.json 11 | 12 | !SBC_Config/buildSBCConfig.js 13 | cdk-outputs.json 14 | SBC_Config.ini 15 | 16 | *.ovpn 17 | *.pem 18 | *.drawio 19 | -------------------------------------------------------------------------------- /week-05/.npmignore: -------------------------------------------------------------------------------- 1 | *.ts 2 | !*.d.ts 3 | 4 | # CDK asset staging directory 5 | .cdk.staging 6 | cdk.out 7 | -------------------------------------------------------------------------------- /week-05/README.md: -------------------------------------------------------------------------------- 1 | # Amazon Chime Voice Connector with an SBC 2 | 3 | This week's episode builds upon many of the previous versions. Check the requirements and processes [here](https://github.com/aws-samples/building-with-amazon-chime/tree/main/week-01#chime-voice-connector-with-asterisk-demo) to get the basic deployment done as well as the configuration of the VoIP client. However, there will be a few notable changes. 4 | 5 | ## Overview 6 | ![Overview](images/Week-05-Overview.png) 7 | 8 | This is a fairly major overhaul of the VoIP connectivity with the addition of a VPN and SBC. This is being done to more accurately reflect common design scenarios. 9 | ### VPN Connectivity 10 | 11 | In this demo, an OpenVPN server is added to the Asterisk server. This is not how a deployment would likely look in production, but serves as a way to emulate a private PBX network. 12 | 13 | OpenVPN will be installed and configure on the Asterisk server. Then, a profile will be created that you can download and use in a local VPN client. 14 | 15 | The script to deploy OpenVPN can be found [here](https://github.com/angristan/openvpn-install) and is part of the Asterisk build already. The CDK output includes a command that will allow you to download the created profile from the Asterisk server. The OpenVPN client can be found [here](https://openvpn.net/vpn-client/) 16 | 17 | #### OpenVPN Info 18 | Import the Profile: 19 | ![Import](images/ImportProfile.png) 20 | 21 | Connect: 22 | ![Connect](images/ConnectVPN.png) 23 | 24 | 25 | ### SBC Deployment 26 | 27 | The largest component added here is the AudioCodes SBC. One important not regarding this demo is that it requires you to subscribe to the AudioCode PAYG Product in [Amazon MarketPlace](https://aws.amazon.com/marketplace/pp/prodview-4wxi3q2ixfcz2). This subscription does require you to use a special IAM role and has been included. 28 | 29 | This SBC deployment includes two network Interfaces. One of these interfaces is used as the connection to Amazon Chime Voice Connector and one interface is used as the connection to the Asterisk server. This separation of interfaces will allow for further security and routing options in the future and is a very common practice when deploying SBCs. 30 | 31 | If you'd like to experiment more with AudioCodes, be sure to check our their documentation on using an [SBC with Amazon Chime Voice Connector](https://www.audiocodes.com/media/14694/mediant-ve-sbc-with-amazon-chime-voice-connector.pdf). 32 | 33 | AudioCodes includes some useful tools that we'll also be discussing that can be found [here](http://redirect.audiocodes.com/install/index.html). 34 | 35 | To log in to the AudioCode, connect to the public IP address and use these credentials: 36 | - Login: Admin 37 | - Password: INSTANCE_ID 38 | 39 | The instance ID can be found in the EC2 Console and is also included in the CDK output. This will change for every deployment. 40 | 41 | #### Configuring the SBC 42 | 43 | Once logged in, the SBC will need to be configured. An incremental configuration file is included in this demo and can be used to quickly configure your SBC. From the SBC UI: SETUP -> ADMINISTRATION -> Maintenance -> Auxiliary Files -> INI file (incremental) 44 | 45 | A file has been created for you as part of the deployment that can be used to configure your SBC. Choose the SBC_CONFIG/SBC_Config.ini file from this local directory and Load File. 46 | 47 | ### Asterisk Changes 48 | 49 | Because we are using a VPN to connect to the Asterisk now, a few things will change. Because the SBC is being used to get from a private network space to the public Internet space, all communication from the Asterisk to the SBC is done using private network address space. This means that that the IP address used in the VoIP client will now be the private IP address and will only be usable when connected to the VPN. 50 | -------------------------------------------------------------------------------- /week-05/SBC_Config/SBC_Template.ini: -------------------------------------------------------------------------------- 1 | [SIP Params] 2 | 3 | SIPGATEWAYNAME = 'OUTBOUND_HOST_NAME' 4 | USEGATEWAYNAMEFOROPTIONS = 1 5 | 6 | 7 | [ CpMediaRealm ] 8 | 9 | FORMAT Index = MediaRealmName, IPv4IF, IPv6IF, RemoteIPv4IF, RemoteIPv6IF, PortRangeStart, MediaSessionLeg, PortRangeEnd, TCPPortRangeStart, TCPPortRangeEnd, IsDefault, QoeProfile, BWProfile, TopologyLocation; 10 | CpMediaRealm 0 = "AsteriskMedia", "eth1", "", "", "", 6000, 14883, 65531, 0, 0, 0, "", "", 0; 11 | CpMediaRealm 1 = "ChimeMedia", "eth0", "", "", "", 6000, 14883, 65531, 0, 0, 0, "", "", 1; 12 | 13 | [ \CpMediaRealm ] 14 | 15 | 16 | [ SBCRoutingPolicy ] 17 | 18 | FORMAT Index = Name, LCREnable, LCRAverageCallLength, LCRDefaultCost, LdapServerGroupName; 19 | SBCRoutingPolicy 0 = "Default_SBCRoutingPolicy", 0, 1, 0, ""; 20 | 21 | [ \SBCRoutingPolicy ] 22 | 23 | 24 | [ SRD ] 25 | 26 | FORMAT Index = Name, BlockUnRegUsers, MaxNumOfRegUsers, EnableUnAuthenticatedRegistrations, SharingPolicy, UsedByRoutingServer, SBCOperationMode, SBCRoutingPolicyName, SBCDialPlanName, AdmissionProfile; 27 | SRD 0 = "DefaultSRD", 0, -1, 1, 0, 0, 0, "Default_SBCRoutingPolicy", "", ""; 28 | 29 | [ \SRD ] 30 | 31 | 32 | [ MessagePolicy ] 33 | 34 | FORMAT Index = Name, MaxMessageLength, MaxHeaderLength, MaxBodyLength, MaxNumHeaders, MaxNumBodies, SendRejection, MethodList, MethodListType, BodyList, BodyListType, UseMaliciousSignatureDB; 35 | MessagePolicy 0 = "Malicious Signature DB Protection", -1, -1, -1, -1, -1, 1, "", 0, "", 0, 1; 36 | 37 | [ \MessagePolicy ] 38 | 39 | 40 | [ SIPInterface ] 41 | 42 | FORMAT Index = InterfaceName, NetworkInterface, SCTPSecondaryNetworkInterface, ApplicationType, UDPPort, TCPPort, TLSPort, SCTPPort, AdditionalUDPPorts, AdditionalUDPPortsMode, SRDName, MessagePolicyName, TLSContext, TLSMutualAuthentication, TCPKeepaliveEnable, ClassificationFailureResponseType, PreClassificationManSet, EncapsulatingProtocol, MediaRealm, SBCDirectMedia, BlockUnRegUsers, MaxNumOfRegUsers, EnableUnAuthenticatedRegistrations, UsedByRoutingServer, TopologyLocation, PreParsingManSetName, AdmissionProfile, CallSetupRulesSetId; 43 | SIPInterface 0 = "ChimeInterface", "eth0", "", 2, 5060, 5060, 5061, 0, "", 0, "DefaultSRD", "", "default", -1, 0, 500, -1, 0, "ChimeMedia", 0, -1, -1, -1, 0, 1, "", "", -1; 44 | SIPInterface 1 = "AsteriskInterface", "eth1", "", 2, 5060, 5060, 5061, 0, "", 0, "DefaultSRD", "", "default", -1, 0, 500, -1, 0, "AsteriskMedia", 0, -1, -1, -1, 0, 0, "", "", -1; 45 | 46 | [ \SIPInterface ] 47 | 48 | 49 | [ ProxySet ] 50 | 51 | FORMAT Index = ProxyName, EnableProxyKeepAlive, ProxyKeepAliveTime, ProxyLoadBalancingMethod, IsProxyHotSwap, SRDName, ClassificationInput, TLSContextName, ProxyRedundancyMode, DNSResolveMethod, KeepAliveFailureResp, GWIPv4SIPInterfaceName, SBCIPv4SIPInterfaceName, GWIPv6SIPInterfaceName, SBCIPv6SIPInterfaceName, MinActiveServersLB, SuccessDetectionRetries, SuccessDetectionInterval, FailureDetectionRetransmissions; 52 | ProxySet 0 = "AsteriskProxySet", 1, 60, 0, 0, "DefaultSRD", 0, "", -1, -1, "", "", "AsteriskInterface", "", "", 1, 1, 10, -1; 53 | ProxySet 1 = "ChimeProxySet", 1, 60, 0, 0, "DefaultSRD", 0, "", -1, -1, "", "", "ChimeInterface", "", "", 1, 1, 10, -1; 54 | 55 | [ \ProxySet ] 56 | 57 | [ IPGroup ] 58 | 59 | FORMAT Index = Type, Name, ProxySetName, VoiceAIConnector, SIPGroupName, ContactUser, SipReRoutingMode, AlwaysUseRouteTable, SRDName, MediaRealm, InternalMediaRealm, ClassifyByProxySet, ProfileName, MaxNumOfRegUsers, InboundManSet, OutboundManSet, RegistrationMode, AuthenticationMode, MethodList, SBCServerAuthType, OAuthHTTPService, EnableSBCClientForking, SourceUriInput, DestUriInput, ContactName, Username, Password, UUIFormat, QOEProfile, BWProfile, AlwaysUseSourceAddr, MsgManUserDef1, MsgManUserDef2, SIPConnect, SBCPSAPMode, DTLSContext, CreatedByRoutingServer, UsedByRoutingServer, SBCOperationMode, SBCRouteUsingRequestURIPort, SBCKeepOriginalCallID, TopologyLocation, SBCDialPlanName, CallSetupRulesSetId, Tags, SBCUserStickiness, UserUDPPortAssignment, AdmissionProfile, ProxyKeepAliveUsingIPG, SBCAltRouteReasonsSetName, TeamsMediaOptimization, TeamsMOInitialBehavior, SIPSourceHostName; 60 | IPGroup 0 = 0, "AsteriskIPGroup", "AsteriskProxySet", "", "", "ASTERISK_HOST", -1, 0, "DefaultSRD", "AsteriskMedia", "", 1, "", -1, -1, -1, 0, 0, "", -1, "", 0, -1, -1, "INTERNAL_PRIVATE_IP", "", "$1$gQ==", 0, "", "", 1, "", "", 0, 0, "default", 0, 0, -1, 0, 0, 0, "", -1, "", 0, 0, "", 0, "", 0, 0, ""; 61 | IPGroup 1 = 0, "ChimeIPGroup", "ChimeProxySet", "", "OUTBOUND_HOST_NAME", "", -1, 0, "DefaultSRD", "ChimeMedia", "", 1, "", -1, -1, -1, 0, 0, "", -1, "", 0, -1, -1, "EXTERNAL_PUBLIC_IP", "", "$1$gQ==", 0, "", "", 1, "", "", 0, 0, "default", 0, 0, -1, 0, 0, 1, "", -1, "", 0, 0, "", 1, "", 0, 0, ""; 62 | 63 | [ \IPGroup ] 64 | 65 | 66 | [ ProxyIp ] 67 | 68 | FORMAT Index = ProxySetId, ProxyIpIndex, IpAddress, TransportType, Priority, Weight; 69 | ProxyIp 0 = "0", 0, "ASTERISK_HOST:5060", 0, 0, 0; 70 | ProxyIp 1 = "1", 0, "OUTBOUND_HOST_NAME:5060", 0, 0, 0; 71 | 72 | [ \ProxyIp ] 73 | 74 | [ IP2IPRouting ] 75 | 76 | FORMAT Index = RouteName, RoutingPolicyName, SrcIPGroupName, SrcUsernamePrefix, SrcHost, DestUsernamePrefix, DestHost, RequestType, MessageConditionName, ReRouteIPGroupName, Trigger, CallSetupRulesSetId, DestType, DestIPGroupName, DestSIPInterfaceName, DestAddress, DestPort, DestTransportType, AltRouteOptions, GroupPolicy, CostGroup, DestTags, ModifiedDestUserName, SrcTags, IPGroupSetName, RoutingTagName, InternalAction; 77 | IP2IPRouting 1 = "terminate OPTIONS", "Default_SBCRoutingPolicy", "AsteriskIPGroup", "*", "*", "*", "*", 6, "", "Any", 0, -1, 1, "", "", "internal", 0, -1, 0, 0, "", "", "", "", "", "default", ""; 78 | IP2IPRouting 2 = "terminate OPTIONS", "Default_SBCRoutingPolicy", "ChimeIPGroup", "*", "*", "*", "*", 6, "", "Any", 0, -1, 1, "", "", "internal", 0, -1, 0, 0, "", "", "", "", "", "default", ""; 79 | IP2IPRouting 10 = "Asterisk -> Chime", "Default_SBCRoutingPolicy", "AsteriskIPGroup", "*", "*", "*", "*", 0, "", "Any", 0, -1, 0, "ChimeIPGroup", "ChimeInterface", "", 0, -1, 0, 0, "", "", "", "", "", "default", ""; 80 | IP2IPRouting 20 = "Chime -> Asterisk", "Default_SBCRoutingPolicy", "ChimeIPGroup", "*", "*", "*", "*", 0, "", "Any", 0, -1, 0, "AsteriskIPGroup", "AsteriskInterface", "", 0, -1, 0, 0, "", "", "", "", "", "default", ""; 81 | 82 | 83 | [ \IP2IPRouting ] 84 | 85 | 86 | [ NATTranslation ] 87 | 88 | FORMAT Index = SrcIPInterfaceName, RemoteInterfaceName, TargetIpMode, TargetIPAddress, SourceStartPort, SourceEndPort, TargetStartPort, TargetEndPort; 89 | NATTranslation 0 = "eth0", "", 0, "EXTERNAL_PUBLIC_IP", "", "", "", ""; 90 | 91 | [ \NATTranslation ] 92 | 93 | [ GwRoutingPolicy ] 94 | 95 | FORMAT Index = Name, LCREnable, LCRAverageCallLength, LCRDefaultCost, LdapServerGroupName; 96 | GwRoutingPolicy 0 = "GwRoutingPolicy", 0, 1, 0, ""; 97 | 98 | [ \GwRoutingPolicy ] 99 | 100 | [ MaliciousSignatureDB ] 101 | 102 | FORMAT Index = Name, Pattern; 103 | MaliciousSignatureDB 0 = "SIPVicious", "Header.User-Agent.content prefix 'friendly-scanner'"; 104 | MaliciousSignatureDB 1 = "SIPScan", "Header.User-Agent.content prefix 'sip-scan'"; 105 | MaliciousSignatureDB 2 = "Smap", "Header.User-Agent.content prefix 'smap'"; 106 | MaliciousSignatureDB 3 = "Sipsak", "Header.User-Agent.content prefix 'sipsak'"; 107 | MaliciousSignatureDB 4 = "Sipcli", "Header.User-Agent.content prefix 'sipcli'"; 108 | MaliciousSignatureDB 5 = "Sivus", "Header.User-Agent.content prefix 'SIVuS'"; 109 | MaliciousSignatureDB 6 = "Gulp", "Header.User-Agent.content prefix 'Gulp'"; 110 | MaliciousSignatureDB 7 = "Sipv", "Header.User-Agent.content prefix 'sipv'"; 111 | MaliciousSignatureDB 8 = "Sundayddr Worm", "Header.User-Agent.content prefix 'sundayddr'"; 112 | MaliciousSignatureDB 9 = "VaxIPUserAgent", "Header.User-Agent.content prefix 'VaxIPUserAgent'"; 113 | MaliciousSignatureDB 10 = "VaxSIPUserAgent", "Header.User-Agent.content prefix 'VaxSIPUserAgent'"; 114 | MaliciousSignatureDB 11 = "SipArmyKnife", "Header.User-Agent.content prefix 'siparmyknife'"; 115 | 116 | [ \MaliciousSignatureDB ] 117 | 118 | [ AudioCoders ] 119 | 120 | FORMAT Index = AudioCodersGroupId, AudioCodersIndex, Name, pTime, rate, PayloadType, Sce, CoderSpecific; 121 | AudioCoders 0 = "AudioCodersGroups_0", 0, 2, 2, 90, -1, 0, ""; 122 | 123 | [ \AudioCoders ] 124 | 125 | -------------------------------------------------------------------------------- /week-05/SBC_Config/buildSBCConfig.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | const cdk_output = require("./cdk-outputs.json"); 3 | 4 | 5 | fs.readFile('SBC_Template.ini', 'utf8', function (err,data) { 6 | if (err) { 7 | return console.log(err); 8 | } 9 | var result = data.replace(/OUTBOUND_HOST_NAME/g, cdk_output.ChimeWithSBC.VoiceConnector) 10 | .replace(/ASTERISK_HOST/g, cdk_output.ChimeWithSBC.AsteriskPrivateIP) 11 | .replace(/EXTERNAL_PUBLIC_IP/g, cdk_output.ChimeWithSBC.publicSBCIP) 12 | .replace(/INTERNAL_PRIVATE_IP/g, cdk_output.ChimeWithSBC.privateSBCIP); 13 | 14 | fs.writeFile('SBC_Config.ini', result, 'utf8', function (err) { 15 | if (err) return console.log(err); 16 | }); 17 | }); 18 | 19 | -------------------------------------------------------------------------------- /week-05/bin/chime-with-sbc.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import * as cdk from '@aws-cdk/core'; 3 | import { ChimeWithSBC } from '../lib/chime-with-sbc'; 4 | 5 | const app = new cdk.App(); 6 | 7 | new ChimeWithSBC(app, 'ChimeWithSBC', { 8 | env: { 9 | account: process.env.CDK_DEFAULT_ACCOUNT, 10 | region: process.env.CDK_DEFAULT_REGION 11 | } 12 | }) 13 | -------------------------------------------------------------------------------- /week-05/cdk.context.json: -------------------------------------------------------------------------------- 1 | { 2 | "availability-zones:account=213847755697:region=us-east-1": [ 3 | "us-east-1a", 4 | "us-east-1b", 5 | "us-east-1c", 6 | "us-east-1d", 7 | "us-east-1e", 8 | "us-east-1f" 9 | ], 10 | "ami:account=213847755697:filters.description.0=AudioCodes Mediant VE SBC - PAYG*:filters.image-type.0=machine:filters.name.0=AudioCodes SBC 7.20A*:filters.state.0=available:region=us-east-1": "ami-0d53ce9f7fa96e829" 11 | } 12 | -------------------------------------------------------------------------------- /week-05/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node --prefer-ts-exts bin/chime-with-sbc.ts", 3 | "context": { 4 | "@aws-cdk/aws-apigateway:usagePlanKeyOrderInsensitiveId": true, 5 | "@aws-cdk/core:enableStackNameDuplicates": "true", 6 | "aws-cdk:enableDiffNoFail": "true", 7 | "@aws-cdk/core:stackRelativeExports": "true", 8 | "@aws-cdk/aws-ecr-assets:dockerIgnoreSupport": true, 9 | "@aws-cdk/aws-secretsmanager:parseOwnedSecretName": true, 10 | "@aws-cdk/aws-kms:defaultKeyPolicies": true, 11 | "@aws-cdk/aws-s3:grantWriteWithoutAcl": true, 12 | "@aws-cdk/aws-ecs-patterns:removeDefaultDesiredCount": true, 13 | "@aws-cdk/aws-rds:lowercaseDbIdentifier": true, 14 | "@aws-cdk/aws-efs:defaultEncryptionAtRest": true 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /week-05/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | if [ -f "cdk.context.json" ]; then 3 | echo "" 4 | echo "INFO: Removing cdk.context.json" 5 | rm cdk.context.json 6 | else 7 | echo "" 8 | echo "INFO: cdk.context.json not present, nothing to remove" 9 | fi 10 | if [ ! -f "package-lock.json" ]; then 11 | echo "" 12 | echo "Installing Packages" 13 | echo "" 14 | npm install 15 | fi 16 | echo "" 17 | echo "Building CDK" 18 | echo "" 19 | npm run build 20 | echo "" 21 | echo "Deploying CDK" 22 | echo "" 23 | cdk deploy --parameters SecurityGroupIP=$ExternalIP -O SBC_Config/cdk-outputs.json 24 | pushd SBC_Config 25 | node buildSBCConfig.js 26 | popd 27 | -------------------------------------------------------------------------------- /week-05/images/ConnectVPN.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/building-with-amazon-chime/969c1c7a36fd7e4e982922290a05ec9ad5b420fc/week-05/images/ConnectVPN.png -------------------------------------------------------------------------------- /week-05/images/ImportProfile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/building-with-amazon-chime/969c1c7a36fd7e4e982922290a05ec9ad5b420fc/week-05/images/ImportProfile.png -------------------------------------------------------------------------------- /week-05/images/SBC_Configuration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/building-with-amazon-chime/969c1c7a36fd7e4e982922290a05ec9ad5b420fc/week-05/images/SBC_Configuration.png -------------------------------------------------------------------------------- /week-05/images/Week-05-Overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/building-with-amazon-chime/969c1c7a36fd7e4e982922290a05ec9ad5b420fc/week-05/images/Week-05-Overview.png -------------------------------------------------------------------------------- /week-05/lib/chime-with-sbc.ts: -------------------------------------------------------------------------------- 1 | import * as ec2 from "@aws-cdk/aws-ec2"; 2 | import * as cdk from '@aws-cdk/core'; 3 | import { KeyPair } from 'cdk-ec2-key-pair'; 4 | import * as iam from '@aws-cdk/aws-iam'; 5 | import { CustomResource, Duration } from '@aws-cdk/core'; 6 | import lambda = require('@aws-cdk/aws-lambda'); 7 | import custom = require('@aws-cdk/custom-resources') 8 | import { Asset } from '@aws-cdk/aws-s3-assets'; 9 | import { SubnetType } from "@aws-cdk/aws-ec2"; 10 | import * as ssm from '@aws-cdk/aws-ssm'; 11 | import * as path from 'path'; 12 | 13 | export class ChimeWithSBC extends cdk.Stack { 14 | constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { 15 | super(scope, id, props); 16 | 17 | const key = new KeyPair(this, 'KeyPair', { 18 | name: 'audioCode-keypair', 19 | description: 'Key Pair created with CDK Deployment', 20 | }); 21 | key.grantReadOnPublicKey 22 | 23 | const sbcEip = new ec2.CfnEIP(this, 'sbcEip') 24 | 25 | 26 | const vpc = new ec2.Vpc(this, 'VPC', { 27 | natGateways: 0, 28 | subnetConfiguration: [ 29 | { 30 | cidrMask: 24, 31 | name: "AudioCode", 32 | subnetType: ec2.SubnetType.PUBLIC 33 | }, 34 | ]}); 35 | 36 | const audioCodeSubnet = vpc.selectSubnets({subnetType: SubnetType.PUBLIC}).subnets[0]; 37 | const asteriskSubnet = vpc.selectSubnets({subnetType: SubnetType.PUBLIC}).subnets[0]; 38 | 39 | const privateSBCInterface = new ec2.CfnNetworkInterface(this, "privateSBCInterface", { 40 | subnetId: asteriskSubnet.subnetId 41 | }); 42 | 43 | // const publicSBCInterface = new ec2.CfnNetworkInterface(this, 'publicSBCInterface', { 44 | // subnetId: audioCodeSubnet.subnetId 45 | // }) 46 | 47 | // const privateAsteriskInterface = new ec2.CfnNetworkInterface(this, "privateAsteriskInterface", { 48 | // subnetId: asteriskSubnet.subnetId 49 | // }); 50 | 51 | 52 | const chimeSecurityGroup = new ec2.SecurityGroup(this, 'ChimeSecurityGroup', { 53 | vpc, 54 | description: 'Security Group for Asterisk Server', 55 | allowAllOutbound: true 56 | }); 57 | 58 | chimeSecurityGroup.addIngressRule(ec2.Peer.ipv4('3.80.16.0/23'), ec2.Port.udp(5060), 'Allow Chime Voice Connector Signaling Access') 59 | chimeSecurityGroup.addIngressRule(ec2.Peer.ipv4('3.80.16.0/23'), ec2.Port.tcp(5060), 'Allow Chime Voice Connector Signaling Access') 60 | chimeSecurityGroup.addIngressRule(ec2.Peer.ipv4('3.80.16.0/23'), ec2.Port.tcp(5061), 'Allow Chime Voice Connector Signaling Access') 61 | chimeSecurityGroup.addIngressRule(ec2.Peer.ipv4('3.80.16.0/23'), ec2.Port.udpRange(5000,65000), 'Allow Chime Voice Connector Media Access') 62 | chimeSecurityGroup.addIngressRule(ec2.Peer.ipv4('99.77.253.0/24'), ec2.Port.udp(5060), 'Allow Chime Voice Connector Signaling Access') 63 | chimeSecurityGroup.addIngressRule(ec2.Peer.ipv4('99.77.253.0/24'), ec2.Port.tcp(5060), 'Allow Chime Voice Connector Signaling Access') 64 | chimeSecurityGroup.addIngressRule(ec2.Peer.ipv4('99.77.253.0/24'), ec2.Port.tcp(5061), 'Allow Chime Voice Connector Signaling Access') 65 | chimeSecurityGroup.addIngressRule(ec2.Peer.ipv4('99.77.253.0/24'), ec2.Port.udpRange(5000,65000), 'Allow Chime Voice Connector Media Access') 66 | 67 | const sbcAdminSecurityGroup = new ec2.SecurityGroup(this, 'sbcAdminSecurityGroup', { 68 | vpc, 69 | description: 'Security Group for Asterisk Server', 70 | allowAllOutbound: true 71 | }); 72 | 73 | // sbcAdminSecurityGroup.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(80), 'Allow HTTP Access') 74 | sbcAdminSecurityGroup.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(443), 'Allow HTTPS Access') 75 | 76 | const LANSecurityGroup = new ec2.SecurityGroup(this, 'LANSecurityGroup', { 77 | vpc: vpc, 78 | description: 'Security Group for AudioCode SBC', 79 | allowAllOutbound: true 80 | }); 81 | 82 | LANSecurityGroup.addIngressRule(ec2.Peer.ipv4(audioCodeSubnet.ipv4CidrBlock), ec2.Port.allTraffic(), 'All inbound traffic from local VPC') 83 | 84 | const VPNSecurityGroup = new ec2.SecurityGroup(this, 'VPNCodeSecurityGroup', { 85 | vpc: vpc, 86 | description: 'Security Group for AudioCode SBC', 87 | allowAllOutbound: true 88 | }); 89 | 90 | VPNSecurityGroup.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(22), 'Allow SSH Access') 91 | VPNSecurityGroup.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(1194), 'Allow VPN Traffic') 92 | 93 | const asteriskRole = new iam.Role(this, 'asteriskRole', { 94 | assumedBy: new iam.ServicePrincipal('ec2.amazonaws.com') 95 | }) 96 | 97 | asteriskRole.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonSSMManagedInstanceCore')) 98 | 99 | const audioCodeRole = new iam.Role(this, 'audioCodeRole', { 100 | assumedBy: new iam.ServicePrincipal('ec2.amazonaws.com') 101 | 102 | }) 103 | 104 | audioCodeRole.addToPolicy(new iam.PolicyStatement({ 105 | resources: ['*'], 106 | actions: [ 107 | 'aws-marketplace:MeterUsage' 108 | ] 109 | })) 110 | 111 | audioCodeRole.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonSSMManagedInstanceCore')) 112 | 113 | const audioCodeAmi = new ec2.LookupMachineImage({ 114 | name: 'AudioCodes SBC 7.20A*', 115 | filters: { 116 | 'description': ['AudioCodes Mediant VE SBC - PAYG*'] 117 | }, 118 | }) 119 | 120 | const createVoiceConnectorRole = new iam.Role(this, 'createChimeLambdaRole', { 121 | assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), 122 | inlinePolicies: { 123 | ['chimePolicy']: new iam.PolicyDocument( { statements: [new iam.PolicyStatement({ 124 | resources: ['*'], 125 | actions: ['chime:*', 126 | 'lambda:*']})]}) 127 | }, 128 | managedPolicies: [ iam.ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSLambdaBasicExecutionRole") ] 129 | }) 130 | 131 | const createVoiceConnectorLambda = new lambda.Function(this, 'createVCLambda', { 132 | code: lambda.Code.fromAsset("src", {exclude: ["**", "!createVoiceConnector.py"]}), 133 | handler: 'createVoiceConnector.on_event', 134 | runtime: lambda.Runtime.PYTHON_3_8, 135 | role: createVoiceConnectorRole, 136 | timeout: Duration.seconds(60) 137 | }); 138 | 139 | const voiceConnectorProvider = new custom.Provider(this, 'voiceConnectorProvider', { 140 | onEventHandler: createVoiceConnectorLambda 141 | }) 142 | 143 | const voiceConnectorResource = new CustomResource(this, 'voiceConnectorResource', { 144 | serviceToken: voiceConnectorProvider.serviceToken, 145 | properties: { 'region': this.region, 146 | 'eip': sbcEip.ref } 147 | }) 148 | 149 | const phoneNumber = voiceConnectorResource.getAttString('phoneNumber') 150 | const voiceConnectorId = voiceConnectorResource.getAttString('voiceConnectorId') 151 | const outboundHostName = voiceConnectorResource.getAttString('outboundHostName') 152 | 153 | const phoneNumberParameter = new ssm.StringParameter(this, 'phoneNumber', { 154 | parameterName: '/asterisk/phoneNumber', 155 | stringValue: phoneNumber, 156 | }); 157 | 158 | const voiceConnectorParameter = new ssm.StringParameter(this, 'voiceConnector', { 159 | parameterName: '/asterisk/voiceConnector', 160 | stringValue: voiceConnectorId 161 | }) 162 | 163 | const outboundHostNameParameter = new ssm.StringParameter(this, 'outboundHostName', { 164 | parameterName: '/asterisk/outboundHostName', 165 | stringValue: outboundHostName 166 | }) 167 | 168 | const privateSBCAddressParameter = new ssm.StringParameter(this, 'privateSBCAddress', { 169 | parameterName: '/asterisk/privateSBCAddress', 170 | stringValue: privateSBCInterface.attrPrimaryPrivateIpAddress 171 | }) 172 | 173 | const sbcInstance = new ec2.Instance(this, 'sbcInstance', { 174 | vpc: vpc, 175 | instanceType: ec2.InstanceType.of(ec2.InstanceClass.R4, ec2.InstanceSize.LARGE), 176 | machineImage: audioCodeAmi, 177 | role: audioCodeRole, 178 | vpcSubnets: { 179 | subnets: [audioCodeSubnet, asteriskSubnet] 180 | } 181 | }); 182 | 183 | new ec2.CfnEIPAssociation(this, "SBC EIP Association", { 184 | eip: sbcEip.ref, 185 | instanceId: sbcInstance.instanceId, 186 | networkInterfaceId: "0" 187 | }) 188 | 189 | new ec2.CfnNetworkInterfaceAttachment(this, "privateSBCNetworkAtachment", { 190 | deviceIndex: "1", 191 | instanceId: sbcInstance.instanceId, 192 | networkInterfaceId: privateSBCInterface.ref, 193 | deleteOnTermination: true}); 194 | 195 | 196 | sbcInstance.addSecurityGroup(LANSecurityGroup) 197 | sbcInstance.addSecurityGroup(sbcAdminSecurityGroup) 198 | sbcInstance.addSecurityGroup(chimeSecurityGroup) 199 | 200 | const asteriskUserData = ec2.UserData.forLinux(); 201 | 202 | const asteriskConfig = new Asset(this, 'AsteriskConfig', {path: path.join(__dirname, '../src/config.sh')}); 203 | 204 | const configPath = asteriskUserData.addS3DownloadCommand({ 205 | bucket:asteriskConfig.bucket, 206 | bucketKey:asteriskConfig.s3ObjectKey, 207 | }); 208 | 209 | asteriskUserData.addExecuteFileCommand({ 210 | filePath:configPath, 211 | arguments: '--verbose -y' 212 | }); 213 | 214 | const asteriskAmi = new ec2.AmazonLinuxImage({ 215 | generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2, 216 | cpuType: ec2.AmazonLinuxCpuType.ARM_64}); 217 | 218 | const asteriskInstnace = new ec2.Instance(this, 'AsteriskInstance', { 219 | vpc, 220 | instanceType: ec2.InstanceType.of(ec2.InstanceClass.T4G, ec2.InstanceSize.LARGE), 221 | machineImage: asteriskAmi, 222 | keyName: key.keyPairName, 223 | role: asteriskRole, 224 | userData: asteriskUserData, 225 | }); 226 | 227 | asteriskInstnace.addSecurityGroup(LANSecurityGroup) 228 | asteriskInstnace.addSecurityGroup(VPNSecurityGroup) 229 | 230 | asteriskConfig.grantRead(asteriskRole); 231 | phoneNumberParameter.grantRead(asteriskRole); 232 | voiceConnectorParameter.grantRead(asteriskRole); 233 | outboundHostNameParameter.grantRead(asteriskRole); 234 | privateSBCAddressParameter.grantRead(asteriskRole) 235 | 236 | new cdk.CfnOutput(this, 'publicSBCIP', { value: sbcInstance.instancePublicIp }); 237 | new cdk.CfnOutput(this, 'privateSBCIP', { value: privateSBCInterface.attrPrimaryPrivateIpAddress }) 238 | new cdk.CfnOutput(this, 'Key Name', { value: key.keyPairName }) 239 | new cdk.CfnOutput(this, 'Download Key Command', { value: 'aws secretsmanager get-secret-value --secret-id ec2-ssh-key/audioCode-keypair/private --query SecretString --output text > cdk-key.pem && chmod 400 cdk-key.pem' }) 240 | new cdk.CfnOutput(this, 'ssh command', { value: 'ssh -i cdk-key.pem -o IdentitiesOnly=yes ec2-user@' + asteriskInstnace.instancePublicIp }) 241 | new cdk.CfnOutput(this, 'PhoneNumber', { value: phoneNumber}), 242 | new cdk.CfnOutput(this, 'VoiceConnector', { value: outboundHostName}) 243 | new cdk.CfnOutput(this, 'AsteriskPrivateIP', { value: asteriskInstnace.instancePrivateIp}) 244 | new cdk.CfnOutput(this, 'AsteriskPublicIP', { value: asteriskInstnace.instancePublicIp}) 245 | new cdk.CfnOutput(this, 'downloadOVPNProfile', { value: 'scp -i cdk-key.pem -o IdentitiesOnly=yes ec2-user@' + asteriskInstnace.instancePublicIp + ':/tmp/ChimeDemo.ovpn ./'}) 246 | new cdk.CfnOutput(this, 'SBCInstance', { value: sbcInstance.instanceId }) 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /week-05/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ec2-cdk", 3 | "version": "0.1.0", 4 | "bin": { 5 | "ec2-cdk": "bin/ec2-cdk.js" 6 | }, 7 | "scripts": { 8 | "build": "tsc", 9 | "watch": "tsc -w", 10 | "cdk": "cdk" 11 | }, 12 | "devDependencies": { 13 | "@aws-cdk/assert": "^1.106.1", 14 | "@types/jest": "^26.0.23", 15 | "@types/node": "15.6.1", 16 | "aws-cdk": "^1.106.1", 17 | "ts-node": "^10.0.0", 18 | "typescript": "~4.3.2" 19 | }, 20 | "dependencies": { 21 | "@aws-cdk/aws-ec2": "^1.106.1", 22 | "@aws-cdk/aws-s3-deployment": "^1.106.1", 23 | "@aws-cdk/core": "^1.106.1", 24 | "@aws-cdk/custom-resources": "^1.106.1", 25 | "cdk-ec2-key-pair": "^2.2.0", 26 | "source-map-support": "^0.5.19" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /week-05/src/config.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -xe 2 | HOMEDIR=/home/ec2-user 3 | yum update -y 4 | yum install net-tools -y 5 | yum install wget -y 6 | amazon-linux-extras install epel -y 7 | yum -y install make gcc gcc-c++ make subversion libxml2-devel ncurses-devel openssl-devel vim-enhanced man glibc-devel autoconf libnewt kernel-devel kernel-headers linux-headers openssl-devel zlib-devel libsrtp libsrtp-devel uuid libuuid-devel mariadb-server jansson-devel libsqlite3x libsqlite3x-devel epel-release.noarch bash-completion bash-completion-extras unixODBC unixODBC-devel libtool-ltdl libtool-ltdl-devel mysql-connector-odbc mlocate libiodbc sqlite sqlite-devel sql-devel.i686 sqlite-doc.noarch sqlite-tcl.x86_64 patch libedit-devel jq 8 | 9 | mkdir /root/vpn 10 | cd /root/vpn 11 | curl -O https://raw.githubusercontent.com/angristan/openvpn-install/master/openvpn-install.sh 12 | chmod +x openvpn-install.sh 13 | 14 | export APPROVE_INSTALL=y 15 | export ENDPOINT=$(curl -4 ifconfig.co) 16 | export APPROVE_IP=y 17 | export IPV6_SUPPORT=n 18 | export PORT_CHOICE=1 19 | export PROTOCOL_CHOICE=2 20 | export DNS=1 21 | export COMPRESSION_ENABLED=n 22 | export CUSTOMIZE_ENC=n 23 | export CLIENT=ChimeDemo 24 | export PASS=1 25 | 26 | ./openvpn-install.sh 27 | 28 | chown ec2-user /root/ChimeDemo.ovpn 29 | cp /root/ChimeDemo.ovpn /tmp/ 30 | cd /tmp 31 | wget https://downloads.asterisk.org/pub/telephony/asterisk/asterisk-16-current.tar.gz 32 | tar xvzf asterisk-16-current.tar.gz 33 | cd asterisk-16*/ 34 | ./configure --libdir=/usr/lib64 --with-jansson-bundled 35 | make menuselect.makeopts 36 | menuselect/menuselect \ 37 | --disable BUILD_NATIVE \ 38 | --disable chan_sip \ 39 | --disable chan_skinny \ 40 | --enable cdr_csv \ 41 | --enable res_snmp \ 42 | --enable res_http_websocket \ 43 | menuselect.makeopts 44 | make 45 | make install 46 | make basic-pbx 47 | touch /etc/redhat-release 48 | make config 49 | ldconfig 50 | 51 | IP=$( curl http://169.254.169.254/latest/meta-data/public-ipv4 ) 52 | REGION=$( curl http://169.254.169.254/latest/meta-data/placement/region ) 53 | PhoneNumber=$( aws ssm get-parameter --name /asterisk/phoneNumber --region $REGION | jq -r '.Parameter.Value' ) 54 | VoiceConnectorHost=$( aws ssm get-parameter --name /asterisk/voiceConnector --region $REGION | jq -r '.Parameter.Value' ) 55 | OutboundHostName=$( aws ssm get-parameter --name /asterisk/outboundHostName --region $REGION | jq -r '.Parameter.Value' ) 56 | PrivateSBCAddress=$( aws ssm get-parameter --name /asterisk/privateSBCAddress --region $REGION | jq -r '.Parameter.Value' ) 57 | 58 | echo "[udp] 59 | type=transport 60 | protocol=udp 61 | bind=0.0.0.0 62 | allow_reload=yes 63 | 64 | [VoiceConnector] 65 | type=endpoint 66 | context=from-voiceConnector 67 | transport=udp 68 | disallow=all 69 | allow=ulaw 70 | aors=VoiceConnector 71 | direct_media=no 72 | ice_support=yes 73 | force_rport=yes 74 | 75 | [VoiceConnector] 76 | type=identify 77 | endpoint=VoiceConnector 78 | match=$PrivateSBCAddress 79 | 80 | [VoiceConnector] 81 | type=aor 82 | contact=sip:$PrivateSBCAddress 83 | 84 | [$PhoneNumber] 85 | type=endpoint 86 | context=from-phone 87 | disallow=all 88 | allow=ulaw 89 | transport=udp 90 | auth=$PhoneNumber 91 | aors=$PhoneNumber 92 | send_pai=yes 93 | direct_media=no 94 | rewrite_contact=yes 95 | ice_support=yes 96 | force_rport=yes 97 | 98 | [$PhoneNumber] 99 | type=auth 100 | auth_type=userpass 101 | password=ChimeDemo 102 | username=$PhoneNumber 103 | 104 | [$PhoneNumber] 105 | type=aor 106 | max_contacts=5" > /etc/asterisk/pjsip.conf 107 | 108 | echo "; extensions.conf - the Asterisk dial plan 109 | ; 110 | [general] 111 | static=yes 112 | writeprotect=no 113 | clearglobalvars=no 114 | 115 | [catch-all] 116 | exten => _[+0-9].,1,Answer() 117 | exten => _[+0-9].,n,Wait(1) 118 | exten => _[+0-9].,n,Playback(hello-world) 119 | exten => _[+0-9].,n,Wait(1) 120 | exten => _[+0-9].,n,echo() 121 | exten => _[+0-9].,n,Wait(1) 122 | exten => _[+0-9].,n,Hangup() 123 | 124 | [from-phone] 125 | include => outbound_phone 126 | 127 | [outbound_phone] 128 | exten => _+X.,1,NoOP(Outbound Normal) 129 | same => n,Dial(PJSIP/\${EXTEN}@VoiceConnector,20) 130 | same => n,Congestion 131 | 132 | [from-voiceConnector] 133 | include => phones 134 | include => catch-all 135 | 136 | [phones] 137 | exten => $PhoneNumber,1,Dial(PJSIP/$PhoneNumber)" > /etc/asterisk/extensions.conf 138 | 139 | echo "[options] 140 | runuser = asterisk 141 | rungroup = asterisk" > /etc/asterisk/asterisk.conf 142 | 143 | echo "[general] 144 | [logfiles] 145 | console = verbose,notice,warning,error 146 | messages = notice,warning,error" > /etc/asterisk/logger.conf 147 | 148 | groupadd asterisk 149 | useradd -r -d /var/lib/asterisk -g asterisk asterisk 150 | usermod -aG audio,dialout asterisk 151 | chown -R asterisk.asterisk /etc/asterisk 152 | chown -R asterisk.asterisk /var/{lib,log,spool}/asterisk 153 | 154 | systemctl start asterisk 155 | -------------------------------------------------------------------------------- /week-05/src/createVoiceConnector.py: -------------------------------------------------------------------------------- 1 | 2 | import json 3 | import boto3 4 | import time 5 | import uuid 6 | 7 | chime = boto3.client('chime') 8 | 9 | def authorizeEIP (voiceConnectorId, elasticIP): 10 | response = chime.put_voice_connector_origination( 11 | VoiceConnectorId=voiceConnectorId, 12 | Origination={ 13 | 'Routes': [ 14 | { 15 | 'Host': elasticIP, 16 | 'Port': 5060, 17 | 'Protocol': 'UDP', 18 | 'Priority': 1, 19 | 'Weight': 1 20 | }, 21 | ], 22 | 'Disabled': False 23 | } 24 | ) 25 | print(response) 26 | 27 | response = chime.put_voice_connector_termination( 28 | VoiceConnectorId=voiceConnectorId, 29 | Termination={ 30 | 'CpsLimit': 1, 31 | 'CallingRegions': [ 32 | 'US', 33 | ], 34 | 'CidrAllowedList': [ 35 | elasticIP + '/32', 36 | ], 37 | 'Disabled': False 38 | } 39 | ) 40 | print(response) 41 | 42 | 43 | def getPhoneNumber (): 44 | search_response = chime.search_available_phone_numbers( 45 | # AreaCode='string', 46 | # City='string', 47 | # Country='string', 48 | State='IL', 49 | # TollFreePrefix='string', 50 | MaxResults=1 51 | ) 52 | phoneNumberToOrder = search_response['E164PhoneNumbers'][0] 53 | print ('Phone Number: ' + phoneNumberToOrder) 54 | phone_order = chime.create_phone_number_order( 55 | ProductType='VoiceConnector', 56 | E164PhoneNumbers=[ 57 | phoneNumberToOrder, 58 | ] 59 | ) 60 | print ('Phone Order: ' + str(phone_order)) 61 | 62 | check_phone_order = chime.get_phone_number_order( 63 | PhoneNumberOrderId=phone_order['PhoneNumberOrder']['PhoneNumberOrderId'] 64 | ) 65 | order_status = check_phone_order['PhoneNumberOrder']['Status'] 66 | timeout = 0 67 | 68 | while not order_status == 'Successful': 69 | timeout += 1 70 | print('Checking status: ' + str(order_status)) 71 | time.sleep(5) 72 | check_phone_order = chime.get_phone_number_order( 73 | PhoneNumberOrderId=phone_order['PhoneNumberOrder']['PhoneNumberOrderId'] 74 | ) 75 | order_status = check_phone_order['PhoneNumberOrder']['Status'] 76 | if timeout == 5: 77 | return 'Could not get phone number' 78 | 79 | return phoneNumberToOrder 80 | 81 | def createVoiceConnector (region, phoneNumber): 82 | print(str(uuid.uuid1())) 83 | print(region) 84 | response = chime.create_voice_connector( 85 | Name='Trunk' + str(uuid.uuid1()), 86 | AwsRegion='us-east-1', 87 | RequireEncryption=False 88 | ) 89 | 90 | voiceConnectorId = response['VoiceConnector']['VoiceConnectorId'] 91 | outboundHostName = response['VoiceConnector']['OutboundHostName'] 92 | 93 | response = chime.associate_phone_numbers_with_voice_connector( 94 | VoiceConnectorId=voiceConnectorId, 95 | E164PhoneNumbers=[ 96 | phoneNumber, 97 | ], 98 | ForceAssociate=True 99 | ) 100 | 101 | voiceConnector = { 'voiceConnectorId': voiceConnectorId, 'outboundHostName': outboundHostName, 'phoneNumber': phoneNumber} 102 | return voiceConnector 103 | 104 | def on_event(event, context): 105 | print(event) 106 | request_type = event['RequestType'] 107 | if request_type == 'Create': return on_create(event) 108 | if request_type == 'Update': return on_update(event) 109 | if request_type == 'Delete': return on_delete(event) 110 | raise Exception("Invalid request type: %s" % request_type) 111 | 112 | def on_create(event): 113 | physical_id = 'VoiceConnectorResources' 114 | region = event['ResourceProperties']['region'] 115 | elasticIP = event['ResourceProperties']['eip'] 116 | 117 | newPhoneNumber = getPhoneNumber() 118 | voiceConnector = createVoiceConnector(region, newPhoneNumber) 119 | authorizeEIP(voiceConnector['voiceConnectorId'], elasticIP) 120 | 121 | return { 'PhysicalResourceId': physical_id, 'Data': voiceConnector } 122 | 123 | def on_update(event): 124 | physical_id = event["PhysicalResourceId"] 125 | props = event["ResourceProperties"] 126 | print("update resource %s with props %s" % (physical_id, props)) 127 | return { 'PhysicalResourceId': physical_id } 128 | 129 | 130 | def on_delete(event): 131 | physical_id = event["PhysicalResourceId"] 132 | print("delete resource %s" % physical_id) 133 | return { 'PhysicalResourceId': physical_id } 134 | -------------------------------------------------------------------------------- /week-05/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2018", 4 | "module": "commonjs", 5 | "lib": ["es2018"], 6 | "declaration": true, 7 | "strict": true, 8 | "noImplicitAny": true, 9 | "strictNullChecks": true, 10 | "noImplicitThis": true, 11 | "alwaysStrict": true, 12 | "noUnusedLocals": false, 13 | "noUnusedParameters": false, 14 | "noImplicitReturns": true, 15 | "noFallthroughCasesInSwitch": false, 16 | "inlineSourceMap": true, 17 | "inlineSources": true, 18 | "experimentalDecorators": true, 19 | "strictPropertyInitialization": false, 20 | "typeRoots": ["./node_modules/@types"] 21 | }, 22 | "exclude": ["cdk.out"] 23 | } 24 | -------------------------------------------------------------------------------- /week-06/.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | !jest.config.js 3 | *.d.ts 4 | node_modules 5 | 6 | # CDK asset staging directory 7 | .cdk.staging 8 | cdk.out 9 | 10 | package-lock.json 11 | 12 | !SBC_Config/buildSBCConfig.js 13 | cdk-outputs.json 14 | SBC_Config.ini 15 | 16 | *.ovpn 17 | *.pem 18 | *.drawio 19 | -------------------------------------------------------------------------------- /week-06/.npmignore: -------------------------------------------------------------------------------- 1 | *.ts 2 | !*.d.ts 3 | 4 | # CDK asset staging directory 5 | .cdk.staging 6 | cdk.out 7 | -------------------------------------------------------------------------------- /week-06/README.md: -------------------------------------------------------------------------------- 1 | # Amazon Chime Voice Connector with an SBC and SIPREC 2 | 3 | This week's episode builds upon many of the previous versions but especially last week's build of an AudioCode SBC. Check the requirements and processes [here](https://github.com/aws-samples/building-with-amazon-chime/tree/main/week-05#amazon-chime-voice-connector-with-an-sbc). The basics aren't going to change. The VoIP client will still connect through a VPN and the SBC should be configured through the produced .ini file. 4 | 5 | ## Overview 6 | ![Overview](images/Week-06-Overview.png) 7 | 8 | The change here is with the addition of the SIPREC path from the SBC to a new VoiceConnector. This VoiceConnector is configured to support streaming to Kinesis Video Streams. 9 | 10 | ### SBC Deployment 11 | 12 | The main SBC deployment is no different from last week's deployment. Documentation for configuring the SIPREC components can be found [here](https://www.audiocodes.com/media/14650/mediant-sbc-for-amazon-chime-voice-connector-siprec-configuration-note.pdf). These configurations have been done as part of the build and can be found in the SBC_Config directory. 13 | 14 | ### Additional Deployments (Optional) 15 | 16 | Two additional repositories can be used as part of this deployment for additional enhancements. 17 | 18 | [Real-Time Transcription with Amazon Chime Voice Connector](https://github.com/aws-samples/amazon-chime-voiceconnector-transcription) 19 | 20 | This repository will help deploy services that will capture the media on the KVS stream and transcribe the speech detected and output to a DynamoDB and Web Socket. 21 | 22 | [AI-powered Speech Analytics with Amazon Chime Voice Connector](https://github.com/aws-samples/chime-voiceconnector-agent-assist) 23 | 24 | This repository will assist with the previous repository and present the live transcription. 25 | -------------------------------------------------------------------------------- /week-06/SBC_Config/SBC_Template.ini: -------------------------------------------------------------------------------- 1 | [SIP Params] 2 | 3 | SIPGATEWAYNAME = 'OUTBOUND_HOST_NAME' 4 | USEGATEWAYNAMEFOROPTIONS = 1 5 | SIPRECSERVERDESTUSERNAME = 'STREAMING_HOST_NAME' 6 | 7 | [ CpMediaRealm ] 8 | 9 | FORMAT Index = MediaRealmName, IPv4IF, IPv6IF, RemoteIPv4IF, RemoteIPv6IF, PortRangeStart, MediaSessionLeg, PortRangeEnd, TCPPortRangeStart, TCPPortRangeEnd, IsDefault, QoeProfile, BWProfile, TopologyLocation; 10 | CpMediaRealm 0 = "AsteriskMedia", "eth1", "", "", "", 6000, 14883, 65531, 0, 0, 0, "", "", 0; 11 | CpMediaRealm 1 = "ChimeMedia", "eth0", "", "", "", 6000, 14883, 65531, 0, 0, 0, "", "", 1; 12 | 13 | [ \CpMediaRealm ] 14 | 15 | 16 | [ SBCRoutingPolicy ] 17 | 18 | FORMAT Index = Name, LCREnable, LCRAverageCallLength, LCRDefaultCost, LdapServerGroupName; 19 | SBCRoutingPolicy 0 = "Default_SBCRoutingPolicy", 0, 1, 0, ""; 20 | 21 | [ \SBCRoutingPolicy ] 22 | 23 | 24 | [ SRD ] 25 | 26 | FORMAT Index = Name, BlockUnRegUsers, MaxNumOfRegUsers, EnableUnAuthenticatedRegistrations, SharingPolicy, UsedByRoutingServer, SBCOperationMode, SBCRoutingPolicyName, SBCDialPlanName, AdmissionProfile; 27 | SRD 0 = "DefaultSRD", 0, -1, 1, 0, 0, 0, "Default_SBCRoutingPolicy", "", ""; 28 | 29 | [ \SRD ] 30 | 31 | 32 | [ MessagePolicy ] 33 | 34 | FORMAT Index = Name, MaxMessageLength, MaxHeaderLength, MaxBodyLength, MaxNumHeaders, MaxNumBodies, SendRejection, MethodList, MethodListType, BodyList, BodyListType, UseMaliciousSignatureDB; 35 | MessagePolicy 0 = "Malicious Signature DB Protection", -1, -1, -1, -1, -1, 1, "", 0, "", 0, 1; 36 | 37 | [ \MessagePolicy ] 38 | 39 | 40 | [ SIPInterface ] 41 | 42 | FORMAT Index = InterfaceName, NetworkInterface, SCTPSecondaryNetworkInterface, ApplicationType, UDPPort, TCPPort, TLSPort, SCTPPort, AdditionalUDPPorts, AdditionalUDPPortsMode, SRDName, MessagePolicyName, TLSContext, TLSMutualAuthentication, TCPKeepaliveEnable, ClassificationFailureResponseType, PreClassificationManSet, EncapsulatingProtocol, MediaRealm, SBCDirectMedia, BlockUnRegUsers, MaxNumOfRegUsers, EnableUnAuthenticatedRegistrations, UsedByRoutingServer, TopologyLocation, PreParsingManSetName, AdmissionProfile, CallSetupRulesSetId; 43 | SIPInterface 0 = "ChimeInterface", "eth0", "", 2, 5060, 5060, 5061, 0, "", 0, "DefaultSRD", "", "default", -1, 0, 500, -1, 0, "ChimeMedia", 0, -1, -1, -1, 0, 1, "", "", -1; 44 | SIPInterface 1 = "AsteriskInterface", "eth1", "", 2, 5060, 5060, 5061, 0, "", 0, "DefaultSRD", "", "default", -1, 0, 500, -1, 0, "AsteriskMedia", 0, -1, -1, -1, 0, 0, "", "", -1; 45 | 46 | [ \SIPInterface ] 47 | 48 | 49 | [ ProxySet ] 50 | 51 | FORMAT Index = ProxyName, EnableProxyKeepAlive, ProxyKeepAliveTime, ProxyLoadBalancingMethod, IsProxyHotSwap, SRDName, ClassificationInput, TLSContextName, ProxyRedundancyMode, DNSResolveMethod, KeepAliveFailureResp, GWIPv4SIPInterfaceName, SBCIPv4SIPInterfaceName, GWIPv6SIPInterfaceName, SBCIPv6SIPInterfaceName, MinActiveServersLB, SuccessDetectionRetries, SuccessDetectionInterval, FailureDetectionRetransmissions; 52 | ProxySet 0 = "AsteriskProxySet", 1, 60, 0, 0, "DefaultSRD", 0, "", -1, -1, "", "", "AsteriskInterface", "", "", 1, 1, 10, -1; 53 | ProxySet 1 = "ChimeProxySet", 1, 60, 0, 0, "DefaultSRD", 0, "", -1, -1, "", "", "ChimeInterface", "", "", 1, 1, 10, -1; 54 | ProxySet 2 = "SIPRECProxySet", 1, 60, 0, 0, "DefaultSRD", 0, "", -1, -1, "", "", "ChimeInterface", "", "", 1, 1, 10, -1; 55 | 56 | [ \ProxySet ] 57 | 58 | [ IPGroup ] 59 | 60 | FORMAT Index = Type, Name, ProxySetName, VoiceAIConnector, SIPGroupName, ContactUser, SipReRoutingMode, AlwaysUseRouteTable, SRDName, MediaRealm, InternalMediaRealm, ClassifyByProxySet, ProfileName, MaxNumOfRegUsers, InboundManSet, OutboundManSet, RegistrationMode, AuthenticationMode, MethodList, SBCServerAuthType, OAuthHTTPService, EnableSBCClientForking, SourceUriInput, DestUriInput, ContactName, Username, Password, UUIFormat, QOEProfile, BWProfile, AlwaysUseSourceAddr, MsgManUserDef1, MsgManUserDef2, SIPConnect, SBCPSAPMode, DTLSContext, CreatedByRoutingServer, UsedByRoutingServer, SBCOperationMode, SBCRouteUsingRequestURIPort, SBCKeepOriginalCallID, TopologyLocation, SBCDialPlanName, CallSetupRulesSetId, Tags, SBCUserStickiness, UserUDPPortAssignment, AdmissionProfile, ProxyKeepAliveUsingIPG, SBCAltRouteReasonsSetName, TeamsMediaOptimization, TeamsMOInitialBehavior, SIPSourceHostName; 61 | IPGroup 0 = 0, "AsteriskIPGroup", "AsteriskProxySet", "", "", "ASTERISK_HOST", -1, 0, "DefaultSRD", "AsteriskMedia", "", 1, "", -1, -1, -1, 0, 0, "", -1, "", 0, -1, -1, "INTERNAL_PRIVATE_IP", "", "$1$gQ==", 0, "", "", 1, "", "", 0, 0, "default", 0, 0, -1, 0, 0, 0, "", -1, "", 0, 0, "", 0, "", 0, 0, ""; 62 | IPGroup 1 = 0, "ChimeIPGroup", "ChimeProxySet", "", "OUTBOUND_HOST_NAME", "", -1, 0, "DefaultSRD", "ChimeMedia", "", 1, "", -1, -1, -1, 0, 0, "", -1, "", 0, -1, -1, "EXTERNAL_PUBLIC_IP", "", "$1$gQ==", 0, "", "", 1, "", "", 0, 0, "default", 0, 0, -1, 0, 0, 1, "", -1, "", 0, 0, "", 1, "", 0, 0, ""; 63 | IPGroup 2 = 0, "SIPRECIPGroup", "SIPRECProxySet", "", "STREAMING_HOST_NAME", "", -1, 0, "DefaultSRD", "ChimeMedia", "", 1, "", -1, -1, 1, 0, 0, "", -1, "", 0, -1, -1, "EXTERNAL_PUBLIC_IP", "", "$1$gQ==", 0, "", "", 1, "", "", 0, 0, "default", 0, 0, -1, 0, 0, 1, "", -1, "", 0, 0, "", 0, "", 0, 0, ""; 64 | [ \IPGroup ] 65 | 66 | 67 | [ ProxyIp ] 68 | 69 | FORMAT Index = ProxySetId, ProxyIpIndex, IpAddress, TransportType, Priority, Weight; 70 | ProxyIp 0 = "0", 0, "ASTERISK_HOST:5060", 0, 0, 0; 71 | ProxyIp 1 = "1", 0, "OUTBOUND_HOST_NAME:5060", 0, 0, 0; 72 | ProxyIp 2 = "2", 0, "STREAMING_HOST_NAME:5060", 0, 0, 0; 73 | 74 | [ \ProxyIp ] 75 | 76 | [ IP2IPRouting ] 77 | 78 | FORMAT Index = RouteName, RoutingPolicyName, SrcIPGroupName, SrcUsernamePrefix, SrcHost, DestUsernamePrefix, DestHost, RequestType, MessageConditionName, ReRouteIPGroupName, Trigger, CallSetupRulesSetId, DestType, DestIPGroupName, DestSIPInterfaceName, DestAddress, DestPort, DestTransportType, AltRouteOptions, GroupPolicy, CostGroup, DestTags, ModifiedDestUserName, SrcTags, IPGroupSetName, RoutingTagName, InternalAction; 79 | IP2IPRouting 1 = "terminate OPTIONS", "Default_SBCRoutingPolicy", "AsteriskIPGroup", "*", "*", "*", "*", 6, "", "Any", 0, -1, 1, "", "", "internal", 0, -1, 0, 0, "", "", "", "", "", "default", ""; 80 | IP2IPRouting 2 = "terminate OPTIONS", "Default_SBCRoutingPolicy", "ChimeIPGroup", "*", "*", "*", "*", 6, "", "Any", 0, -1, 1, "", "", "internal", 0, -1, 0, 0, "", "", "", "", "", "default", ""; 81 | IP2IPRouting 10 = "Asterisk -> Chime", "Default_SBCRoutingPolicy", "AsteriskIPGroup", "*", "*", "*", "*", 0, "", "Any", 0, -1, 0, "ChimeIPGroup", "ChimeInterface", "", 0, -1, 0, 0, "", "", "", "", "", "default", ""; 82 | IP2IPRouting 20 = "Chime -> Asterisk", "Default_SBCRoutingPolicy", "ChimeIPGroup", "*", "*", "*", "*", 0, "", "Any", 0, -1, 0, "AsteriskIPGroup", "AsteriskInterface", "", 0, -1, 0, 0, "", "", "", "", "", "default", ""; 83 | 84 | 85 | [ \IP2IPRouting ] 86 | 87 | [ MessageManipulations ] 88 | 89 | FORMAT Index = ManipulationName, ManSetID, MessageType, Condition, ActionSubject, ActionType, ActionValue, RowRole; 90 | MessageManipulations 0 = "SIPREC - src", 1, "Invite.Request", "Header.Contact regex (.*)(>;)(.*)", "Header.Contact", 2, "$1+$2+'+sip.src'", 0; 91 | MessageManipulations 1 = "SIPREC - From", 1, "Invite.Request", 'Body.application/rs-metadata regex (.*)( cdk-key.pem && chmod 400 cdk-key.pem' }) 258 | new cdk.CfnOutput(this, 'ssh command', { value: 'ssh -i cdk-key.pem -o IdentitiesOnly=yes ec2-user@' + asteriskInstnace.instancePublicIp }) 259 | new cdk.CfnOutput(this, 'PhoneNumber', { value: phoneNumber}), 260 | new cdk.CfnOutput(this, 'VoiceConnector', { value: outboundHostName}) 261 | new cdk.CfnOutput(this, 'AsteriskPrivateIP', { value: asteriskInstnace.instancePrivateIp}) 262 | new cdk.CfnOutput(this, 'AsteriskPublicIP', { value: asteriskInstnace.instancePublicIp}) 263 | new cdk.CfnOutput(this, 'downloadOVPNProfile', { value: 'scp -i cdk-key.pem -o IdentitiesOnly=yes ec2-user@' + asteriskInstnace.instancePublicIp + ':/tmp/ChimeDemo.ovpn ./'}) 264 | new cdk.CfnOutput(this, 'SBCInstance', { value: sbcInstance.instanceId }) 265 | new cdk.CfnOutput(this, 'streamingHostName', { value: streamingHostName}) 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /week-06/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ec2-cdk", 3 | "version": "0.1.0", 4 | "bin": { 5 | "ec2-cdk": "bin/ec2-cdk.js" 6 | }, 7 | "scripts": { 8 | "build": "tsc", 9 | "watch": "tsc -w", 10 | "cdk": "cdk" 11 | }, 12 | "devDependencies": { 13 | "@aws-cdk/assert": "^1.106.1", 14 | "@types/jest": "^26.0.23", 15 | "@types/node": "15.6.1", 16 | "aws-cdk": "^1.106.1", 17 | "ts-node": "^10.0.0", 18 | "typescript": "~4.3.2" 19 | }, 20 | "dependencies": { 21 | "@aws-cdk/aws-ec2": "^1.106.1", 22 | "@aws-cdk/aws-s3-deployment": "^1.106.1", 23 | "@aws-cdk/core": "^1.106.1", 24 | "@aws-cdk/custom-resources": "^1.106.1", 25 | "cdk-ec2-key-pair": "^2.2.0", 26 | "source-map-support": "^0.5.19" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /week-06/src/config.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -xe 2 | HOMEDIR=/home/ec2-user 3 | yum update -y 4 | yum install net-tools -y 5 | yum install wget -y 6 | amazon-linux-extras install epel -y 7 | yum -y install make gcc gcc-c++ make subversion libxml2-devel ncurses-devel openssl-devel vim-enhanced man glibc-devel autoconf libnewt kernel-devel kernel-headers linux-headers openssl-devel zlib-devel libsrtp libsrtp-devel uuid libuuid-devel mariadb-server jansson-devel libsqlite3x libsqlite3x-devel epel-release.noarch bash-completion bash-completion-extras unixODBC unixODBC-devel libtool-ltdl libtool-ltdl-devel mysql-connector-odbc mlocate libiodbc sqlite sqlite-devel sql-devel.i686 sqlite-doc.noarch sqlite-tcl.x86_64 patch libedit-devel jq 8 | 9 | mkdir /root/vpn 10 | cd /root/vpn 11 | curl -O https://raw.githubusercontent.com/angristan/openvpn-install/master/openvpn-install.sh 12 | chmod +x openvpn-install.sh 13 | 14 | export APPROVE_INSTALL=y 15 | export ENDPOINT=$(curl -4 ifconfig.co) 16 | export APPROVE_IP=y 17 | export IPV6_SUPPORT=n 18 | export PORT_CHOICE=1 19 | export PROTOCOL_CHOICE=2 20 | export DNS=1 21 | export COMPRESSION_ENABLED=n 22 | export CUSTOMIZE_ENC=n 23 | export CLIENT=ChimeDemo 24 | export PASS=1 25 | 26 | ./openvpn-install.sh 27 | 28 | chown ec2-user /root/ChimeDemo.ovpn 29 | cp /root/ChimeDemo.ovpn /tmp/ 30 | cd /tmp 31 | wget https://downloads.asterisk.org/pub/telephony/asterisk/asterisk-16-current.tar.gz 32 | tar xvzf asterisk-16-current.tar.gz 33 | cd asterisk-16*/ 34 | ./configure --libdir=/usr/lib64 --with-jansson-bundled 35 | make menuselect.makeopts 36 | menuselect/menuselect \ 37 | --disable BUILD_NATIVE \ 38 | --disable chan_sip \ 39 | --disable chan_skinny \ 40 | --enable cdr_csv \ 41 | --enable res_snmp \ 42 | --enable res_http_websocket \ 43 | menuselect.makeopts 44 | make 45 | make install 46 | make basic-pbx 47 | touch /etc/redhat-release 48 | make config 49 | ldconfig 50 | 51 | IP=$( curl http://169.254.169.254/latest/meta-data/public-ipv4 ) 52 | REGION=$( curl http://169.254.169.254/latest/meta-data/placement/region ) 53 | PhoneNumber=$( aws ssm get-parameter --name /asterisk/phoneNumber --region $REGION | jq -r '.Parameter.Value' ) 54 | VoiceConnectorHost=$( aws ssm get-parameter --name /asterisk/voiceConnector --region $REGION | jq -r '.Parameter.Value' ) 55 | OutboundHostName=$( aws ssm get-parameter --name /asterisk/outboundHostName --region $REGION | jq -r '.Parameter.Value' ) 56 | PrivateSBCAddress=$( aws ssm get-parameter --name /asterisk/privateSBCAddress --region $REGION | jq -r '.Parameter.Value' ) 57 | 58 | echo "[udp] 59 | type=transport 60 | protocol=udp 61 | bind=0.0.0.0 62 | allow_reload=yes 63 | 64 | [VoiceConnector] 65 | type=endpoint 66 | context=from-voiceConnector 67 | transport=udp 68 | disallow=all 69 | allow=ulaw 70 | aors=VoiceConnector 71 | direct_media=no 72 | ice_support=yes 73 | force_rport=yes 74 | 75 | [VoiceConnector] 76 | type=identify 77 | endpoint=VoiceConnector 78 | match=$PrivateSBCAddress 79 | 80 | [VoiceConnector] 81 | type=aor 82 | contact=sip:$PrivateSBCAddress 83 | 84 | [$PhoneNumber] 85 | type=endpoint 86 | context=from-phone 87 | disallow=all 88 | allow=ulaw 89 | transport=udp 90 | auth=$PhoneNumber 91 | aors=$PhoneNumber 92 | send_pai=yes 93 | direct_media=no 94 | rewrite_contact=yes 95 | ice_support=yes 96 | force_rport=yes 97 | 98 | [$PhoneNumber] 99 | type=auth 100 | auth_type=userpass 101 | password=ChimeDemo 102 | username=$PhoneNumber 103 | 104 | [$PhoneNumber] 105 | type=aor 106 | max_contacts=5" > /etc/asterisk/pjsip.conf 107 | 108 | echo "; extensions.conf - the Asterisk dial plan 109 | ; 110 | [general] 111 | static=yes 112 | writeprotect=no 113 | clearglobalvars=no 114 | 115 | [catch-all] 116 | exten => _[+0-9].,1,Answer() 117 | exten => _[+0-9].,n,Wait(1) 118 | exten => _[+0-9].,n,Playback(hello-world) 119 | exten => _[+0-9].,n,Wait(1) 120 | exten => _[+0-9].,n,echo() 121 | exten => _[+0-9].,n,Wait(1) 122 | exten => _[+0-9].,n,Hangup() 123 | 124 | [from-phone] 125 | include => outbound_phone 126 | 127 | [outbound_phone] 128 | exten => _+X.,1,NoOP(Outbound Normal) 129 | same => n,Dial(PJSIP/\${EXTEN}@VoiceConnector,20) 130 | same => n,Congestion 131 | 132 | [from-voiceConnector] 133 | include => phones 134 | include => catch-all 135 | 136 | [phones] 137 | exten => $PhoneNumber,1,Dial(PJSIP/$PhoneNumber)" > /etc/asterisk/extensions.conf 138 | 139 | echo "[options] 140 | runuser = asterisk 141 | rungroup = asterisk" > /etc/asterisk/asterisk.conf 142 | 143 | echo "[general] 144 | [logfiles] 145 | console = verbose,notice,warning,error 146 | messages = notice,warning,error" > /etc/asterisk/logger.conf 147 | 148 | groupadd asterisk 149 | useradd -r -d /var/lib/asterisk -g asterisk asterisk 150 | usermod -aG audio,dialout asterisk 151 | chown -R asterisk.asterisk /etc/asterisk 152 | chown -R asterisk.asterisk /var/{lib,log,spool}/asterisk 153 | 154 | systemctl start asterisk 155 | -------------------------------------------------------------------------------- /week-06/src/createVoiceConnector.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | import time 3 | import uuid 4 | 5 | chime = boto3.client('chime') 6 | 7 | def authorizeOrigination (voiceConnectorId, elasticIP): 8 | print("Authorizing Origination: " + voiceConnectorId) 9 | response = chime.put_voice_connector_origination( 10 | VoiceConnectorId=voiceConnectorId, 11 | Origination={ 12 | 'Routes': [ 13 | { 14 | 'Host': elasticIP, 15 | 'Port': 5060, 16 | 'Protocol': 'UDP', 17 | 'Priority': 1, 18 | 'Weight': 1 19 | }, 20 | ], 21 | 'Disabled': False 22 | } 23 | ) 24 | print("Origination Authorized: " + str(response)) 25 | 26 | def authorizeTermination (voiceConnectorId, elasticIP): 27 | print("Authorizing Termination: " + voiceConnectorId) 28 | response = chime.put_voice_connector_termination( 29 | VoiceConnectorId=voiceConnectorId, 30 | Termination={ 31 | 'CpsLimit': 1, 32 | 'CallingRegions': [ 33 | 'US', 34 | ], 35 | 'CidrAllowedList': [ 36 | elasticIP + '/32', 37 | ], 38 | 'Disabled': False 39 | } 40 | ) 41 | print("Termination Authorized: " + str(response)) 42 | 43 | def enableStreaming (voiceConnectorId): 44 | response = chime.put_voice_connector_streaming_configuration( 45 | VoiceConnectorId=voiceConnectorId, 46 | StreamingConfiguration={ 47 | 'DataRetentionInHours': 1, 48 | 'Disabled': False, 49 | 'StreamingNotificationTargets': [ 50 | { 51 | 'NotificationTarget': 'EventBridge' 52 | }, 53 | ] 54 | } 55 | ) 56 | print("Streaming Enabled: " + str(response)) 57 | 58 | def getPhoneNumber (): 59 | print("Getting Phone Number") 60 | search_response = chime.search_available_phone_numbers( 61 | # AreaCode='string', 62 | # City='string', 63 | # Country='string', 64 | State='IL', 65 | # TollFreePrefix='string', 66 | MaxResults=1 67 | ) 68 | phoneNumberToOrder = search_response['E164PhoneNumbers'][0] 69 | print ('Phone Number: ' + phoneNumberToOrder) 70 | phone_order = chime.create_phone_number_order( 71 | ProductType='VoiceConnector', 72 | E164PhoneNumbers=[ 73 | phoneNumberToOrder, 74 | ] 75 | ) 76 | print ('Phone Order: ' + str(phone_order)) 77 | 78 | check_phone_order = chime.get_phone_number_order( 79 | PhoneNumberOrderId=phone_order['PhoneNumberOrder']['PhoneNumberOrderId'] 80 | ) 81 | order_status = check_phone_order['PhoneNumberOrder']['Status'] 82 | timeout = 0 83 | 84 | while not order_status == 'Successful': 85 | timeout += 1 86 | print('Checking status: ' + str(order_status)) 87 | time.sleep(5) 88 | check_phone_order = chime.get_phone_number_order( 89 | PhoneNumberOrderId=phone_order['PhoneNumberOrder']['PhoneNumberOrderId'] 90 | ) 91 | order_status = check_phone_order['PhoneNumberOrder']['Status'] 92 | if timeout == 5: 93 | return 'Could not get phone number' 94 | print ("Phone Number Ordered: " + phoneNumberToOrder) 95 | return phoneNumberToOrder 96 | 97 | def createVoiceConnector (region): 98 | print("Creating Voice Connector") 99 | response = chime.create_voice_connector( 100 | Name='Trunk' + str(uuid.uuid1()), 101 | AwsRegion='us-east-1', 102 | RequireEncryption=False 103 | ) 104 | 105 | voiceConnectorId = response['VoiceConnector']['VoiceConnectorId'] 106 | outboundHostName = response['VoiceConnector']['OutboundHostName'] 107 | voiceConnector = { 'voiceConnectorId': voiceConnectorId, 'outboundHostName': outboundHostName} 108 | print("Voice Connector Created: " + str(response)) 109 | print("voiceConnector: " + str(voiceConnector)) 110 | return voiceConnector 111 | 112 | def assoiciatePhoneNumber (voiceConnector, phoneNumber): 113 | print("Associating Phone Number: " + str(voiceConnector)) 114 | print("Associating Phone Number: " + phoneNumber) 115 | response = chime.associate_phone_numbers_with_voice_connector( 116 | VoiceConnectorId=voiceConnector['voiceConnectorId'], 117 | E164PhoneNumbers=[ 118 | phoneNumber, 119 | ], 120 | ForceAssociate=True 121 | ) 122 | print("Phone Number associated: " + str(response)) 123 | voiceConnector['phoneNumber'] = phoneNumber 124 | print("voiceConnector: "+ str(voiceConnector)) 125 | return voiceConnector 126 | 127 | 128 | def on_event(event, context): 129 | print(event) 130 | request_type = event['RequestType'] 131 | if request_type == 'Create': return on_create(event) 132 | if request_type == 'Update': return on_update(event) 133 | if request_type == 'Delete': return on_delete(event) 134 | raise Exception("Invalid request type: %s" % request_type) 135 | 136 | def on_create(event): 137 | physical_id = 'VoiceConnectorResources' 138 | region = event['ResourceProperties']['region'] 139 | elasticIP = event['ResourceProperties']['eip'] 140 | streaming = event['ResourceProperties']['streaming'] 141 | print('Streaming: ' + streaming) 142 | 143 | if streaming == 'true': 144 | print("Creating VoiceConnector with Streaming") 145 | voiceConnector = createVoiceConnector(region) 146 | authorizeTermination(voiceConnector['voiceConnectorId'], elasticIP) 147 | enableStreaming(voiceConnector['voiceConnectorId']) 148 | voiceConnector['phoneNumber'] = '' 149 | else: 150 | print("Creating VoiceConnector with Phone Number and without Streaming") 151 | newPhoneNumber = getPhoneNumber() 152 | voiceConnector = createVoiceConnector(region) 153 | voiceConnector = assoiciatePhoneNumber(voiceConnector,newPhoneNumber) 154 | authorizeTermination(voiceConnector['voiceConnectorId'], elasticIP) 155 | authorizeOrigination(voiceConnector['voiceConnectorId'], elasticIP) 156 | 157 | print(str(voiceConnector)) 158 | return { 'PhysicalResourceId': physical_id, 'Data': voiceConnector } 159 | 160 | def on_update(event): 161 | physical_id = event["PhysicalResourceId"] 162 | props = event["ResourceProperties"] 163 | print("update resource %s with props %s" % (physical_id, props)) 164 | return { 'PhysicalResourceId': physical_id } 165 | 166 | 167 | def on_delete(event): 168 | physical_id = event["PhysicalResourceId"] 169 | print("delete resource %s" % physical_id) 170 | return { 'PhysicalResourceId': physical_id } 171 | -------------------------------------------------------------------------------- /week-06/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2018", 4 | "module": "commonjs", 5 | "lib": ["es2018"], 6 | "declaration": true, 7 | "strict": true, 8 | "noImplicitAny": true, 9 | "strictNullChecks": true, 10 | "noImplicitThis": true, 11 | "alwaysStrict": true, 12 | "noUnusedLocals": false, 13 | "noUnusedParameters": false, 14 | "noImplicitReturns": true, 15 | "noFallthroughCasesInSwitch": false, 16 | "inlineSourceMap": true, 17 | "inlineSources": true, 18 | "experimentalDecorators": true, 19 | "strictPropertyInitialization": false, 20 | "typeRoots": ["./node_modules/@types"] 21 | }, 22 | "exclude": ["cdk.out"] 23 | } 24 | --------------------------------------------------------------------------------