├── .dockerignore ├── .github └── workflows │ └── testimage.yml ├── .gitignore ├── .gitmodules ├── .travis.yml ├── CHANGELOG.md ├── Dockerfile ├── LICENSE.txt ├── Makefile ├── README.md ├── ROADMAP.md ├── bld.mk ├── demo ├── .env ├── .gitignore ├── Makefile ├── dkr.mk └── docker-compose.yml ├── dkr.mk ├── src ├── acme │ ├── bin │ │ └── acme-extract.sh │ └── entry.d │ │ ├── 10-acme-common │ │ └── 50-acme-monitor-tlscert ├── asterisk │ ├── config │ │ ├── alsa.conf │ │ ├── asterisk.conf │ │ ├── ccss.conf │ │ ├── cli_aliases.conf │ │ ├── console.conf │ │ ├── features.conf │ │ ├── indications.conf │ │ ├── logger.conf │ │ ├── modules.conf │ │ ├── musiconhold.conf │ │ ├── pjproject.conf │ │ └── rtp.conf │ └── entry.d │ │ ├── 50-asterisk-seed-conf │ │ └── 51-asterisk-generate-tlscert ├── autoban │ ├── README.md │ ├── config │ │ ├── autoban.conf │ │ └── manager.conf │ ├── doc │ │ ├── autoban.conf.sample │ │ └── autoban.md │ ├── entry.d │ │ └── 50-autoban-read-nftfile │ ├── exit.d │ │ └── 50-autoban-write-nftfile │ ├── nft │ │ └── autoban.nft │ └── php │ │ ├── autoban.class.inc │ │ ├── autoban.php │ │ ├── autoband.php │ │ └── nft.class.inc ├── common │ └── php │ │ └── error.inc ├── docker │ ├── bin │ │ ├── docker-common.sh │ │ ├── docker-config.sh │ │ ├── docker-entrypoint.sh │ │ └── docker-service.sh │ └── entry.d │ │ ├── 20-docker-print-versions │ │ └── 50-docker-update-loglevel ├── notused │ ├── bin │ │ ├── astqueue.sh │ │ ├── relpath │ │ └── voipbl.sh │ ├── entry.d │ │ └── 20-cronjob-voipbl │ └── php │ │ └── amiupdateconfig.class.inc ├── privatedial │ ├── README.md │ ├── bin │ │ └── minivm-send │ ├── config │ │ ├── extensions.conf │ │ ├── extensions_local.conf │ │ ├── minivm.conf │ │ ├── pjsip.conf │ │ ├── pjsip_endpoint.conf │ │ ├── pjsip_transport.conf │ │ └── pjsip_wizard.conf │ └── doc │ │ └── privatedial.md └── websms │ ├── README.md │ ├── config │ └── websms.conf │ ├── doc │ ├── websms.conf.sample │ └── websms.md │ ├── entry.d │ ├── 30-websms-migrate │ └── 50-websms-update-port │ └── php │ ├── astqueue.class.inc │ ├── websms.class.inc │ ├── websms.php │ └── websmsd.php └── test ├── Makefile ├── acme └── .gitignore ├── bld.mk ├── dkr.mk ├── ssl.mk └── ssl └── .gitignore /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .gitignore 3 | .github 4 | .gitattributes 5 | src/notused 6 | local 7 | demo/ 8 | -------------------------------------------------------------------------------- /.github/workflows/testimage.yml: -------------------------------------------------------------------------------- 1 | name: Docker Image Makefile CI 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | 15 | - name: Build docker images 16 | run: make build-all 17 | 18 | - name: Run tests 19 | shell: 'script -q -e -c "bash {0}"' 20 | run: | 21 | make test-all 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *private 2 | local* 3 | sub/* 4 | !sub/module 5 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "sub/module/phpami"] 2 | path = sub/module/phpami 3 | url = https://github.com/ofbeaton/phpami.git 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | os: linux 3 | dist: jammy 4 | services: docker 5 | install: make build-all 6 | script: make test-all 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 1.1.12 2 | 3 | - [build](Makefile) Now use alpine:3.22 (asterisk:20.11.1). 4 | 5 | # 1.1.11 6 | 7 | - [build](Makefile) Now use alpine:3.21 (asterisk:20.11.0). 8 | - [build](Makefile) Switch to php84. 9 | 10 | # 1.1.10 11 | 12 | - [build](Makefile) Now use alpine:3.20 (asterisk:20.9.3). 13 | - [asterisk](src/asterisk/config/modules.conf) Add modules that are not loaded by default helping to get rid of error messages during startup. 14 | - [asterisk](src/asterisk/config/console.conf) Add `console.conf` so we can use `chan_console.so` since `chan_alsa.so` will be removed in asterisk:21. 15 | - [demo](demo/Makefile) Now use `chan_console.so` since `chan_alsa.so` will be removed in asterisk:21. 16 | 17 | # 1.1.9 18 | 19 | - [build](Makefile) Now use alpine:3.20 (asterisk:20.8.1). 20 | - [build](Dockerfile) Switch to php83. 21 | - [demo](demo) Fixed target `apk_list`. 22 | 23 | # 1.1.8 24 | 25 | - [build](Makefile) Due to changed behavior with docker 26, files are now copied on the host instead of being hard-linked. 26 | - [build](Makefile) Now set `BLD_CVER ?= ast200` to match codec and asterisk versions. 27 | - [demo](demo/docker-compose.yml) Remove obsolete element `version` in docker-compose.yml. 28 | 29 | # 1.1.7 30 | 31 | - [build](Makefile) Now use alpine:3.19 (asterisk:20.5.2). 32 | - [build](Dockerfile) Switch to php82 and make it a build argument: `--build-arg PHP_VER=php82`. 33 | - [build](Makefile) Now set `DOCKER_BUILDKIT=1` since we don't use special symbolic links. 34 | - [build](bld.mk) Harmonize [bld.mk](bld.mk) between repositories. 35 | - [build](dkr.mk) Harmonize [dkr.mk](dkr.mk) between repositories. 36 | - [docker](src/docker) Added switch to set run as user [docker-service.sh](src/docker/bin/docker-service.sh). 37 | - [docker](src/docker) Improve debug message in [docker-service.sh](src/docker/bin/docker-service.sh). 38 | 39 | 40 | # 1.1.6 41 | 42 | - [build](Makefile) Now use alpine:3.18 (asterisk:18.15.1). 43 | - [build](Dockerfile) Removed unavaiable package. 44 | 45 | # 1.1.5 46 | 47 | - [github](.github/workflows/testimage.yml) Now use GitHub Actions to test image. 48 | - [demo](demo/Makefile) Now depend on the docker-compose-plugin. 49 | - [build](Makefile) Set `DOCKER_BUILDKIT=0` to make `docker build` handle our symbolic links as we intended. 50 | 51 | # 1.1.4 52 | 53 | - [privatedial](src/privatedial) Allow all TLS protocols in the `minivm-send` bash script. 54 | - [test](test/Makefile) Now also test the PHP is setup correctly. 55 | 56 | # 1.1.3 57 | 58 | - [repo](src) Fix the bug; websms and autoban not working. We need to use softlinks to ../../share/php81/. 59 | - [docker](README.md) Corrected misspelling. 60 | 61 | # 1.1.2 62 | 63 | - [build](Dockerfile) Fix the bug; websmsd not working. We need to set DOCKER_PHP_DIR=/usr/share/php81. 64 | 65 | # 1.1.1 66 | 67 | - [build](Makefile) Now use alpine:3.17 (asterisk:18.15.0). 68 | - [build](Dockerfile) Switch to php81. 69 | - [test](.travis.yml) Updated dist to jammy. 70 | 71 | # 1.1.0 72 | 73 | - [build](Makefile) Now use alpine:3.16 (asterisk:18.11.2). 74 | - [build](Dockerfile) Switch to php8. 75 | - [demo](demo) Switch to php8. 76 | - [privatedial](src/privatedial) BREAKING. In `extensions.conf` now use `APP_SMS = /usr/local/bin/websms`. 77 | 78 | # 1.0.0 79 | 80 | - [docker](src/docker) Now use alpine:3.15 (asterisk:18.2.2). 81 | - [autoban](src/autoban) Let autoban manipulate nft without breaking docker networking. Since docker 5:20.10 container DNS resolve is based on nft rules (with iptables-nft) which autoban's nft implementation interfered with resulting in container unable to resolve network names. 82 | - [autoban](src/autoban) Now use DOCKER_NFT_DIR=/etc/nftables.d. 83 | - [autoban](src/autoban) Only load `.nft` files if nft is installed. 84 | - [build](Makefile) Now push images to registry. 85 | - [demo](demo) Updated demo to also work with docker-compose >= 2. 86 | - [test](test/Makefile) Added container name-space network inspection targets. 87 | 88 | # 0.9.9 89 | 90 | - [docker](src/docker) Now use alpine:3.14 (asterisk:18.2.2). 91 | - [docker](ROADMAP.md) Use [travis-ci.com](https://travis-ci.com/). 92 | 93 | # 0.9.8 94 | 95 | - [autoban](src/autoban) Let autoband handle AMI connection failures nicely. 96 | 97 | # 0.9.7 98 | 99 | - [docker](src/docker) Now use alpine:3.13 (asterisk:18.1.1). 100 | - [test](test/Makefile) Move tests into test dir. 101 | 102 | # 0.9.6 103 | 104 | - [repo](hooks) Fixed bug in hooks/pre_build. Use curl in `make pre_build`. 105 | 106 | # 0.9.5 107 | 108 | - [codec](sub/codec) Provide the [G.729](https://en.wikipedia.org/wiki/G.729) and [G.723.1](https://en.wikipedia.org/wiki/G.723.1) audio codecs. 109 | - [codec](sub/codec) Improved handling of codec versions (`BLD_CVER` in Makefile). 110 | 111 | # 0.9.4 112 | 113 | - [websms](src/websms) Use `prox_addr = 172.16.0.0/12,192.168.0.0/16` by default. 114 | 115 | # 0.9.3 116 | 117 | - [acme](src/acme) Introduce `ACME_POSTHOOK="sv restart asterisk"` and run that after we have updated the certificates. 118 | - [docker](src/docker) Don't move `DOCKER_APPL_SSL_DIR=$DOCKER_SSL_DIR/asterisk` to persistent storage. Data there is updated at container startup anyway. Moreover there is no need to remove old data when it is updated. 119 | - [privatedial](src/privatedial) In `pjsip_transport.conf` set `method=tlsv1_2` to harden TLS. 120 | 121 | # 0.9.2 122 | 123 | - [docker](src/docker) Use the native envvar `SVDIR` instead of `DOCKER_RUNSV_DIR`. 124 | - [docker](src/docker) Update docker-common.sh. 125 | - [docker](src/docker) Now use docker-config.sh. 126 | - [docker](src/docker) Update docker-entrypoint.sh. 127 | - [docker](src/docker) Update docker-service.sh. 128 | - [docker](src/docker) Now use DOCKER_ENTRY_DIR=/etc/docker/entry.d and DOCKER_EXIT_DIR=/etc/docker/exit.d. 129 | - [docker](Makefile) Improved smoke test. 130 | - [acme](src/acme/bin/acme-extract.sh) Update module. 131 | - [privatedial](src/privatedial) Breaking change. Now use `cert_file=/etc/ssl/asterisk/cert.pem` and `priv_key_file=/etc/ssl/asterisk/priv_key.pem` 132 | 133 | # 0.9.1 134 | 135 | - [repo](hooks) Added hooks/pre_build which assembles files from sub-modules. 136 | - [repo](.travis.yml) Revisited `.travis.yml`. 137 | - [docker](README.md) Proofread documentation. 138 | - [docker](README.md) Fixed broken hyperlinks in documentation. 139 | 140 | # 0.9.0 141 | 142 | - [privatedial](src/privatedial) Break out endpoints from pjsip_wizard.conf to pjsip_endpoint.conf. 143 | - [privatedial](src/privatedial) Use Hangup() instead of Goto() when entering extension `h`. 144 | - [privatedial](src/privatedial) Work around bug in [MinivmGreet()](https://wiki.asterisk.org/wiki/display/AST/Asterisk+16+Application_MinivmGreet). 145 | - [privatedial](src/privatedial) Renamed dialplan contexts. 146 | - [privatedial](src/privatedial) Dialplan `[sub_voicemail]` now handles CHANUNAVAIL correctly. 147 | - [privatedial](src/privatedial) Added `endpoint/bind_rtp_to_media_address = yes` 148 | - [docker](README.md) Complete documentation. 149 | - [docker](src/docker) Now use alpine:3.12 (asterisk:16.7.0). 150 | - [websms](src/websms) `WEBSMSD_PORT=80` sets PHP web server port, used by WebSMS. 151 | - [repo](src) Harmonized file names in `entry.d` and `exit.d`. 152 | - [repo](sub) Use git submodule for third party projects. 153 | 154 | # 0.8.0 155 | 156 | - [websms](src/websms) Harmonized configuration parameter names. 157 | - [websms](src/websms) Harmonized function names. 158 | - [websms](src/websms) Facilitate static key-value pairs, `val_static = "key1=value1,key2=value2"`. 159 | - [websms](src/websms) Parameter `val_numform`, now takes `E.164` (omit +) and `E.123`. 160 | - [websms](src/websms) Improved Unicode configuration, allowing `val_unicode = "key=value"`. 161 | - [websms](src/websms) Added authorization methods, `plain` and `none`. 162 | - [websms](src/websms) Allow multiple API interfaces to be configured. 163 | - [websms](src/websms) Now accept incoming message with null body. 164 | - [websms](src/websms) Code clean up. 165 | - [privatedial](src/privatedial) Use set_var=TRUNK_ENDPOINT to set outgoing target for each endpoint individually. 166 | - [privatedial](src/privatedial) Don't use `endpoint/from_user`, it overwrites CallerID. 167 | 168 | # 0.7.0 169 | 170 | - [acme](src/acme/bin/acme-extract.sh) Support both v1 and v2 formats of the acme.json file. 171 | - [acme](src/acme/entry.d/50-acme-monitor-tlscert) Support both host and domain wildcard TLS certificates. 172 | - [websms](src/websms) Complete documentation. 173 | - [privatedial](src/privatedial) Advancing documentation. 174 | - [docker](README.md) Advancing documentation. 175 | - [docker](src/notused) Cleanup `src/notused`. 176 | - [docker](src/docker) Consolidate common functions in src/docker/bin/docker-common.sh. 177 | 178 | # 0.6.0 179 | 180 | - [docker](Dockerfile) Audio via PulseAudio. 181 | - [docker](src/docker) Now use alpine:3.11 (asterisk:16.6.2). 182 | - [demo](demo) Added demo. 183 | - [demo](demo) Enabled audio via PulseAudio socket and cookie. 184 | - [demo](demo) Use host timezone by mounting /etc/localtime. 185 | - [websms](src/websms) Updating documentation. 186 | - [privatedial](src/privatedial) Added demo-echotest in IVR. 187 | - [privatedial](src/privatedial) Fixed initiation issue for minivm. 188 | 189 | # 0.5.2 190 | 191 | - [websms](src/websms) Fixing bugs related to special characters in SMS messages 192 | - [websms](src/websms) Added `val_unicode` parameter. Set to `ucs-2` to make sure all characters are within the Unicode BMP (up to U+FFFF). 193 | - [websms](src/websms) Updating documentation. 194 | - [websms](src/websms) Refactoring of `astqueue.class.ini` to better cope with message encoding. 195 | - [privatedial](src/privatedial) added `sub_decode_body` to cope with encoded messages. 196 | 197 | # 0.5.1 198 | 199 | - [docker](Makefile) Enable PHP profiling using xdebug. 200 | - [autoban](src/autoban) Optimized code with respect to efficiency and speed. 201 | - [autoban](src/autoban) Improved command line options of the shell utility. 202 | 203 | # 0.5.0 204 | 205 | - [acme](src/acme) Fixed dumpcert.sh leaking to stdout. Have it write to logger instead. 206 | - [autoban](src/autoban) Added shell utility autoban, which helps to manage the NFT state 207 | - [autoban](src/autoban) Updated documentation. 208 | - [autoban](src/autoban) Now write to autoban.nft every time we get a security event and update NFT, so that its state is always preserved. 209 | - [autoban](src/autoban) Code base now refactored and split into autoban.class.inc and nft.class.inc 210 | - [websms](src/websms) Updated documentation. 211 | 212 | # 0.4.0 213 | 214 | - [privatedial](src/privatedial) Now keep main dial-plan conf files separate. 215 | - [privatedial](src/privatedial) Start to document the PrivateDial dial-plan. 216 | - [autoban](src/autoban) Now don't crash if autoban.conf does not have both an `[autoban]` and an `[nftables]` section. 217 | - [autoban](src/autoban) Renamed autoband.php (it was autoban.php) 218 | - [autoban](src/autoban) Updated documentation. 219 | - [asterisk](src/asterisk) Added Networking section in README.md. 220 | 221 | # 0.3.0 222 | 223 | - [acme](src/acme) New support for [Let’s Encrypt](https://letsencrypt.org/) TLS certificates using [Traefik](https://traefik.io/) using `ACME_FILE=/acme/acme.json`. 224 | - [asterisk](src/asterisk) Configuration now supports UDP, TCP and TLS and SDES. 225 | - [asterisk](src/asterisk) Generate self-signed TLS certificate. 226 | - [asterisk](src/asterisk) Improved structure of `pjsip_wizard.conf`. 227 | - [asterisk](src/asterisk) Don't answer when device is UNAVAILABLE in `[dp_answer]` 228 | - [docker](src/docker) The [docker-service.sh](src/docker/bin/docker-service.sh) script now have options: down, force, log, name, source, quiet. 229 | - [websms](src/websms) Added `val_numform` parameter. Set to `E164` to strip phone numbers from leading +. 230 | 231 | # 0.2.1 232 | 233 | - [asterisk](src/asterisk) Sanitize incoming extensions so they are all international 234 | - [asterisk](src/asterisk) Move APP_SMS global to extensions.conf 235 | - [websms](src/websms) Use `$_POST` since `file_get_contents("php://input")` cannot handle multipart/form-data 236 | - [websms](src/websms) Allow IP addr filtering behind proxy by using HTTP_X_FORWARDED_FOR 237 | - [websms](src/websms) websmsd.php parameters are json decoded and searched recursively 238 | - [websms](src/websms) Also support Zadarma POST parameters in websms.class.inc 239 | - [websms](src/websms) Started WebSMS (separate) documentation 240 | - [autoban](src/autoban) Fixed new bug in autoban.class.inc 241 | - [autoban](src/autoban) Added conf sample file autoban.conf.sample 242 | 243 | # 0.2.0 244 | 245 | - [repo](src) Now reorganize repo files according to which service they provide 246 | - [docker](Dockerfile) alpine 3.10.3 released so now build using alpine:3.10 247 | - [docker](Dockerfile) Added Health check 248 | - [docker](src/docker) Introduce a `SIGTERM` trap in `docker-entrypoint.sh` allowing graceful container termination with `exit.d` script execution 249 | - [docker](src/docker) [docker-service.sh](src/docker/bin/docker-service.sh) now also take switches -n and -l. 250 | - [docker](src/docker) We now create directory structure when an empty volume is mounted at /srv. 251 | - [asterisk](src/asterisk) Based on live testing updated templates in pjsip_wizard.conf 252 | - [asterisk](src/asterisk) Now use extensions-local.conf to keep all local configurations 253 | - [asterisk](src/asterisk) Fixed typo in rtp.conf 254 | - [websms](src/websms) Retired service sms/d which has been succeeded by websms/d 255 | - [websms](src/websms) New verify POST request in websms.class.inc 256 | - [websms](src/websms) New check source IP address in websms.class.inc 257 | - [autoban](src/autoban) New service Autoban, which listens to security AMI events and dynamically configures nftables to block abusing IPs. 258 | - [autoban](src/autoban) autoban.class.inc (formerly nft.class.inc) is now state less 259 | - [autoban](src/autoban) Restricting Autoban's AMI access to a minimum 260 | - [autoban](src/autoban) Autoban now has `repeatmult` punishing repeat offenders progressively more severely 261 | - [autoban](src/autoban) Autoban now use nftables timeouts 262 | - [autoban](src/autoban) Added `entry.d` and `exit.d` scripts so that the `nft` state is loaded/saved at container startup/shutdown. 263 | 264 | # 0.1.0 265 | 266 | - [docker](Dockerfile) Using alpine:3.9 since for alpine:3.10 there are dependency errors reported when asterisk starts. 267 | - [privatedial](src/privatedial) minivm-send bash script simplify minivm configuration. 268 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG DIST=alpine 2 | ARG REL=latest 3 | 4 | 5 | # 6 | # 7 | # target: mini 8 | # 9 | # asterisk, minimal 10 | # 11 | # 12 | 13 | FROM $DIST:$REL AS mini 14 | ARG PHP_VER=php84 15 | LABEL maintainer=mlan 16 | 17 | ENV PHP_VER=$PHP_VER \ 18 | SVDIR=/etc/service \ 19 | DOCKER_PERSIST_DIR=/srv \ 20 | DOCKER_BIN_DIR=/usr/local/bin \ 21 | DOCKER_ENTRY_DIR=/etc/docker/entry.d \ 22 | DOCKER_EXIT_DIR=/etc/docker/exit.d \ 23 | DOCKER_PHP_DIR=/usr/share/$PHP_VER \ 24 | DOCKER_SPOOL_DIR=/var/spool/asterisk \ 25 | DOCKER_CONF_DIR=/etc/asterisk \ 26 | DOCKER_LOG_DIR=/var/log/asterisk \ 27 | DOCKER_LIB_DIR=/var/lib/asterisk \ 28 | DOCKER_DL_DIR=/usr/lib/asterisk/modules \ 29 | DOCKER_NFT_DIR=/etc/nftables.d \ 30 | DOCKER_SEED_CONF_DIR=/usr/share/asterisk/config \ 31 | DOCKER_SEED_NFT_DIR=/usr/share/nftables \ 32 | DOCKER_SSL_DIR=/etc/ssl \ 33 | ACME_POSTHOOK="sv restart asterisk" \ 34 | SYSLOG_LEVEL=4 \ 35 | SYSLOG_OPTIONS=-SDt \ 36 | WEBSMSD_PORT=80 37 | ENV DOCKER_MOH_DIR=$DOCKER_LIB_DIR/moh \ 38 | DOCKER_ACME_SSL_DIR=$DOCKER_SSL_DIR/acme \ 39 | DOCKER_APPL_SSL_DIR=$DOCKER_SSL_DIR/asterisk 40 | 41 | # 42 | # Copy utility scripts including docker-entrypoint.sh to image 43 | # 44 | 45 | COPY src/*/bin $DOCKER_BIN_DIR/ 46 | COPY src/*/entry.d $DOCKER_ENTRY_DIR/ 47 | COPY src/*/exit.d $DOCKER_EXIT_DIR/ 48 | COPY src/*/php $DOCKER_PHP_DIR/ 49 | COPY sub/*/php $DOCKER_PHP_DIR/ 50 | COPY src/*/config $DOCKER_SEED_CONF_DIR/ 51 | COPY src/*/nft $DOCKER_SEED_NFT_DIR/ 52 | 53 | # 54 | # Facilitate persistent storage and install asterisk 55 | # 56 | 57 | RUN source docker-common.sh \ 58 | && source docker-config.sh \ 59 | && dc_persist_dirs \ 60 | $DOCKER_APPL_SSL_DIR \ 61 | $DOCKER_CONF_DIR \ 62 | $DOCKER_LOG_DIR \ 63 | $DOCKER_MOH_DIR \ 64 | $DOCKER_NFT_DIR \ 65 | $DOCKER_SPOOL_DIR \ 66 | && mkdir -p $DOCKER_ACME_SSL_DIR \ 67 | && ln -sf $DOCKER_PHP_DIR/autoban.php $DOCKER_BIN_DIR/autoban \ 68 | && ln -sf $DOCKER_PHP_DIR/websms.php $DOCKER_BIN_DIR/websms \ 69 | && apk --no-cache --update add \ 70 | asterisk 71 | 72 | # 73 | # Entrypoint, how container is run 74 | # 75 | 76 | ENTRYPOINT ["docker-entrypoint.sh"] 77 | CMD ["asterisk", "-fp"] 78 | 79 | 80 | # 81 | # 82 | # target: base 83 | # 84 | # asterisk add-ons: WebSMS and AutoBan 85 | # 86 | # 87 | 88 | FROM mini AS base 89 | 90 | # 91 | # Install packages used by the add-ons and register services 92 | # 93 | 94 | RUN apk --no-cache --update add \ 95 | asterisk-curl \ 96 | asterisk-speex \ 97 | asterisk-srtp \ 98 | openssl \ 99 | curl \ 100 | $PHP_VER \ 101 | $PHP_VER-curl \ 102 | $PHP_VER-json \ 103 | runit \ 104 | bash \ 105 | nftables \ 106 | jq \ 107 | && ln -sf /usr/bin/$PHP_VER /usr/bin/php \ 108 | && docker-service.sh \ 109 | "syslogd -nO- -l$SYSLOG_LEVEL $SYSLOG_OPTIONS" \ 110 | "crond -f -c /etc/crontabs" \ 111 | "-q asterisk -pf" \ 112 | "-n websmsd php -S 0.0.0.0:$WEBSMSD_PORT -t $DOCKER_PHP_DIR websmsd.php" \ 113 | "$DOCKER_PHP_DIR/autoband.php" \ 114 | && mkdir -p /var/spool/asterisk/staging 115 | 116 | # 117 | # Have runit's runsvdir start all services 118 | # 119 | 120 | CMD runsvdir -P ${SVDIR} 121 | 122 | # 123 | # Check if all services are running 124 | # 125 | 126 | HEALTHCHECK CMD sv status ${SVDIR}/* 127 | 128 | 129 | # 130 | # 131 | # target: full 132 | # 133 | # Add sounds and configure ALSA pluging to PulseAudio 134 | # 135 | # 136 | 137 | FROM base AS full 138 | 139 | # 140 | # Copy patent-encumbered codecs to image 141 | # 142 | 143 | COPY sub/*/module $DOCKER_DL_DIR/ 144 | 145 | # 146 | # Install packages supporting audio 147 | # 148 | 149 | RUN apk --no-cache --update add \ 150 | asterisk-alsa \ 151 | alsa-plugins-pulse \ 152 | asterisk-sounds-en \ 153 | sox 154 | 155 | # 156 | # 157 | # target: extra 158 | # 159 | # all asterisk packages 160 | # 161 | # 162 | 163 | FROM full AS xtra 164 | 165 | # 166 | # Install all asterisk packages 167 | # 168 | 169 | RUN apk --no-cache --update add \ 170 | asterisk-doc \ 171 | asterisk-fax \ 172 | asterisk-mobile \ 173 | asterisk-odbc \ 174 | asterisk-pgsql \ 175 | asterisk-tds \ 176 | asterisk-dbg \ 177 | asterisk-dev \ 178 | asterisk-sounds-moh \ 179 | man-pages 180 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 mlan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Makefile 2 | # 3 | # build 4 | # 5 | 6 | -include *.mk 7 | 8 | BLD_ARG ?= --build-arg DIST=alpine --build-arg REL=3.22 --build-arg PHP_VER=php84 9 | BLD_REPO ?= mlan/asterisk 10 | BLD_VER ?= latest 11 | BLD_TGT ?= full 12 | BLD_TGTS ?= mini base full xtra 13 | BLD_CMT ?= HEAD 14 | BLD_CVER ?= ast200 15 | BLD_DNLD ?= curl -o 16 | BLD_KIT ?= DOCKER_BUILDKIT=1 17 | 18 | TST_REPO ?= $(BLD_REPO) 19 | TST_VER ?= $(BLD_VER) 20 | TST_ENV ?= -C test 21 | TST_TGTE ?= $(addprefix test-,all diff down env htop logs sh sv up) 22 | TST_INDX ?= 1 2 3 4 23 | TST_TGTI ?= $(addprefix test_,$(TST_INDX)) $(addprefix test-up_,$(TST_INDX)) 24 | 25 | export TST_REPO TST_VER 26 | 27 | push: 28 | # 29 | # PLEASE REVIEW THESE IMAGES WHICH ARE ABOUT TO BE PUSHED TO THE REGISTRY 30 | # 31 | @docker image ls $(BLD_REPO) 32 | # 33 | # ARE YOU SURE YOU WANT TO PUSH THESE IMAGES TO THE REGISTRY? [yN] 34 | @read input; [ "$${input}" = "y" ] 35 | docker push --all-tags $(BLD_REPO) 36 | 37 | build-all: $(addprefix build_,$(BLD_TGTS)) 38 | 39 | build: build_$(BLD_TGT) 40 | 41 | build_%: pre_build 42 | $(BLD_KIT) docker build $(BLD_ARG) --target $* \ 43 | $(addprefix --tag $(BLD_REPO):,$(call bld_tags,$*,$(BLD_VER))) . 44 | 45 | pre_build: Dockerfile pre_autoban pre_codecs 46 | 47 | 48 | pre_autoban: sub/autoban/php/ami.class.inc 49 | 50 | 51 | pre_codecs: codec_g723.so codec_g729.so 52 | 53 | 54 | sub/autoban/php/ami.class.inc: submodule 55 | mkdir -p $(@D) 56 | cp --remove-destination sub/module/phpami/src/Ami.php $@ 57 | 58 | submodule: 59 | git submodule update --init --recursive 60 | 61 | codec_%.so: sub/codecs/download/codec_%-$(BLD_CVER).so 62 | mkdir -p sub/codecs/module 63 | cp --remove-destination $< sub/codecs/module/$@ 64 | 65 | .PRECIOUS: sub/codecs/download/%.so 66 | 67 | sub/codecs/download/%.so: 68 | mkdir -p $(@D) 69 | $(BLD_DNLD) $@ http://asterisk.hosting.lv/bin/$*-gcc4-glibc-x86_64-core2.so 70 | chmod 0755 $@ 71 | 72 | variables: 73 | make -pn | grep -A1 "^# makefile"| grep -v "^#\|^--" | sort | uniq 74 | 75 | ps: 76 | docker ps -a 77 | 78 | prune: 79 | docker image prune -f 80 | 81 | clean: 82 | docker images | grep $(BLD_REPO) | awk '{print $$1 ":" $$2}' | uniq | xargs docker rmi || true 83 | 84 | $(TST_TGTE): 85 | ${MAKE} $(TST_ENV) $@ 86 | 87 | $(TST_TGTI): 88 | ${MAKE} $(TST_ENV) $@ 89 | -------------------------------------------------------------------------------- /ROADMAP.md: -------------------------------------------------------------------------------- 1 | # Road map 2 | 3 | ## PrivateDial 4 | 5 | - Make IVR custom-able 6 | 7 | ## MiniVM 8 | 9 | The application [MinivmGreet()](https://wiki.asterisk.org/wiki/display/AST/Asterisk+16+Application_MinivmGreet) is broken. Now it only plays the "temp" message. 10 | Consider fixing the code in [app_minivm.c](https://github.com/asterisk/asterisk/blob/8f5534a68a01ad3fbe6b1920c8ab160fc3b4df89/apps/app_minivm.c) lines 2326-2345 and file a patch. 11 | 12 | ## Upgrade utility 13 | 14 | - Shell utility that helps upgrading config files. 15 | 16 | ## Music on hold 17 | 18 | - Script that converts sound files to wav 8000 Hz mono. 19 | 20 | ## AutoBan 21 | 22 | - Now `.nft` file is loaded at container startup (entry.d) irrespective of whether autoband is enabled or not. Only load `.nft` files if autoband is enabled. 23 | - Add option to get reverse DNS using gethostbyaddr($ip); in `show who`. 24 | - Perhaps replace entry.d/ with /etc/conf.d/nftables? 25 | - Allow intervals, eg 192.168.1.1-192.168.1.200, in blacklist and whitelist. 26 | 27 | ## WebSMS 28 | 29 | ## Asterisk modules 30 | -------------------------------------------------------------------------------- /bld.mk: -------------------------------------------------------------------------------- 1 | # bld.mk 2 | # 3 | # Docker build make-functions 4 | # 5 | 6 | BLD_VER ?= latest 7 | BLD_TGT ?= full 8 | BLD_CMT ?= HEAD 9 | 10 | # 11 | # $(call bld_tags,mini,) -> mini mini-1.2.3 mini-1.2 mini-1 12 | # $(call bld_tags,full,) -> latest full 1.2.3 1.2 1 full-1.2.3 full-1.2 full-1 13 | # $(call bld_tags,,) -> latest 1.2.3 1.2 1 14 | # 15 | # $(call bld_tags,mini,something) -> mini-something 16 | # $(call bld_tags,full,something) -> something full-something 17 | # $(call bld_tags,,something) -> something 18 | # 19 | # $(call bld_tags,mini,latest) -> mini 20 | # $(call bld_tags,full,latest) -> latest full 21 | # $(call bld_tags,,latest) -> latest 22 | # 23 | bld_tags = $(if $(2),\ 24 | $(call bld_ver,$(1),$(2)),\ 25 | $(call bld_ver,$(1),latest) $(call bld_ver,$(1),$(call bld_gittags))) 26 | 27 | # 28 | # $(call bld_ver,mini,something) -> mini-something 29 | # $(call bld_ver,full,something) -> something full-something 30 | # $(call bld_ver,,something) -> something 31 | # 32 | # $(call bld_ver,mini,latest) -> mini 33 | # $(call bld_ver,full,latest) -> latest full 34 | # $(call bld_ver,,latest) -> latest 35 | # 36 | bld_ver = $(if $(1),\ 37 | $(if $(findstring $(BLD_TGT),$(1)),\ 38 | $(if $(findstring latest,$(2)),latest $(1),$(2) $(addprefix $(1)-,$(2))),\ 39 | $(if $(findstring latest,$(2)),$(1),$(addprefix $(1)-,$(2)))),\ 40 | $(2)) 41 | 42 | # 43 | # $(call bld_tag,full,) -> full 44 | # $(call bld_tag,,) -> latest 45 | # 46 | # $(call bld_tag,full,something) -> full-something 47 | # $(call bld_tag,,something) -> something 48 | # 49 | # $(call bld_tag,full,latest) -> full 50 | # $(call bld_tag,,latest) -> latest 51 | # 52 | bld_tag = $(strip $(if $(1),\ 53 | $(if $(2),$(if $(findstring latest,$(2)),$(1),$(1)-$(2)),$(1)),\ 54 | $(if $(2),$(2),latest))) 55 | 56 | # 57 | # $(call bld_gittags,HEAD) -> 1.2.3 1.2 1 58 | # 59 | bld_gittags = $(subst v,,$(shell git tag --points-at $(BLD_CMT))) 60 | -------------------------------------------------------------------------------- /demo/.env: -------------------------------------------------------------------------------- 1 | COMPOSE_PROJECT_NAME=demo 2 | SYSLOG_LEVEL=8 3 | DOMAIN=example.com 4 | TELE_SRV=tele 5 | SMS_PORT=8080 6 | WEBSMSD_PORT=80 7 | -------------------------------------------------------------------------------- /demo/.gitignore: -------------------------------------------------------------------------------- 1 | pulse/ 2 | -------------------------------------------------------------------------------- /demo/Makefile: -------------------------------------------------------------------------------- 1 | -include *.mk .env .init.env 2 | 3 | SRV_NAME ?= tele 4 | 5 | CNT_CLI ?= asterisk -rvvvddd 6 | 7 | SIPS_URL ?= $(call dkr_srv_ip,$(SRV_NAME)):5061 8 | 9 | SMS_HOST ?= 127.0.0.1:8080 10 | SMS_URL1 ?= /hook/1 11 | SMS_URL2 ?= /hook/2 12 | SMS_FROM ?= +15017122661 13 | SMS_TO ?= +12025550160 14 | SMS_BODY ?= This is a test message, sent $$(date), with special chars: ☺ [$$\"\\];; and 4 additional lines:%0A1%0A2%0A3%0A4. 15 | 16 | 17 | variables: 18 | make -pn | grep -A1 "^# makefile"| grep -v "^#\|^--" | sort | uniq 19 | 20 | init: up 21 | 22 | ps: 23 | docker compose ps 24 | 25 | up: 26 | docker compose up -d 27 | 28 | start: 29 | docker compose start 30 | 31 | stop: 32 | docker compose stop 33 | 34 | down: 35 | docker compose down 36 | 37 | destroy: 38 | docker compose down -v 39 | 40 | config: 41 | docker compose config 42 | 43 | wait_%: 44 | sleep 10 45 | 46 | pw: 47 | dd if=/dev/random count=1 bs=8 2>/dev/null | base64 | sed -e 's/=*$$//' 48 | #od -vAn -N4 -tu4 < /dev/urandom 49 | 50 | sms0: 51 | curl -i $(SMS_HOST)$(SMS_URL1) -G \ 52 | --data-urlencode "zd_echo=$(shell date)" 53 | @echo 54 | 55 | sms1: 56 | curl -i $(SMS_HOST)$(SMS_URL1) -X POST \ 57 | --data-urlencode "To=$(SMS_TO)" \ 58 | --data-urlencode "From=$(SMS_FROM)" \ 59 | --data "Body=$(SMS_BODY)" 60 | 61 | sms2: 62 | curl -i $(SMS_HOST)$(SMS_URL2) -X POST \ 63 | --data-urlencode "To=$(SMS_TO)" \ 64 | --data-urlencode "From=$(SMS_FROM)" \ 65 | --data "Body=$(SMS_BODY)" 66 | 67 | logs: 68 | docker compose logs $(SRV_NAME) 69 | 70 | sh: 71 | docker compose exec $(SRV_NAME) bash 72 | 73 | cli: 74 | docker compose exec $(SRV_NAME) $(CNT_CLI) 75 | 76 | diff: 77 | docker container diff $(call dkr_srv_cnt,$(SRV_NAME)) 78 | 79 | top: 80 | docker compose top $(SRV_NAME) 81 | 82 | env: 83 | docker compose exec $(SRV_NAME) env 84 | 85 | sv: 86 | docker compose exec $(SRV_NAME) sh -c 'sv status $$SVDIR/*' 87 | 88 | nft: 89 | docker compose exec $(SRV_NAME) nft list ruleset 90 | 91 | autoban: 92 | docker compose exec $(SRV_NAME) autoban show 93 | 94 | htop: tools_install 95 | docker compose exec $(SRV_NAME) htop 96 | 97 | ipcs: 98 | docker compose exec $(SRV_NAME) ipcs 99 | 100 | logger: 101 | docker compose exec $(SRV_NAME) asterisk -rx \ 102 | 'logger add channel console notice,warning,error,verbose' 103 | 104 | restart_%: 105 | docker compose exec $(SRV_NAME) sv restart $* 106 | 107 | apk_list: 108 | docker compose exec $(SRV_NAME) /bin/sh -c 'apk info -sq $$(apk info -q) | sed -r "N;N;s/([^ ]+) installed size:\n([^ ]+) (.).*/\2\3\t\1/" | sort -h' 109 | 110 | apk_add_%: 111 | docker compose exec $(SRV_NAME) apk -q --no-cache --update add $* 112 | 113 | tools_install: 114 | docker compose exec $(SRV_NAME) apk --no-cache --update add \ 115 | nano lsof htop openldap-clients bind-tools iputils strace util-linux \ 116 | pulseaudio-utils alsa-utils 117 | 118 | xdebug_install: 119 | $(eval PHP_VER=$(shell docker compose exec $(SRV_NAME) sh -c 'echo $$PHP_VER')) 120 | docker compose exec $(SRV_NAME) apk --no-cache --update add \ 121 | $(PHP_VER)-pecl-xdebug 122 | docker compose exec $(SRV_NAME) sed -i '1 a xdebug.profiler_enable = 1' /etc/$(PHP_VER)/php.ini 123 | docker compose exec $(SRV_NAME) sed -i '2 a zend_extension=xdebug.so' /etc/$(PHP_VER)/conf.d/50_xdebug.ini 124 | docker compose exec $(SRV_NAME) sv restart websmsd autoband 125 | 126 | xdebug_getdata: 127 | docker cp $(call dkr_srv_cnt,$(SRV_NAME)):tmp/. ../local/xdebug 128 | 129 | sips_test: 130 | docker run --rm -it --network bridge drwetter/testssl.sh $(SIPS_URL) || true 131 | 132 | sound_enable: pulse/socket 133 | cp -f $${PULSE_COOKIE-$$HOME/.config/pulse/cookie} pulse/cookie 134 | sudo mount --bind $$(pactl info | sed '1!d;s/.*:\s*//g') pulse/socket 135 | docker compose exec $(SRV_NAME) asterisk -rx 'module load chan_console.so' || true 136 | 137 | sound_disable: 138 | rm pulse/cookie 139 | sudo umount pulse/socket 140 | 141 | pulse/socket: 142 | mkdir -p pulse 143 | touch pulse/socket 144 | 145 | sound_1: 146 | docker compose exec $(SRV_NAME) play -nq -t alsa synth 1 sine 300 147 | 148 | sound_2: 149 | docker compose exec $(SRV_NAME) play -q /var/lib/asterisk/sounds/en/hello-world.gsm 150 | 151 | sound_3: apk_add_alsa-utils 152 | docker compose exec $(SRV_NAME) aplay -Dpulse /usr/share/sounds/alsa/Front_Right.wav 153 | 154 | sound_4: apk_add_pulseaudio-utils 155 | docker compose exec $(SRV_NAME) paplay /usr/share/sounds/alsa/Front_Center.wav 156 | 157 | sound_5: 158 | docker compose exec $(SRV_NAME) asterisk -rx \ 159 | 'channel originate console/+12025550160@dp_entry_call_inout application playback hello-world' 160 | 161 | sound_6: 162 | docker compose exec $(SRV_NAME) asterisk -rx 'console dial t@dp_ivr_recgreet' 163 | 164 | -------------------------------------------------------------------------------- /demo/dkr.mk: -------------------------------------------------------------------------------- 1 | ../dkr.mk -------------------------------------------------------------------------------- /demo/docker-compose.yml: -------------------------------------------------------------------------------- 1 | name: demo 2 | 3 | services: 4 | tele: 5 | image: mlan/asterisk 6 | network_mode: bridge # Only here to help testing 7 | cap_add: 8 | - sys_ptrace # Only here to help testing 9 | - net_admin # Allow NFT, used by AutoBan 10 | - net_raw # Allow NFT, used by AutoBan 11 | ports: 12 | - "${SMS_PORT-8080}:${WEBSMSD_PORT:-80}" # WEBSMSD port mapping 13 | - "5060:5060/udp" # SIP UDP port 14 | - "5060:5060" # SIP TCP port 15 | - "5061:5061" # SIP TLS port 16 | - "10000-10099:10000-10099/udp" # RTP ports 17 | environment: 18 | - SYSLOG_LEVEL=${SYSLOG_LEVEL-4} # Logging 19 | - HOSTNAME=${TELE_SRV-tele}.${DOMAIN-docker.localhost} 20 | - PULSE_SERVER=unix:/run/pulse/socket # Use host audio 21 | - PULSE_COOKIE=/run/pulse/cookie # Use host audio 22 | - WEBSMSD_PORT=${WEBSMSD_PORT-80} # WEBSMSD internal port 23 | volumes: 24 | - tele-conf:/srv # Persistent storage 25 | - ./pulse:/run/pulse:rshared # Use host audio 26 | - /etc/localtime:/etc/localtime:ro # Use host timezone 27 | 28 | volumes: 29 | tele-conf: # Persistent storage 30 | -------------------------------------------------------------------------------- /dkr.mk: -------------------------------------------------------------------------------- 1 | # dkr.mk 2 | # 3 | # Container make-functions 4 | # 5 | 6 | # 7 | # $(call dkr_srv_cnt,app) -> d03dda046e0b90c... 8 | # 9 | dkr_srv_cnt = $(shell docker compose ps -q $(1) | head -n1) 10 | # 11 | # $(call dkr_cnt_ip,demo-app-1) -> 172.28.0.3 12 | # 13 | dkr_cnt_ip = $(shell docker inspect -f \ 14 | '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' \ 15 | $(1) | head -n1) 16 | # 17 | # $(call dkr_srv_ip,app) -> 172.28.0.3 18 | # 19 | dkr_srv_ip = $(shell docker inspect -f \ 20 | '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' \ 21 | $$(docker compose ps -q $(1)) | head -n1) 22 | # 23 | # $(call dkr_cnt_pid,demo-app-1) -> 9755 24 | # 25 | dkr_cnt_pid = $(shell docker inspect --format '{{.State.Pid}}' $(1)) 26 | # 27 | #cnt_ip_old = $(shell docker inspect -f \ 28 | # '{{range .NetworkSettings.Networks}}{{println .IPAddress}}{{end}}' \ 29 | # $(1) | head -n1) 30 | 31 | # 32 | # $(call dkr_img_env,image,envvar) -> value 33 | # 34 | dkr_img_env = $(shell docker inspect -f \ 35 | '{{range .Config.Env}}{{println .}}{{end}}' $(1) | grep -P "^$(2)=" | sed 's/[^=]*=//' 36 | 37 | # 38 | # $(call dkr_cnt_state,demo-app-1) -> docker inspect -f '{{.State.Status}}' demo-app-1 39 | # 40 | dkr_cnt_state = docker inspect -f '{{.State.Status}}' $(1) 41 | 42 | # 43 | # $(call dkr_cnt_wait_run,test-db,180) -> i=0; time while ! [ "$(docker inspect -f '{{.State.Status}}' test-db)" = "running" ]; do sleep 1; i=$((i+1)); if [[ $i > 180 ]]; then echo test-db timeout with state: $(docker inspect -f '{{.State.Status}}' test-db); break; fi; done 44 | # 45 | dkr_cnt_wait_run = i=0; time while ! [ "$$($(call dkr_cnt_state, $(1)))" = "running" ]; do sleep 1; i=$$((i+1)); if [[ $$i > $(2) ]]; then echo $(1) timeout with state: $$($(call dkr_cnt_state, $(1))); break; fi; done 46 | 47 | # 48 | # $(call dkr_srv_wait_run,180,app) -> wait up to 180s for app to enter state running 49 | # 50 | dkr_srv_wait_run = $(call dkr_cnt_wait_run,$(call dkr_srv_cnt $(1)),$(2)) 51 | 52 | # 53 | # $(call dkr_cnt_wait_log,app,ready for connections) -> time docker logs -f app | sed -n '/ready for connections/{p;q}' 54 | # 55 | dkr_cnt_wait_log = time docker logs -f $(1) 2>&1 | sed -n '/$(2)/{p;q}' 56 | 57 | # 58 | # $(call dkr_pull_missing,mariadb:latest) -> if ! docker image inspect mariadb:latest &>/dev/null; then docker pull mariadb:latest; fi 59 | # 60 | dkr_pull_missing = if ! docker image inspect $(1) &>/dev/null; then docker pull $(1); fi 61 | 62 | # 63 | # List IPs of containers 64 | # 65 | ip-list: 66 | @for srv in $$(docker ps --format "{{.Names}}"); do \ 67 | echo $$srv $$(docker inspect -f \ 68 | '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' $$srv); \ 69 | done | column -t 70 | -------------------------------------------------------------------------------- /src/acme/bin/acme-extract.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | # Copyright (c) 2017 Brian 'redbeard' Harrington 3 | # 4 | # acme-export.sh - A simple utility to explode a Traefik acme.json file into a 5 | # directory of certificates and a private key 6 | # 7 | # Usage - acme-export.sh /etc/traefik/acme.json /etc/ssl/ 8 | # 9 | # Dependencies - 10 | # util-linux 11 | # openssl 12 | # jq 13 | # The MIT License (MIT) 14 | # 15 | # Permission is hereby granted, free of charge, to any person obtaining a copy 16 | # of this software and associated documentation files (the "Software"), to deal 17 | # in the Software without restriction, including without limitation the rights 18 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 19 | # copies of the Software, and to permit persons to whom the Software is 20 | # furnished to do so, subject to the following conditions: 21 | # 22 | # The above copyright notice and this permission notice shall be included in 23 | # all copies or substantial portions of the Software. 24 | # 25 | 26 | # Exit codes: 27 | # 1 - A component is missing or could not be read 28 | # 2 - There was a problem reading acme.json 29 | # 4 - The destination certificate directory does not exist 30 | # 8 - Missing private key 31 | 32 | #set -o errexit 33 | ##set -o pipefail 34 | #set -o nounset 35 | #set -o verbose 36 | 37 | 38 | # 39 | # configuration 40 | # 41 | . docker-common.sh 42 | 43 | DOCKER_ACME_SSL_DIR=${DOCKER_ACME_SSL_DIR-/etc/ssl/acme} 44 | ACME_FILE=${ACME_FILE-/acme/acme.json} 45 | 46 | CMD_DECODE_BASE64="base64 -d" 47 | 48 | # 49 | # functions 50 | # 51 | usage() { echo "$(basename $0) " ;} 52 | 53 | test_args() { 54 | # when called by inotifyd the first argument is the single character 55 | # event descriptor, lets drop it 56 | dc_log 7 "Called with args $@" 57 | [ $# -ge 0 ] && [ ${#1} -eq 1 ] && shift 58 | readonly acmefile="${1-$ACME_FILE}" 59 | readonly certdir="${2-$DOCKER_ACME_SSL_DIR}" 60 | } 61 | 62 | test_dependencies() { 63 | # Allow us to exit on a missing jq binary 64 | if dc_is_installed jq; then 65 | dc_log 7 "The package jq is installed." 66 | else 67 | dc_log 4 "You must have the binary jq to use this." 68 | exit 1 69 | fi 70 | } 71 | 72 | test_acmefile() { 73 | if [ ! -r "${acmefile}" ]; then 74 | dc_log 4 "There was a problem reading from (${acmefile}). We need to read this file to explode the JSON bundle... exiting." 75 | exit 2 76 | fi 77 | } 78 | 79 | test_certdir() { 80 | if [ ! -d "${certdir}" ]; then 81 | dc_log 4 "Path ${certdir} does not seem to be a directory. We need a directory in which to explode the JSON bundle... exiting." 82 | exit 4 83 | fi 84 | } 85 | 86 | make_certdirs() { 87 | # If they do not exist, create the needed subdirectories for our assets 88 | # and place each in a variable for later use, normalizing the path 89 | mkdir -p "${certdir}/certs" "${certdir}/private" 90 | pdir="${certdir}/private" 91 | cdir="${certdir}/certs" 92 | } 93 | 94 | bad_acme() { 95 | dc_log 4 "There was a problem parsing your acme.json file." 96 | exit 2 97 | } 98 | 99 | read_letsencryptkey() { 100 | # look for key assuming acme v2 format 101 | priv=$(jq -e -r '.[].Account.PrivateKey' "${acmefile}" 2>/dev/null) 102 | if [ $? -eq 0 ]; then 103 | acmeversion=2 104 | dc_log 7 "Using acme v2 format, the PrivateKey was found in ${acmefile}" 105 | else 106 | # look for key assuming acme v1 format 107 | priv=$(jq -e -r '.Account.PrivateKey' "${acmefile}" 2>/dev/null) 108 | if [ $? -eq 0 ]; then 109 | acmeversion=1 110 | dc_log 7 "Using acme v1 format, the PrivateKey was found in ${acmefile}" 111 | else 112 | dc_log 4 "There didn't seem to be a private key in ${acmefile}. Please ensure that there is a key in this file and try again." 113 | exit 2 114 | fi 115 | fi 116 | } 117 | 118 | save_letsencryptkey() { 119 | local keyfile=${pdir}/letsencrypt.key 120 | printf -- \ 121 | "-----BEGIN RSA PRIVATE KEY-----\n%s\n-----END RSA PRIVATE KEY-----\n" \ 122 | ${priv} | fold -w 65 | \ 123 | openssl rsa -inform pem -out $keyfile 2>/dev/null 124 | if [ -e $keyfile ]; then 125 | dc_log 7 "PrivateKey is valid and saved in $keyfile" 126 | else 127 | dc_log 4 "PrivateKey appers NOT to be valid" 128 | exit 2 129 | fi 130 | } 131 | 132 | read_domains() { 133 | # Process the certificates for each of the domains in acme.json 134 | case $acmeversion in 135 | 1) jq_filter='.Certificates[].Domain.Main' ;; 136 | 2) jq_filter='.[].Certificates[].domain.main' ;; 137 | esac 138 | domains=$(jq -r $jq_filter $acmefile) 139 | if [ -n "$domains" ]; then 140 | dc_log 7 "Extracting private key and cert bundle for domains $domains." 141 | else 142 | dc_log 4 "Unable to find any domains in $acmefile." 143 | exit 2 144 | fi 145 | } 146 | 147 | # 148 | # Traefik stores a cert bundle for each domain. Within this cert 149 | # bundle there is both proper the certificate and the Let's Encrypt CA 150 | # 151 | save_certs() { 152 | dc_log 5 "Extracting private keys and cert bundles in ${acmefile}" 153 | case $acmeversion in 154 | 1) 155 | jq_crtfilter='.Certificates[] | select (.Domain.Main == $domain )| .Certificate' 156 | jq_keyfilter='.Certificates[] | select (.Domain.Main == $domain )| .Key' 157 | ;; 158 | 2) 159 | jq_crtfilter='.[].Certificates[] | select (.domain.main == $domain )| .certificate' 160 | jq_keyfilter='.[].Certificates[] | select (.domain.main == $domain )| .key' 161 | ;; 162 | esac 163 | for domain in $domains; do 164 | crt=$(jq -e -r --arg domain "$domain" "$jq_crtfilter" $acmefile) || bad_acme 165 | echo "${crt}" | ${CMD_DECODE_BASE64} > "${cdir}/${domain}.crt" 166 | key=$(jq -e -r --arg domain "$domain" "$jq_keyfilter" $acmefile) || bad_acme 167 | echo "${key}" | ${CMD_DECODE_BASE64} > "${pdir}/${domain}.key" 168 | done 169 | } 170 | 171 | # 172 | # Run command in ACME_POSTHOOK if it contain a valid command and runsvdir is running. 173 | # 174 | run_posthook() { 175 | if (pidof runsvdir >/dev/null && [ -n "$ACME_POSTHOOK" ] && command -v $ACME_POSTHOOK >/dev/null); then 176 | local out=$(eval "$ACME_POSTHOOK" 2>&1) 177 | [ -n "$out" ] && dc_log 7 "$ACME_POSTHOOK : $out" 178 | fi 179 | } 180 | 181 | # 182 | # run 183 | # 184 | test_args $@ 185 | test_dependencies 186 | test_acmefile 187 | test_certdir 188 | read_letsencryptkey 189 | make_certdirs 190 | save_letsencryptkey 191 | read_domains 192 | save_certs 193 | run_posthook 194 | -------------------------------------------------------------------------------- /src/acme/entry.d/10-acme-common: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # 10-acme-common 4 | # 5 | # Define variables and functions used during container initialization. 6 | # 7 | # Variables defined in Dockerfile 8 | # DOCKER_ACME_SSL_DIR DOCKER_APPL_SSL_DIR 9 | # 10 | ACME_FILE=${ACME_FILE-/acme/acme.json} 11 | HOSTNAME=${HOSTNAME-$(hostname)} 12 | DOMAIN=${HOSTNAME#*.} 13 | DOCKER_APPL_SSL_CERT=${DOCKER_APPL_SSL_CERT-$DOCKER_APPL_SSL_DIR/cert.pem} 14 | DOCKER_APPL_SSL_KEY=${DOCKER_APPL_SSL_KEY-$DOCKER_APPL_SSL_DIR/priv_key.pem} 15 | DOCKER_ACME_SSL_H_CERT=$DOCKER_ACME_SSL_DIR/certs/${HOSTNAME}.crt 16 | DOCKER_ACME_SSL_H_KEY=$DOCKER_ACME_SSL_DIR/private/${HOSTNAME}.key 17 | DOCKER_ACME_SSL_D_CERT=$DOCKER_ACME_SSL_DIR/certs/${DOMAIN}.crt 18 | DOCKER_ACME_SSL_D_KEY=$DOCKER_ACME_SSL_DIR/private/${DOMAIN}.key 19 | 20 | # 21 | # Setup monitoring of ACME_FILE 22 | # 23 | acme_monitor_tls_cert() { 24 | if (dc_is_installed jq && (dc_is_command inotifyd || dc_is_command inotifywait)); then 25 | if [ -s $ACME_FILE ]; then 26 | # run acme-extract.sh on cnt creation (and every time the json file changes) 27 | dc_log 5 "Setup ACME TLS certificate monitoring" 28 | if dc_is_command inotifyd; then 29 | docker-service.sh "-n acme $(which inotifyd) $(which acme-extract.sh) $ACME_FILE:c" 30 | else 31 | docker-service.sh "-n acme sh -c \"while $(which inotifywait) -e close_write $ACME_FILE; do $(which acme-extract.sh); done\"" 32 | fi 33 | # acme-extract.sh reports to logger but it is yet to be started so this run will be quiet 34 | acme-extract.sh $ACME_FILE $DOCKER_ACME_SSL_DIR 35 | fi 36 | else 37 | dc_log 5 "Not all required pkgs installed so cannot setup ACME TLS certificate monitoring" 38 | fi 39 | } 40 | 41 | # 42 | # Arrange sym-links to support both host and domain certificates. 43 | # 44 | acme_symlink_tls_cert() { 45 | if ([ -r $DOCKER_ACME_SSL_H_CERT ] && [ -r $DOCKER_ACME_SSL_H_KEY ]); then 46 | dc_log 5 "Setting up ACME TLS certificate for host $HOSTNAME" 47 | mkdir -p $DOCKER_APPL_SSL_DIR 48 | ln -sf $DOCKER_ACME_SSL_H_CERT $DOCKER_APPL_SSL_CERT 49 | ln -sf $DOCKER_ACME_SSL_H_KEY $DOCKER_APPL_SSL_KEY 50 | else 51 | if ([ -r $DOCKER_ACME_SSL_D_CERT ] && [ -r $DOCKER_ACME_SSL_D_KEY ]); then 52 | dc_log 5 "Setting up ACME TLS certificate for domain $DOMAIN" 53 | mkdir -p $DOCKER_APPL_SSL_DIR 54 | ln -sf $DOCKER_ACME_SSL_D_CERT $DOCKER_APPL_SSL_CERT 55 | ln -sf $DOCKER_ACME_SSL_D_KEY $DOCKER_APPL_SSL_KEY 56 | fi 57 | fi 58 | } 59 | -------------------------------------------------------------------------------- /src/acme/entry.d/50-acme-monitor-tlscert: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # 50-acme-monitor-tlscert 4 | # 5 | # Functions defined in: 6 | # 10-acme-common 7 | # 8 | 9 | # 10 | # Setup ACME monitor and arrange certificate symbolic links 11 | # 12 | acme_monitor_tls_cert 13 | acme_symlink_tls_cert 14 | -------------------------------------------------------------------------------- /src/asterisk/config/alsa.conf: -------------------------------------------------------------------------------- 1 | [general] 2 | ;noaudiocapture = true 3 | -------------------------------------------------------------------------------- /src/asterisk/config/asterisk.conf: -------------------------------------------------------------------------------- 1 | [options] 2 | ; If we want to start Asterisk with a default verbosity for the verbose 3 | ; or debug logger channel types, then we use these settings (by default 4 | ; they are disabled). 5 | ;verbose = 3 6 | ;debug = 3 7 | 8 | ; User and group to run asterisk as. NOTE: This will require changes to 9 | ; directory and device permissions. 10 | ;runuser = asterisk ; The user to run as. The default is root. 11 | ;rungroup = asterisk ; The group to run as. The default is root 12 | 13 | ;defaultlanguage = us 14 | 15 | ; Transmit silence while a channel is in a waiting state, a recording only 16 | ; state, or when DTMF is being generated. Note that the silence internally is 17 | ; generated in raw signed linear format. This means that it must be transcoded 18 | ; into the native format of the channel before it can be sent to the device. 19 | ; It is for this reason that this is optional, as it may result in requiring a 20 | ; temporary codec translation path for a channel that may not otherwise require 21 | ; one. 22 | ;transmit_silence=yes 23 | -------------------------------------------------------------------------------- /src/asterisk/config/ccss.conf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlan/docker-asterisk/56bd8303d40efaf679cee632b33c4faacbabd146/src/asterisk/config/ccss.conf -------------------------------------------------------------------------------- /src/asterisk/config/cli_aliases.conf: -------------------------------------------------------------------------------- 1 | ; 2 | ; CLI Aliases configuration 3 | ; 4 | ; This module also registers a "cli show aliases" CLI command to list 5 | ; configured CLI aliases. 6 | 7 | [general] 8 | template = friendly ; By default, include friendly aliases 9 | 10 | [friendly] 11 | hangup request=channel request hangup 12 | originate=channel originate 13 | help=core show help 14 | pri intense debug span=pri set debug intense span 15 | reload=module reload 16 | pjsip reload=module reload res_pjsip.so res_pjsip_authenticator_digest.so res_pjsip_endpoint_identifier_ip.so res_pjsip_mwi.so res_pjsip_notify.so res_pjsip_outbound_publish.so res_pjsip_publish_asterisk.so res_pjsip_outbound_registration.so 17 | -------------------------------------------------------------------------------- /src/asterisk/config/console.conf: -------------------------------------------------------------------------------- 1 | [default] 2 | input_device = pulse ; When configuring an input device and output device, 3 | output_device = pulse ; use the name that you see when you run the "console list available" CLI command. 4 | active = yes ; This option should only be set for one console. -------------------------------------------------------------------------------- /src/asterisk/config/features.conf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlan/docker-asterisk/56bd8303d40efaf679cee632b33c4faacbabd146/src/asterisk/config/features.conf -------------------------------------------------------------------------------- /src/asterisk/config/logger.conf: -------------------------------------------------------------------------------- 1 | [general] 2 | 3 | [logfiles] 4 | syslog.local0 = verbose,notice,warning,error 5 | ;console = verbose,notice,warning,error 6 | ;messages = notice,warning,error 7 | ;full = verbose,notice,warning,error,debug 8 | ;security = security 9 | -------------------------------------------------------------------------------- /src/asterisk/config/modules.conf: -------------------------------------------------------------------------------- 1 | ; 2 | ; Asterisk configuration file 3 | ; 4 | ; Module Loader configuration file 5 | ; 6 | 7 | [modules] 8 | autoload=yes 9 | ; 10 | ; 11 | ; If you want, load the GTK console right away. 12 | ; 13 | noload => pbx_gtkconsole.so 14 | ;load => pbx_gtkconsole.so 15 | ; 16 | load => res_musiconhold.so 17 | ; 18 | noload => app_adsiprog.so 19 | noload => app_agent_pool.so 20 | noload => app_alarmreceiver.so 21 | noload => app_amd.so 22 | noload => app_confbridge.so 23 | noload => app_festival.so 24 | noload => app_followme.so 25 | noload => app_getcpeid.so 26 | noload => app_page.so 27 | noload => app_queue.so 28 | noload => app_voicemail.so 29 | noload => app_voicemail_imap.so 30 | noload => cdr_csv.so 31 | noload => cdr_custom.so 32 | noload => cdr_manager.so 33 | noload => cdr_sqlite3_custom.so 34 | noload => cdr_syslog.so 35 | noload => cel_custom.so 36 | noload => cel_manager.so 37 | noload => cel_sqlite3_custom.so 38 | noload => chan_alsa.so 39 | noload => chan_console.so 40 | noload => chan_iax2.so 41 | noload => chan_mgcp.so 42 | noload => chan_sip.so 43 | noload => chan_skinny.so 44 | noload => chan_unistim.so 45 | noload => pbx_ael.so 46 | noload => pbx_dundi.so 47 | noload => pbx_lua.so 48 | noload => res_adsi.so 49 | noload => res_ari_applications.so 50 | noload => res_ari_asterisk.so 51 | noload => res_ari_bridges.so 52 | noload => res_ari_channels.so 53 | noload => res_ari_device_states.so 54 | noload => res_ari_endpoints.so 55 | noload => res_ari_events.so 56 | noload => res_ari_playbacks.so 57 | noload => res_ari_recordings.so 58 | noload => res_ari.so 59 | noload => res_ari_sounds.so 60 | noload => res_calendar.so 61 | noload => res_fax.so 62 | noload => res_hep.so 63 | noload => res_hep_pjsip.so 64 | noload => res_hep_rtcp.so 65 | noload => res_parking.so 66 | noload => res_phoneprov.so 67 | noload => res_pjsip_notify.so 68 | noload => res_pjsip_phoneprov_provider.so 69 | noload => res_pjsip_t38.so 70 | noload => res_prometheus.so 71 | noload => res_smdi.so 72 | noload => res_statsd.so 73 | noload => res_stun_monitor.so -------------------------------------------------------------------------------- /src/asterisk/config/musiconhold.conf: -------------------------------------------------------------------------------- 1 | [general] 2 | 3 | [default] 4 | mode=files 5 | directory=moh 6 | sort=random 7 | -------------------------------------------------------------------------------- /src/asterisk/config/pjproject.conf: -------------------------------------------------------------------------------- 1 | [startup] 2 | type= 3 | log_level=default 4 | -------------------------------------------------------------------------------- /src/asterisk/config/rtp.conf: -------------------------------------------------------------------------------- 1 | [general] 2 | ; RTP start and RTP end configure start and end addresses 3 | ; docker will stall if we open a large range of ports, since it runs a 4 | ; a proxy process of each exposed port 5 | rtpstart = 10000 6 | rtpend = 10099 7 | ; 8 | ; Strict RTP protection will drop packets that have passed NAT. Disable to allow 9 | ; remote endpoints connected to LANs. 10 | ; 11 | strictrtp = no 12 | -------------------------------------------------------------------------------- /src/asterisk/entry.d/50-asterisk-seed-conf: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # 50-asterisk-seed-conf 4 | # 5 | # Defined in Dockerfile: 6 | # DOCKER_CONF_DIR DOCKER_SEED_CONF_DIR 7 | # 8 | 9 | # 10 | # If DOCKER_CONF_DIR is empty, initialize it with files from 11 | # DOCKER_SEED_CONF_DIR. We don't want to overwrite any files, 12 | # but we only have "cp -u" (only copy newer) in busybox. 13 | # The directory should be empty when we try to copy, 14 | # so this is just an extra precaution. 15 | # 16 | if [ -z "$(ls -A $DOCKER_CONF_DIR 2>/dev/null)" ]; then 17 | dc_log 5 "Seeding asterisk configuration." 18 | cp -p -u $DOCKER_SEED_CONF_DIR/* $DOCKER_CONF_DIR 19 | fi 20 | -------------------------------------------------------------------------------- /src/asterisk/entry.d/51-asterisk-generate-tlscert: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # 51-asterisk-generate-tlscert 4 | # 5 | # Variables defined in 10-acme-common 6 | # DOCKER_APPL_SSL_CERT DOCKER_APPL_SSL_KEY 7 | # 8 | 9 | # 10 | # Generate self signed certificate if but no certificates 11 | # are given. 12 | # 13 | if ([ ! -s $DOCKER_APPL_SSL_CERT ] || [ ! -s $DOCKER_APPL_SSL_KEY ]) && \ 14 | dc_is_installed openssl; then 15 | dc_log 5 "No TLS certificates found, so generating self-signed cert for host $HOSTNAME" 16 | dc_tls_setup_selfsigned_cert $DOCKER_APPL_SSL_CERT $DOCKER_APPL_SSL_KEY 17 | fi 18 | 19 | -------------------------------------------------------------------------------- /src/autoban/README.md: -------------------------------------------------------------------------------- 1 | doc/autoban.md -------------------------------------------------------------------------------- /src/autoban/config/autoban.conf: -------------------------------------------------------------------------------- 1 | ; 2 | ; autoban.conf 3 | ; 4 | ; This file hold user customization of the AutoBan intrusion detection and 5 | ; prevention system. This is a php ini file. Luckily its syntax is similar to 6 | ; other asterisk conf files. "yes" and "no" have to be within quotation marks 7 | ; otherwise they will be interpreted as Boolean. 8 | ; 9 | 10 | [asmanager] 11 | server = 127.0.0.1 12 | port = 5038 13 | username = autoban 14 | secret = 6003.438 15 | 16 | [autoban] 17 | maxcount = 10 18 | watchtime = 20m 19 | jailtime = 20m 20 | repeatmult = 6 21 | -------------------------------------------------------------------------------- /src/autoban/config/manager.conf: -------------------------------------------------------------------------------- 1 | [general] 2 | enabled = yes 3 | bindaddr = 127.0.0.1 4 | port = 5038 5 | 6 | [autoban] 7 | secret = 6003.438 8 | read = security 9 | write = 10 | -------------------------------------------------------------------------------- /src/autoban/doc/autoban.conf.sample: -------------------------------------------------------------------------------- 1 | ; 2 | ; autoban.conf.sample 3 | ; 4 | ; This file hold user customization of the AutoBan intrusion detection and 5 | ; prevention system. This is a php ini file. Luckily its syntax is similar to 6 | ; other asterisk conf files. "yes" and "no" have to be within quotation marks 7 | ; otherwise they will be interpreted as Boolean. The default values of the 8 | ; variables are given below. You only have to define variables with non default 9 | ; values. 10 | ; 11 | ; 12 | ;[asmanager] 13 | ;server = localhost ; here asterisk runs in same container so ami server is localhost 14 | ;port = 5038 ; should be equal to port used in manager.conf 15 | ;username = phpagi ; should be equal to username used in manager.conf, ie autoban 16 | ;secret = phpagi ; should be equal to secret used in manager.conf 17 | ; 18 | ;[autoban] 19 | ;enabled = true ; autoban is activated when autoban.conf exists and not explicitly disabled here 20 | ;maxcount = 10 ; abuse count at which IP will be jailed, ie its packets dropped 21 | ;watchtime = 20m ; time to keep IP under watch in seconds or time string eg 1d2h3m4s 22 | ;jailtime = 20m ; time to drop packets from IP in seconds or time string eg 1d2h3m4s 23 | ;repeatmult = 6 ; repeat offenders get jailtime multiplied by this factor 24 | ; 25 | ;[nftables] ; you normally don't need to mess with these variables 26 | ;cmd = nft ; 27 | ;family = ip ; 28 | ;table = autoban ; 29 | -------------------------------------------------------------------------------- /src/autoban/doc/autoban.md: -------------------------------------------------------------------------------- 1 | # AutoBan 2 | 3 | AutoBan is an intrusion detection and prevention system which is built in the `mlan/asterisk` container. The intrusion detection is achieved by Asterisk itself. Asterisk generates security events which AutoBan listens to on the AMI interface. When security events occurs AutoBan start to watch the source IP address. Intrusion prevention is achieved by AutoBan asking the Linux kernel firewall [nftables](https://netfilter.org/projects/nftables/) to drop packages from offending source IP addresses. 4 | 5 | ## Operation 6 | 7 | Asterisk generates security events which AutoBan listens to on the AMI interface. When one of the security events; `InvalidAccountID`, `InvalidPassword`, `ChallengeResponseFailed`, or `FailedACL` occurs AutoBan start to watch the source IP address for `watchtime` seconds. If more than `maxcount` security events occurs within this time, all packages from the source IP address is dropped for `jailtime` seconds. 8 | 9 | When the `jailtime` expires packages are again accepted from the source IP address, but for an additional `watchtime` seconds this address is on "parole". Should a security event be detected (from this address) during the parole period it is immediately blocked again, and for a progressively longer time. This progression is configured by `repeatmult`, which determines how many times longer the address is blocked. If no security event is detected from this address during its parole time the IP is no longer being watched. 10 | 11 | To illustrate, first assume `jailtime=20m` and `repeatmult=6`, then the IP is blocked 20 minutes the first time, 2 hours the second, 12 hours the third, 3 days the forth and so on. 12 | 13 | ## Configuration 14 | 15 | The function of AutoBan is controlled by two configuration files; `autoban.conf` and `manager.conf`. Additionally, the docker container needs extra capabilities to be able to control networking. 16 | 17 | | File name | Description | 18 | | ------------ | ------------------------------------------------------------ | 19 | | autoban.conf | Configurations which are unique to the AutoBan service | 20 | | manager.conf | Read by the Asterisk Manager Interface (AMI), configuring both the server and client(s) | 21 | 22 | ### Docker, runtime privileges 23 | 24 | AutoBan uses [nftables](https://en.wikipedia.org/wiki/Nftables) which does the actual package filtering. The container needs additional [runtime privileges](https://docs.docker.com/v17.12/engine/reference/run/#runtime-privilege-and-linux-capabilities) to be able to do that. Nftables needs the `NET_ADMIN` and `NET_RAW` capabilities to function, which you provide by adding these options to the docker run command `--cap-add=NET_ADMIN --cap-add=NET_RAW`. 25 | 26 | ### Configuring AutoBan, autoban.conf 27 | 28 | AutoBan is activated if there is an `autoban.conf` file and that the parameter `enabled` within is not set to `no`. 29 | 30 | | Section | Key | Default | Format | Description | 31 | | ----------- | ---------- | --------- | -------------------------------------------- | ------------------------------------------------------------ | 32 | | [asmanager] | server | localhost | ip or fqdn | Here asterisk runs in same container so AMI server address is localhost or 127.0.0.1 | 33 | | [asmanager] | port | 5038 | integer | AMI server port number, same as port used in manager.conf | 34 | | [asmanager] | username | phpagi | string | AMI client name, same as section [] in manager.conf | 35 | | [asmanager] | secret | phpagi | string | AMI client password, same as secret in manager.conf | 36 | | [autoban] | enabled | true | boolean | AutoBan is activated when autoban.conf exists and not explicitly disabled here | 37 | | [autoban] | maxcount | 10 | integer | Abuse count at which IP will be jailed, that is, its packets will be dropped | 38 | | [autoban] | watchtime | 20m | string, integer followed by unit; d, h, m, s | Time to keep IP under watch in seconds or time string, example: 1d2h3m4s | 39 | | [autoban] | jailtime | 20m | string, integer followed by unit; d, h, m, s | Time to drop packets from IP in seconds or time string example: 1d2h3m4s | 40 | | [autoban] | repeatmult | 6 | integer or float | Repeat offenders get jailtime multiplied by this factor | 41 | 42 | ### Configuring the AMI, manager.conf 43 | 44 | The AMI interface is configured in `manager.conf`.The table below does only describe to most relevant keys. Please refer to [AMI Configuration](https://wiki.asterisk.org/wiki/display/AST/AMI+v2+Specification#AMIv2Specification-AMIConfiguration), for full details. 45 | 46 | | Section | key | Default | Format | Description | 47 | | ---------- | -------- | ------- | ---------- | ------------------------------------------------------------ | 48 | | [general] | enabled | no | boolean | Enable AMI server, yes or no | 49 | | [general] | bindaddr | 0.0.0.0 | ip address | Binds the AMI server to this IP address, for security reasons set this to; 127.0.0.1 | 50 | | [general] | port | 5038 | integer | Port the AMI's TCP server will listen to, same as, port in autoban.conf | 51 | | [] | | | string | Client name which AutoBan uses to authenticate with the AMI server, same as username in autoban.conf | 52 | | [] | secret | | string | Password which AutoBan uses to authenticate with the AMI server, same as secret in autoban.conf | 53 | | [] | read | all | string | A comma delineated list of the allowed class authorizations applied to events, for security reasons limit to; security | 54 | | [] | write | all | string | A comma delineated list of the allowed class authorizations applied to actions, for security reasons limit to; "" | 55 | 56 | ### Default configuration 57 | 58 | If the Asterisk configuration directory is empty, default configuration files will be copied there at container startup. The ones relevant here are `autoban.conf` and `manager.conf`. For security reasons it is suggested that the `secret` is changed in both files. 59 | 60 | `autoban.conf` 61 | 62 | ```ini 63 | [asmanager] 64 | server = 127.0.0.1 65 | port = 5038 66 | username = autoban 67 | secret = 6003.438 68 | 69 | [autoban] 70 | enabled = true 71 | maxcount = 10 72 | watchtime = 20m 73 | jailtime = 20m 74 | repeatmult = 6 75 | ``` 76 | `manager.conf` 77 | 78 | ```ini 79 | [general] 80 | enabled = yes 81 | bindaddr = 127.0.0.1 82 | port = 5038 83 | 84 | [autoban] 85 | secret = 6003.438 86 | read = security 87 | write = 88 | ``` 89 | 90 | ## Implementation 91 | AutoBan keeps track of which IP addresses are being watched, jailed or on parole by using the names sets and timeout constructs of [nftables](https://wiki.nftables.org/wiki-nftables/index.php/Main_Page). Timeout determines the time an element stays in the set. This mechanism is used to "automatically" keep tack of the watch, jail, and parole times. All state information is kept by nftables, so the AutoBan state can be preserved during container restarts by saving and loading the nftables configuration. 92 | 93 | At container startup nftables is configured by loading the file `autoban.nft` which defines the sets; watch, jail, parole, whitelist, and blacklist. Initially these sets does not contain any elements, that is, IP addresses. 94 | 95 | The sets; watch, jail and parole use timeout to keep track of how long the IP addresses should stay in each set. So these sets are dynamic. The sets watch and parole are only used for time keeping, so they have no rule attached. Contrarily, all packages from IP source addresses in the set jail will be dropped. 96 | 97 | If you want to permanently whitelist or blacklist source IP addresses, you can add them to the sets; whitelist or blacklist. Packages from source IP addresses in the set whitelist will always be accepted, whereas they will always be dropped if they are in the set blacklist. 98 | 99 | ## Command line utility, `autoban` 100 | 101 | In addition to the AutoBan daemon, `autoband.php` that listens to AMI events and controls the Linux kernel firewall, there is a shell utility `autoban`, that you can use within the container, that helps with managing the NFT state. It can add, delete, white list and black list IP addresses for example. 102 | 103 | You can see the `autoban` help message by, from within the container, typing: 104 | 105 | ```bash 106 | autoban help 107 | 108 | DESCRIPTION 109 | Shows an overview of the NFT state, which autoban uses to track IP adresses. 110 | Addresses can also be added or deleted. 111 | 112 | USAGE 113 | autoban [SUBCOMMAND] 114 | If no subcommand is given use "show". 115 | 116 | SUBCOMMAND 117 | add = Add to , and/or 118 | addrs from . 119 | del = Delete from , and/or 120 | addrs from . 121 | list List addrs from . 122 | help Print this text. 123 | show Show overview of the NFT state. 124 | 125 | EXAMPLES 126 | Blacklist 77.247.110.24 and 62.210.151.21 and all addresses from jail 127 | autoban add blacklist = 77.247.110.24 jail 62.210.151.21 128 | 129 | Add all addresses in the watch set to the jail and parole sets 130 | autoban add jail parole = watch 131 | 132 | Delete 37.49.230.37 and all addresses in blacklist from jail parole 133 | autoban del jail parole = 37.49.230.37 blacklist 134 | 135 | Delete 45.143.220.72 from all sets 136 | autoban del all = 45.143.220.72 137 | 138 | Delete all addresses from all sets 139 | autoban del all = all 140 | ``` 141 | 142 | You can watch the status of the `nftables` firewall by, from within the container, typing: 143 | 144 | ```bash 145 | nft list ruleset 146 | ``` 147 | -------------------------------------------------------------------------------- /src/autoban/entry.d/50-autoban-read-nftfile: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # 50-autoban-read-nftfile 4 | # 5 | # Load NFT state from file. 6 | # 7 | 8 | # 9 | # Configuration 10 | # The templates are in /usr/share/nftables and the actual state file is kept 11 | # in /etc/nftables.d. 12 | # 13 | source docker-common.sh 14 | 15 | DOCKER_NFT_DIR=${DOCKER_NFT_DIR-/etc/nftables.d} 16 | DOCKER_SEED_NFT_DIR=${DOCKER_SEED_NFT_DIR-/usr/share/nftables} 17 | DOCKER_NFT_FILE=${DOCKER_NFT_FILE-autoban.nft} 18 | DOCKER_NFT_TABLE=${DOCKER_NFT_TABLE-autoban} 19 | 20 | # 21 | # Only proceed if nftables is installed 22 | # 23 | 24 | if ! dc_is_installed nftables; then 25 | dc_log 5 "Not possible to seed nft configuration since nftables is not installed." 26 | return 27 | fi 28 | 29 | # 30 | # Make sure that we have the required directory structure in place under 31 | # DOCKER_PERSIST_DIR. It will be missing if we mount an empty volume there. 32 | # 33 | 34 | mkdir -p ${DOCKER_PERSIST_DIR}${DOCKER_NFT_DIR} 35 | 36 | # 37 | # If DOCKER_NFT_FILE is empty or is missing 38 | # initialize it with files from DOCKER_SEED_NFT_DIR. 39 | # 40 | 41 | nft_file=$DOCKER_NFT_DIR/$DOCKER_NFT_FILE 42 | 43 | if [ ! -s ${nft_file} ]; then 44 | dc_log 5 "Seeding nft configuration." 45 | cp $DOCKER_SEED_NFT_DIR/$DOCKER_NFT_FILE $DOCKER_NFT_DIR 46 | fi 47 | 48 | # 49 | # If nft_file exists have NFT import it 50 | # 51 | 52 | if [ -f ${nft_file} ]; then 53 | dc_log 5 "Importing $nft_file." 54 | if nft delete table $DOCKER_NFT_TABLE 2>/dev/null; then 55 | dc_log 4 "nft table $DOCKER_NFT_TABLE was overwritten" 56 | fi 57 | nft -f ${nft_file} 58 | fi 59 | -------------------------------------------------------------------------------- /src/autoban/exit.d/50-autoban-write-nftfile: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # 50-autoban-write-nftfile 4 | # 5 | # Save current NFT state to file. 6 | # 7 | 8 | # 9 | # Configuration 10 | # The templates are in /usr/share/nftables and the actual state file is kept 11 | # in /etc/nftables.d. 12 | # 13 | source docker-common.sh 14 | 15 | DOCKER_NFT_DIR=${DOCKER_NFT_DIR-/etc/nftables.d} 16 | DOCKER_NFT_FILE=${DOCKER_NFT_FILE-autoban.nft} 17 | DOCKER_NFT_TABLE=${DOCKER_NFT_TABLE-autoban} 18 | 19 | # 20 | # Only proceed if nftables is installed 21 | # 22 | 23 | if ! dc_is_installed nftables; then 24 | dc_log 5 "Not possible to save nft state since nftables is not installed." 25 | return 26 | fi 27 | 28 | # 29 | # Save current nftables state to file. Omit any "expires time-string" by using 30 | # the option --stateless, since nftables refuse to load files with them. 31 | # 32 | 33 | nft_file=$DOCKER_NFT_DIR/$DOCKER_NFT_FILE 34 | if [ -n ${nft_file} ]; then 35 | dc_log 5 "Saving config to $nft_file." 36 | nft --stateless list table $DOCKER_NFT_TABLE > ${nft_file} 37 | fi 38 | -------------------------------------------------------------------------------- /src/autoban/nft/autoban.nft: -------------------------------------------------------------------------------- 1 | #!/usr/sbin/nft -f 2 | # 3 | 4 | table ip autoban { 5 | set watch { 6 | type ipv4_addr 7 | flags timeout 8 | } 9 | set jail { 10 | type ipv4_addr 11 | flags timeout 12 | } 13 | set parole { 14 | type ipv4_addr 15 | flags timeout 16 | } 17 | set blacklist { 18 | type ipv4_addr 19 | } 20 | set whitelist { 21 | type ipv4_addr 22 | } 23 | 24 | chain incoming { 25 | type filter hook input priority security; policy accept; 26 | ip saddr @whitelist accept 27 | ip saddr @blacklist drop 28 | ip saddr @jail drop 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/autoban/php/autoban.class.inc: -------------------------------------------------------------------------------- 1 | true, 24 | 'maxcount' => 10, 25 | 'watchtime' => '20m', 26 | 'jailtime' => '20m', 27 | 'repeatmult' => 6, 28 | ]; 29 | public const NFT_SETS = ['watch','jail','parole','blacklist','whitelist']; 30 | private const TIME_MAXSEC = 99999999; 31 | public $config; 32 | public $debug = false; 33 | public function __construct($config = null, array $optconfig = []) { 34 | parent::__construct(); 35 | if (is_string($config) !== true) { 36 | $config = self::DEFAULT_CONF_FILE; 37 | } 38 | $this->config['autoban'] = self::DEFAULT_CONF_VALS; 39 | if (file_exists($config) === true) { 40 | $config_ini = parse_ini_file($config,true); 41 | if (!empty($config_ini['autoban'])) 42 | $this->config['autoban'] = array_merge($this->config['autoban'], 43 | $config_ini['autoban']); 44 | } else { 45 | $this->config['autoban']['enabled'] = false; 46 | } 47 | foreach ($optconfig as $var => $val) { 48 | $this->config['autoban'][$var] = $val; 49 | } 50 | } 51 | /*-------------------------------------------------------------------------- 52 | Start to count how many time we watch an IP address $ip by adding it to the 53 | 'watch' NFT set and use the number of seconds of its timeout > 'watchtime' 54 | as a counter. If count is > 'maxcount' add $ip to 'jail' and 'parole' NFT 55 | sets with timeouts 'jailtime' and jailtime'+'watchtime'. If $ip is watched 56 | during its parole immediately add it again to the 'jail' and 'parole' NFT sets 57 | with timeouts 'repeatmult' longer than previously. 58 | 59 | @param string $ip eg "23.94.144.50" 60 | @return boolean success 61 | */ 62 | public function book($ip) { 63 | // if not already in jail but on parole or watch count > maxcount 64 | // determine sentence and increment jail and parole counters 65 | if(ip2long($ip) === false) { 66 | trigger_error(sprintf('Got invalid IP address (%s)',$ip),E_USER_WARNING); 67 | return false; 68 | } 69 | $log = null; 70 | $watch_nft = $this->timeoutsec('watch',$ip); 71 | $jail_nft = $this->timeoutsec('jail',$ip); 72 | $parole_nft = $this->timeoutsec('parole',$ip); 73 | $watch_new = $this->incrementwatch($watch_nft); 74 | $jail_new = $this->jailsec($watch_new,$jail_nft,$parole_nft); 75 | $parole_new = $this->parolesec($jail_new); 76 | if ($jail_nft === false) { 77 | if($jail_new !== false) { 78 | $log = 'jail'; 79 | $watch_new = false; 80 | if ($parole_nft !== false) $this->del_addrs('parole',$ip); 81 | } else { 82 | $log = 'watch'; 83 | if ($watch_nft !== false) $this->del_addrs('watch',$ip); 84 | } 85 | } 86 | if ($this->add_addrs('watch',$ip,$watch_new) === false) return false; 87 | if ($this->add_addrs('jail',$ip,$jail_new) === false) return false; 88 | if ($this->add_addrs('parole',$ip,$parole_new) === false) return false; 89 | switch ($log) { 90 | case 'watch': 91 | $this->log(sprintf('Watching %15s %-8d',$ip, 92 | $this->countwatch($watch_new))); 93 | break; 94 | case 'jail': 95 | $this->log(sprintf('Jailing %15s %8s',$ip, 96 | $this->timestr($jail_new)),null,E_USER_WARNING); 97 | break; 98 | } 99 | if ($this->save() === false) return false; 100 | return true; 101 | } 102 | /*-------------------------------------------------------------------------- 103 | Increment both watch count and watchtime, illustrated below, watchtime=20m. 104 | $time $count $time 105 | false 1 20m1s 106 | 20m1s 2 20m2s 107 | 20m2s 3 20m3s 108 | 20m3s 4 20m4s 109 | 20m4s 5 20m5s 110 | 111 | @param mixed $time integer time in seconds or boolean false 112 | @return integer time + 1 113 | */ 114 | private function incrementwatch($time) { 115 | if($time === false) { 116 | return $this->configsec('watchtime') + 1; 117 | } else { 118 | return $time + 1; 119 | } 120 | } 121 | /*-------------------------------------------------------------------------- 122 | @param integer $time integer time in seconds 123 | @return integer count 124 | */ 125 | private function countwatch($time) { 126 | return $time - $this->configsec('watchtime'); 127 | } 128 | /*-------------------------------------------------------------------------- 129 | Compute sentencing time which is last jailtime times repeatmult if in parole. 130 | Sentencing is jailtime if first time offender watch count >= maxcount. 131 | Return false if already in jail or watch count < maxcount. 132 | 133 | @param mixed $watchtime integer time in seconds or boolean false 134 | @param mixed $jailtime integer time in seconds or boolean false 135 | @param mixed $paroletime integer time in seconds or boolean false 136 | @return mixed integer sentence time in seconds or boolean false 137 | */ 138 | private function jailsec($watchtime,$jailtime,$paroletime) { 139 | if ($jailtime !== false) return false; 140 | if ($paroletime !== false) { 141 | $jailt = max($this->configsec('jailtime'), 142 | $paroletime - $this->configsec('watchtime')); 143 | return $jailt * $this->config['autoban']['repeatmult']; 144 | } elseif (($watchtime !== false) && 145 | ($watchtime - $this->configsec('watchtime') >= 146 | $this->config['autoban']['maxcount'])) { 147 | return $sentence = $this->configsec('jailtime'); 148 | } 149 | return false; 150 | } 151 | /*-------------------------------------------------------------------------- 152 | Compute probation time = sentence time + watchtime. Also make sure both 153 | probation and sentence times are sane. 154 | 155 | @param mixed &$sentence integer time in seconds or boolean false 156 | @return mixed integer probation time in seconds or boolean false 157 | */ 158 | private function parolesec(&$sentence) { 159 | if ($sentence === false) return false; 160 | $watchtime = $this->configsec('watchtime'); 161 | if ($watchtime > 0.5*self::TIME_MAXSEC) $watchtime = 0.5*self::TIME_MAXSEC; 162 | $parole = $sentence + $watchtime; 163 | if ($parole > self::TIME_MAXSEC) { 164 | $parole = self::TIME_MAXSEC; 165 | $sentence = $parole - $watchtime; 166 | } 167 | $sentence = round($sentence); 168 | return round($parole); 169 | } 170 | /*-------------------------------------------------------------------------- 171 | @param string $set eg "jail" 172 | @param string $ip eg "23.94.144.50" 173 | @return mixed time integer seconds or boolean false 174 | */ 175 | public function timeoutsec($set,$ip) { 176 | return $this->str2sec($this->get_timeout($set,$ip)); 177 | } 178 | /*-------------------------------------------------------------------------- 179 | @param string $set eg "jail" 180 | @return mixed time integer seconds or boolean false 181 | */ 182 | public function configtime($set) { 183 | switch ($set) { 184 | case 'watch': 185 | $sec = $this->configsec('watchtime'); 186 | break; 187 | case 'jail': 188 | $sec = $this->configsec('jailtime'); 189 | break; 190 | case 'parole': 191 | $sec = $this->configsec('jailtime') + $this->configsec('watchtime'); 192 | break; 193 | default: 194 | $sec = null; 195 | } 196 | return $this->sec2str($sec); 197 | } 198 | /*-------------------------------------------------------------------------- 199 | Convert config times in sting format to seconds eg "20m" to 1200 200 | @param string $param eg, "watchtime" 201 | @return integer $seconds 202 | */ 203 | public function configsec($param) { 204 | $time = $this->config['autoban'][$param]; 205 | if(!is_numeric($time)) $time = $this->str2sec($time); 206 | return $time; 207 | } 208 | /*-------------------------------------------------------------------------- 209 | @param string $message eg "Jailing 23.94.144.50" 210 | @param mixed $error eg 404 211 | @param integer $level eg E_USER_WARNING 212 | @return void 213 | */ 214 | public function log($message, $error = [], $level = E_USER_NOTICE) { 215 | if (isset($error[0])) { 216 | $message = $message.' error: '.$error[0]; 217 | } else { 218 | $nr_watch = count($this->list('watch')); 219 | $nr_jail = count($this->list('jail')); 220 | $message = sprintf('%s (watch %-3d jail %-3d)', 221 | $message,$nr_watch,$nr_jail); 222 | } 223 | trigger_error($message, $level); 224 | } 225 | } 226 | ?> 227 | -------------------------------------------------------------------------------- /src/autoban/php/autoban.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | = Add to , and/or 18 | addrs from . 19 | del = Delete from , and/or 20 | addrs from . 21 | list List addrs from . 22 | help Print this text. 23 | show Show overview of the NFT state. 24 | 25 | EXAMPLES 26 | Blacklist 77.247.110.24 and 62.210.151.21 and all addresses from jail 27 | autoban add blacklist = 77.247.110.24 jail 62.210.151.21 28 | 29 | Add all addresses in the watch set to the jail and parole sets 30 | autoban add jail parole = watch 31 | 32 | Delete 37.49.230.37 and all addresses in blacklist from jail parole 33 | autoban del jail parole = 37.49.230.37 blacklist 34 | 35 | Delete 45.143.220.72 from all sets 36 | autoban del all = 45.143.220.72 37 | 38 | Delete all addresses from all sets 39 | autoban del all = all 40 | 41 | 42 | HELP_MESSAGE; 43 | 44 | /*------------------------------------------------------------------------------ 45 | Initiate logging and load dependencies. 46 | */ 47 | openlog("autoban", LOG_PID | LOG_PERROR, LOG_LOCAL0); 48 | require_once 'error.inc'; 49 | require_once 'autoban.class.inc'; 50 | 51 | /*------------------------------------------------------------------------------ 52 | Create class objects and set log level. 53 | */ 54 | $ban = new Autoban(); 55 | 56 | /*-------------------------------------------------------------------------- 57 | Add elements $addr to NFT set $set 58 | @param array of strings $args eg ["blacklist", "=", "77.247.110.24", "jail"] 59 | @return boolean false if unable to add element else true 60 | */ 61 | function add($args) { 62 | global $ban; 63 | parse($args,$dargs,$sargs,'blacklist'); 64 | foreach ($dargs as $dset) { 65 | $timeout = $ban->configtime($dset); 66 | $assume_ssets = array_intersect($sargs,Autoban::NFT_SETS); 67 | $assume_saddrs = array_diff($sargs,Autoban::NFT_SETS); 68 | foreach ($assume_ssets as $sset) { 69 | $saddrs = array_keys($ban->list($sset)); 70 | $ban->add_addrs($dset, $saddrs, $timeout); 71 | } 72 | $ban->add_addrs($dset, $assume_saddrs, $timeout, true); 73 | } 74 | $ban->save(); 75 | } 76 | 77 | /*-------------------------------------------------------------------------- 78 | Delete elements $args from NFT sets $sets 79 | @param array of strings $args eg ["blacklist", "=", "77.247.110.24", "jail"] 80 | @return boolean false if unable to delete element else true 81 | */ 82 | function del($args) { 83 | global $ban; 84 | parse($args,$dargs,$sargs,'all'); 85 | if (array_search('all', $dargs) !== false) $dargs = Autoban::NFT_SETS; 86 | foreach ($dargs as $dset) { 87 | $assume_ssets = array_intersect($sargs,Autoban::NFT_SETS); 88 | foreach ($assume_ssets as $sset) { 89 | $saddrs = array_keys($ban->list($sset)); 90 | $ban->del_addrs($dset, $saddrs); 91 | } 92 | $assume_saddrs = array_diff($sargs,Autoban::NFT_SETS); 93 | $ban->del_addrs($dset, $assume_saddrs); 94 | } 95 | $ban->save(); 96 | } 97 | 98 | /*-------------------------------------------------------------------------- 99 | List elements in NFT sets $sets 100 | @param array of strings $args eg ["blacklist", "jail"] 101 | @return void 102 | */ 103 | function ls($args) { 104 | global $ban; 105 | if (empty($args) || array_search('all', $args) !== false) 106 | $args = Autoban::NFT_SETS; 107 | foreach ($args as $set) 108 | if (count($args) === 1) 109 | printf("%s\n", implode(' ',array_keys($ban->list($set)))); 110 | else 111 | printf("%s: %s\n", $set, implode(' ',array_keys($ban->list($set)))); 112 | } 113 | 114 | /*-------------------------------------------------------------------------- 115 | Separates argument in to $dargs and $sargs using the separator 116 | @param array of strings $args eg ["blacklist", "=", "77.247.110.24", "jail"] 117 | @param array of strings $dargs eg ["blacklist"] 118 | @param array of strings $sargs eg ["77.247.110.24", "jail"] 119 | @return void 120 | */ 121 | function parse($args, &$dargs, &$sargs, $default = 'all', $separators = ':+-=') { 122 | $left = []; $right = []; 123 | foreach ($args as $arg) { 124 | if (strlen($arg) === 1 && strstr($separators, $arg) !== false) { 125 | $mid = $arg; 126 | } else { 127 | if (empty($mid)) { 128 | array_push($left, $arg); 129 | } else { 130 | array_push($right, $arg); 131 | } 132 | } 133 | } 134 | if (empty($right)) { 135 | $dargs = [$default]; 136 | $sargs = $left; 137 | } else { 138 | $dargs = $left; 139 | $sargs = $right; 140 | } 141 | } 142 | /*------------------------------------------------------------------------------ 143 | Start code execution. 144 | Scrape off command and sub-command and pass the rest of the arguments. 145 | */ 146 | #$ban->debug = true; 147 | $subcmd=@$argv[1]; 148 | unset($argv[0],$argv[1]); 149 | #if(!empty($subcmd)) 150 | # trigger_error(sprintf('Running %s %s', $subcmd, implode(' ',$argv)), 151 | # E_USER_NOTICE); 152 | switch (@$subcmd) { 153 | case 'add': 154 | case 'a': 155 | add(@$argv); 156 | break; 157 | case 'delete': 158 | case 'del': 159 | case 'd': 160 | del(@$argv); 161 | break; 162 | case 'list': 163 | case 'ls': 164 | case 'l': 165 | ls(@$argv); 166 | break; 167 | case 'show': 168 | case 's': 169 | case '': 170 | $ban->show(); 171 | break; 172 | case 'help': 173 | default: 174 | print $HELP_MESSAGE; 175 | break; 176 | } 177 | ?> 178 | -------------------------------------------------------------------------------- /src/autoban/php/autoband.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | book($ip); 25 | } 26 | } 27 | } 28 | 29 | /*------------------------------------------------------------------------------ 30 | Create class objects and set log level. 31 | */ 32 | $ban = new \Autoban('/etc/asterisk/autoban.conf'); 33 | $ami = new \PHPAMI\Ami('/etc/asterisk/autoban.conf'); 34 | $ami->setLogLevel(2); 35 | 36 | /*------------------------------------------------------------------------------ 37 | Register the AMI event handlers to their corresponding events. 38 | */ 39 | $ami->addEventHandler('FailedACL', 'eventAbuse'); 40 | $ami->addEventHandler('InvalidAccountID', 'eventAbuse'); 41 | $ami->addEventHandler('ChallengeResponseFailed', 'eventAbuse'); 42 | $ami->addEventHandler('InvalidPassword', 'eventAbuse'); 43 | 44 | /*------------------------------------------------------------------------------ 45 | Start code execution. 46 | Wait 1s allowing Asterisk time to setup the Asterisk Management Interface (AMI). 47 | If autoban is activated try to connect to the AMI. If successful, start 48 | listening for events indefinitely. If connection fails, retry to connect. 49 | If autoban is deactivated stay in an infinite loop instead of exiting. 50 | Otherwise the system supervisor will relentlessly just try to restart us. 51 | */ 52 | $wait_init = 2; 53 | $wait_extra = 58; 54 | $wait_off = 3600; 55 | if ($ban->config['autoban']['enabled']) { 56 | while(true) { 57 | sleep($wait_init); 58 | if ($ami->connect()) { 59 | trigger_error('Activated and connected to AMI',E_USER_NOTICE); 60 | $ami->waitResponse(); // listen for events until connection fails 61 | $ami->disconnect(); 62 | } else { 63 | trigger_error('Unable to connect to AMI',E_USER_ERROR); 64 | sleep($wait_extra); 65 | } 66 | } 67 | } else { 68 | trigger_error('Disabled! Activate autoban using conf file '. 69 | '(/etc/asterisk/autoban.conf)',E_USER_NOTICE); 70 | while(true) { sleep($wait_off); } 71 | } 72 | 73 | 74 | /*------------------------------------------------------------------------------ 75 | We will never come here. 76 | */ 77 | ?> 78 | -------------------------------------------------------------------------------- /src/autoban/php/nft.class.inc: -------------------------------------------------------------------------------- 1 | 'nft', 8 | 'opt' => '--stateless', 9 | 'sub' => null, 10 | 'family' => 'ip', 11 | 'table' => 'autoban', 12 | 'chain' => null, 13 | 'set' => null, 14 | 'val' => null, 15 | 'stderr' => '2>&1', 16 | ]; 17 | public const NFT_SETS = ['watch','jail','parole','blacklist','whitelist']; 18 | private const TIME_UNITS = ['d' => 86400, 'h' => 3600, 'm' => 60, 's' => 1]; 19 | private const SHOW_MARK = 'X'; 20 | private const SHOW_ID = 'addr'; 21 | private const NFT_DIR = '/etc/nftables.d'; 22 | private const NFT_FILE = 'autoban.nft'; 23 | private $nft_statefile; 24 | public $debug = false; 25 | /*-------------------------------------------------------------------------- 26 | Constructor 27 | @return void 28 | */ 29 | public function __construct() { 30 | $nft_dir = (getenv('DOCKER_NFT_DIR') !== false) ? 31 | getenv('DOCKER_NFT_DIR') : self::NFT_DIR; 32 | $nft_file = (getenv('DOCKER_NFT_FILE') !== false) ? 33 | getenv('DOCKER_NFT_FILE') : self::NFT_FILE; 34 | $this->nft_statefile = $nft_dir.'/'.$nft_file; 35 | } 36 | /*-------------------------------------------------------------------------- 37 | Add element $addr to NFT set $set with timeout $timeout seconds 38 | @param string $set eg "jail" 39 | @param string $addr eg "23.94.144.50" 40 | @param mixed $timeout int seconds eg 1200 or boolean false 41 | @return boolean false if unable to add element else true 42 | */ 43 | public function add($set, $addr, $timeout = null, $check = false) { 44 | if ($check && !$this->is_addr($addr)) return false; 45 | if ($this->add_addrs($set,$addr,$timeout) === false) return false; 46 | if ($this->save() === false) return false; 47 | return true; 48 | } 49 | /*-------------------------------------------------------------------------- 50 | Delete element $addr from NFT set $set 51 | @param string $set eg "jail" 52 | @param string $addr eg "23.94.144.50" 53 | @return mixed boolean false if unable to del element else true 54 | */ 55 | public function del($set, $addr = null, $check = false) { 56 | if ($check) if (!$this->is_addr($addr)) return false; 57 | if ($this->del_addrs($set,$addr) === false) return false; 58 | if ($this->save() === false) return false; 59 | return true; 60 | } 61 | /*-------------------------------------------------------------------------- 62 | Get array of all addr in set $set. 63 | @param string $set eg "jail" 64 | @return array [string addr => string time, ...] or [] is set is empty 65 | */ 66 | public function list($set) { 67 | $return = $this->list_addr($set); 68 | if ($return === false) return []; 69 | return $this->array_elem($return); 70 | } 71 | /*-------------------------------------------------------------------------- 72 | Get timeout for $addr from NFT set $set 73 | @param string $set eg "jail" 74 | @param string $addr eg "23.94.144.50" 75 | @return mixed exec return string or boolean false if unable to get timeout 76 | */ 77 | public function get_timeout($set,$addr) { 78 | $timeout = $this->array_elem($this->get_addr($set,$addr)); 79 | if (!empty($timeout)) return array_values($timeout)[0]; 80 | return false; 81 | } 82 | /*-------------------------------------------------------------------------- 83 | Add element $addr to NFT set $set with timeout $timeout seconds 84 | @param string $set eg "jail" 85 | @param mixed $addrs string eg "23.94.144.50" or array ["23.94.144.50",...] or null or false 86 | @param mixed $timeout int seconds eg 1200 or boolean false 87 | @return mixed exec return string or boolean false if unable to add elements 88 | */ 89 | public function add_addrs($set,$addrs,$timeout = null) { 90 | if (empty($addrs) || $timeout === false) return true; 91 | $suffix = (empty($timeout)) ? '' : ' timeout '.$this->timestr($timeout); 92 | if(is_array($addrs)) { 93 | $addrs = array_map(function($a) use ($suffix) { return $a.$suffix; }, $addrs); 94 | $val = implode(', ', $addrs); 95 | } else { 96 | $val = $addrs.$suffix; 97 | } 98 | $args = ['sub'=>'add element', 'set'=>$set, 'val'=>'{'.$val.'}']; 99 | return $this->exec($args); 100 | } 101 | /*-------------------------------------------------------------------------- 102 | Delete element $addr from NFT set $set 103 | @param string $set eg "jail" 104 | @param mixed $addrs string eg "23.94.144.50" or array ["23.94.144.50","all",...] or null 105 | @return mixed exec return string or boolean false if unable to del elements 106 | */ 107 | public function del_addrs($set, $addrs = null) { 108 | if (empty($addrs)) return true; 109 | $given_addrs = (is_array($addrs)) ? $addrs : [$addrs]; 110 | if (array_search('all', $given_addrs) !== false) { 111 | $args = ['sub'=>'flush set','set'=>$set]; 112 | } else { 113 | $valid_addrs = array_keys($this->list($set)); 114 | $the_addrs = array_intersect($given_addrs, $valid_addrs); 115 | if (empty($the_addrs)) return true; 116 | $val = implode(', ', $the_addrs); 117 | $args = ['sub'=>'delete element','set'=>$set,'val'=>'{'.$val.'}']; 118 | } 119 | return $this->exec($args); 120 | } 121 | /*-------------------------------------------------------------------------- 122 | Get element $addr from NFT set $set 123 | @param string $set eg "jail" 124 | @param string $addr eg "23.94.144.50" 125 | @return mixed exec return string or boolean false if unable to del element 126 | */ 127 | public function get_addr($set,$addr) { 128 | if (empty($addr)) return true; 129 | $args = ['sub'=>'get element','set'=>$set,'val'=>'{'.$addr.'}']; 130 | return $this->exec($args, false); 131 | } 132 | /*-------------------------------------------------------------------------- 133 | List elements in NFT set $set 134 | @param string $set eg "jail" 135 | @return mixed exec return string or boolean false if unable to del element 136 | */ 137 | public function list_addr($set) { 138 | $args = ['sub'=>'list set','set'=>$set]; 139 | return $this->exec($args); 140 | } 141 | /*-------------------------------------------------------------------------- 142 | NFT returns elements = { 23.94.144.50 timeout 40m, ...} 143 | We stuff this into $this->timeout[$set] = [string addr => string time, ...]. 144 | 145 | @param array $return strings return from calling NFT 146 | @return array [string addr => string time, ...] or [] is set is empty 147 | */ 148 | public function array_elem($return) { 149 | if ($return === false) return []; 150 | preg_match('/flags timeout/', implode($return), $hastimeout); 151 | preg_match('/elements = {([^}]+)}/', implode($return), $matches); 152 | if (empty($matches[1])) return []; 153 | $elements = preg_split('/,/', $matches[1]); 154 | $timeout = []; 155 | foreach ($elements as $element) { 156 | if (empty($hastimeout)) { 157 | $timeout += [trim($element) => Self::SHOW_MARK]; 158 | } else { 159 | $addrntime = explode(' timeout ',$element); 160 | $timeout += [trim($addrntime[0]) => trim($addrntime[1])]; 161 | } 162 | } 163 | return $timeout; 164 | } 165 | /*-------------------------------------------------------------------------- 166 | Save NFT state to file. Rename the old state file by appending "~" to the 167 | filename before saving the NFT state. 168 | @return mixed state string or boolean false if unsuccessful 169 | */ 170 | public function save() { 171 | $args = ['sub'=>'list ruleset', 'family'=>null, 'table'=>null]; 172 | $return = $this->exec($args); 173 | if ($return === false) return false; 174 | @rename($this->nft_statefile, $this->nft_statefile.'~'); 175 | if (file_put_contents($this->nft_statefile,implode(PHP_EOL, $return)) 176 | === false) { 177 | $this->log('('.$this->nft_statefile.')', [ 'Unable to write' ], 178 | E_USER_WARNING); 179 | return false; 180 | } 181 | return $return; 182 | } 183 | /*-------------------------------------------------------------------------- 184 | @param array $args NFT cli arguments eg ['sub'=>'list set','set'=>'jail'] 185 | @return mixed NFT return string or boolean false if error status 186 | */ 187 | private function exec($args, $logerror = true) { 188 | $exec_array = array_merge(self::NFT_SYNTAX,$args); 189 | $exec_string = implode(' ',$exec_array); 190 | $this->debug($exec_string); 191 | exec($exec_string,$return,$status); 192 | if ($status === 0) { 193 | return $return; 194 | } else { 195 | if ($logerror) { 196 | $this->log('('.$exec_array['sub'].')', $return, E_USER_WARNING); 197 | $this->debug($exec_string); 198 | } 199 | return false; 200 | } 201 | } 202 | /*-------------------------------------------------------------------------- 203 | @param mixed $time string eg, "1d9h40m1s" integer 1200 or null 204 | @return string $time eg, "1d9h40m1s" or null or boolean false 205 | */ 206 | public function timestr($time) { 207 | if (empty($time)) return $time; 208 | if (is_numeric($time)) return $this->sec2str($time); 209 | if ($this->str2sec($time) === false) { 210 | return false;; 211 | } else { 212 | return $time; 213 | } 214 | } 215 | /*-------------------------------------------------------------------------- 216 | @param string $time eg, "1d9h40m1s" 217 | @return mixed $seconds int seconds or boolean false 218 | */ 219 | public function str2sec($time) { 220 | if ($time === false || $time === null) return $time; 221 | preg_match_all('/(\d+)([dhms])/',$time,$matches); 222 | if (empty($matches[0])) return false; 223 | $unitvalue = array_combine($matches[2],$matches[1]); 224 | $seconds = 0; 225 | foreach ($unitvalue as $unit => $value) { 226 | $seconds += self::TIME_UNITS[$unit] * $value; 227 | } 228 | return $seconds; 229 | } 230 | /*-------------------------------------------------------------------------- 231 | @param integer $seconds 232 | @return string $time eg, "1d9h40m1s" 233 | */ 234 | public function sec2str($seconds) { 235 | if ($seconds === false || $seconds === null) return $seconds; 236 | $time = ""; 237 | foreach (self::TIME_UNITS as $unit => $scale) { 238 | $number = floor($seconds / $scale); 239 | if ($number > 0) { 240 | $time .= sprintf('%d%s',$number,$unit); 241 | $seconds = $seconds % $scale; 242 | } 243 | } 244 | return $time; 245 | } 246 | /*-------------------------------------------------------------------------- 247 | @param string $addr eg "23.94.144.50" 248 | @return boolean 249 | */ 250 | public function is_addr($addr) { 251 | if (ip2long($addr) === false) { 252 | trigger_error(sprintf('Got invalid IP address (%s)', 253 | $addr),E_USER_WARNING); 254 | return false; 255 | } else return true; 256 | } 257 | /*-------------------------------------------------------------------------- 258 | @param mixed $time string eg, "1d9h40m1s" 259 | @return boolean 260 | */ 261 | public function is_timestr($timeout) { 262 | if ($this->timestr($timeout) === false) { 263 | trigger_error(sprintf('Got invalid timeout value (%s)', 264 | $timeout),E_USER_WARNING); 265 | return false; 266 | } else return true; 267 | } 268 | /*-------------------------------------------------------------------------- 269 | @param string $message eg "Jailing 23.94.144.50" 270 | @param mixed $error eg 404 271 | @param integer $level eg E_USER_WARNING 272 | @return void 273 | */ 274 | public function log($message, $error = [], $level = E_USER_NOTICE) { 275 | if (isset($error[0])) { 276 | $message = $message.' error: '.$error[0]; 277 | } 278 | trigger_error($message, $level); 279 | } 280 | /*-------------------------------------------------------------------------- 281 | print table with headers: addr watch jail parole blacklist whitelist 282 | @return void 283 | */ 284 | public function show() { 285 | $db = []; 286 | $id = self::SHOW_ID; 287 | $headers = array_merge([$id],self::NFT_SETS); 288 | $sets_num = count(self::NFT_SETS); 289 | $form = "%15s".str_repeat("%13s", $sets_num)."\n"; 290 | foreach (self::NFT_SETS as $set) { 291 | $elems = $this->list($set); 292 | if (!empty($elems)) foreach ($elems as $addr => $time) { 293 | $db_index = array_search($addr, array_column($db, $id)); 294 | if ($db_index === false) { 295 | $db_index = -1 + array_push($db, array_merge([$id => $addr], 296 | array_combine(self::NFT_SETS, 297 | array_fill(0,$sets_num,null)))); 298 | } 299 | $db[$db_index][$set] = $time; 300 | } 301 | } 302 | usort($db, function($a,$b) use ($id) { 303 | return ip2long($a[$id]) <=> ip2long($b[$id]); 304 | }); 305 | $db_sums = array_map(function($head) use ($db) { 306 | return count(array_filter(array_column($db, $head))); 307 | }, $headers); 308 | vprintf($form,$headers); 309 | if (!empty($db)) foreach ($db as $elem) { 310 | vprintf($form,array_values($elem)); 311 | } 312 | vprintf($form,$db_sums); 313 | 314 | } 315 | /*-------------------------------------------------------------------------- 316 | Print variable if $debug or $this->debug is true 317 | @param mixed $var 318 | @param boolean $debug 319 | @return void 320 | */ 321 | public function debug($var, $debug = false) { 322 | if($debug || $this->debug) { 323 | var_dump($var); 324 | } 325 | } 326 | } 327 | ?> 328 | -------------------------------------------------------------------------------- /src/common/php/error.inc: -------------------------------------------------------------------------------- 1 | 37 | -------------------------------------------------------------------------------- /src/docker/bin/docker-common.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # docker-common.sh 4 | # 5 | # Defines common functions. Source this file from other scripts. 6 | # 7 | DOCKER_LOGLEVEL=${DOCKER_LOGLEVEL-5} 8 | DOCKER_LOGENTRY=${DOCKER_LOGENTRY-docker-entrypoint.sh} 9 | DOCKER_LOGUSAGE=${DOCKER_LOGUSAGE-usage} 10 | 11 | # 12 | # Write messages to console if interactive or syslog if not. 13 | # Usage: inform priority message 14 | # The priority may be specified numerically or as a facility.level pair. 15 | # Example user.notice, or 1.6 level is one of: 16 | # 0|emerg|1|alert|2|crit|3|err|4|warning|5|notice|6|info|7|debug 17 | # 18 | dc_log() { 19 | local script=$(basename $0) 20 | local stamp="$(dc_log_stamp)" 21 | local prio=$1 22 | local level=${prio#*.} 23 | local logtag="${script}[${$}]" 24 | local ttytag="$(dc_log_stamp)$(dc_log_tag $level $logtag):" 25 | shift 26 | # Assume interactive if we have stdout open and print usage message if needed. 27 | if [ -t 1 ]; then 28 | echo "$@" 29 | case "$level" in 30 | 0|emerg|1|alert|2|crit|3|err) $DOCKER_LOGUSAGE 2>/dev/null ;; 31 | esac 32 | else 33 | # If we have /dev/log socket send message to logger otherwise to stdout. 34 | if [ -S /dev/log ]; then 35 | logger -t "$logtag" -p "$prio" "$@" 36 | else 37 | if dc_log_level "$level"; then 38 | echo "$ttytag $@" 39 | fi 40 | fi 41 | fi 42 | } 43 | 44 | # 45 | # Color log output. Used if the syslogd daemon is not running. 46 | # 47 | dc_log_tag() { 48 | local level=$1 49 | local string=$2 50 | local c l 51 | case $level in 52 | 0|emerg) c=91; l=EMERG ;; 53 | 1|alert) c=91; l=ALERT ;; 54 | 2|crit) c=91; l=CRIT ;; 55 | 3|err) c=91; l=ERROR ;; 56 | 4|warning) c=93; l=WARN ;; 57 | 5|notice) c=92; l=NOTE ;; 58 | 6|info) c=92; l=INFO ;; 59 | 7|debug) c=92; l=DEBUG ;; 60 | esac 61 | printf "\e[%sm%s %s\e[0m\n" $c $string $l 62 | } 63 | 64 | # 65 | # Use $DOCKER_LOGLEVEL during image build phase. Assume we are in build phase if 66 | # $DOCKER_LOGENTRY is not running. 67 | # 68 | dc_log_level() { 69 | local level=$1 70 | if pidof $DOCKER_LOGENTRY >/dev/null; then 71 | [ "$level" -le "$SYSLOG_LEVEL" ] 72 | else 73 | [ "$level" -le "$DOCKER_LOGLEVEL" ] 74 | fi 75 | } 76 | 77 | # 78 | # Don't add time stamp during image build phase. Assume we are in build phase if 79 | # $DOCKER_LOGENTRY is not running. 80 | # 81 | dc_log_stamp() { 82 | if grep -q $DOCKER_LOGENTRY /proc/1/cmdline; then 83 | date +'%b %e %X ' 84 | fi 85 | } 86 | 87 | # 88 | # Tests if command is in the path 89 | # 90 | dc_is_command() { [ -x "$(command -v $1)" ] ;} 91 | 92 | # 93 | # Tests if pkgs are installed 94 | # 95 | dc_is_installed() { 96 | if dc_is_command apk; then 97 | ver_cmd="apk -e info" 98 | elif dc_is_command dpkg; then 99 | ver_cmd="dpkg -s" 100 | else 101 | dc_log 5 "No package manager found among: apk dpkg" 102 | fi 103 | for cmd in $@; do 104 | $ver_cmd $cmd > /dev/null 2>&1 || return 1 105 | done 106 | } 107 | 108 | # 109 | # Update loglevel 110 | # 111 | dc_update_loglevel() { 112 | loglevel=${1-$SYSLOG_LEVEL} 113 | if [ -n "$loglevel" ]; then 114 | dc_log 5 "Setting syslogd level=$loglevel." 115 | docker-service.sh "syslogd -nO- -l$loglevel $SYSLOG_OPTIONS" 116 | [ -n "$DOCKER_RUNFUNC" ] && sv restart syslogd 117 | fi 118 | } 119 | 120 | # 121 | # Print package versions 122 | # 123 | dc_pkg_versions() { 124 | local pkgs="$@" 125 | local len=$(echo $pkgs | tr " " "\n" | wc -L) 126 | local ver ver_cmd sed_flt 127 | local os=$(sed -rn 's/PRETTY_NAME="(.*)"/\1/p' /etc/os-release) 128 | local kern=$(uname -r) 129 | local host=$(uname -n) 130 | dc_log 5 $host $os $kern 131 | if dc_is_command apk; then 132 | ver_cmd="apk info -s" 133 | sed_flt="s/.*-(.*)-.*/\1/p" 134 | elif dc_is_command dpkg; then 135 | ver_cmd="dpkg -s" 136 | sed_flt="s/Version: ([[:digit:]]+\.[[:digit:]]+\.[[:digit:]]+).*/\1/p" 137 | else 138 | dc_log 5 "No package manager found among: apk dpkg" 139 | fi 140 | for pkg in $pkgs; do 141 | ver=$($ver_cmd $pkg 2> /dev/null | sed -rn "$sed_flt") 142 | if [ -n "$ver" ]; then 143 | printf "\t%-${len}s\t%s\n" $pkg $ver 144 | fi 145 | done 146 | } 147 | -------------------------------------------------------------------------------- /src/docker/bin/docker-config.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # docker-config.sh 4 | # 5 | # Defines common functions. Source this file from other scripts. 6 | # 7 | # Defined in Dockerfile: 8 | # DOCKER_UNLOCK_FILE 9 | # 10 | HOSTNAME=${HOSTNAME-$(hostname)} 11 | DOMAIN=${HOSTNAME#*.} 12 | TLS_KEYBITS=${TLS_KEYBITS-2048} 13 | TLS_CERTDAYS=${TLS_CERTDAYS-30} 14 | DOCKER_CRONTAB_FILE=${DOCKER_CRONTAB_FILE-/etc/crontab} 15 | DOCKER_CRONTAB_ENV=${DOCKER_CRONTAB_ENV-CRONTAB_ENTRY} 16 | 17 | # 18 | # general file manipulation commands, used both during build and run time 19 | # 20 | 21 | _escape() { echo "$@" | sed 's|/|\\\/|g' | sed 's|;|\\\;|g' | sed 's|\$|\\\$|g' | sed "s/""'""/\\\x27/g" ;} 22 | 23 | dc_modify() { 24 | local cfg_file=$1 25 | shift 26 | local lhs="$1" 27 | shift 28 | local eq= 29 | local rhs= 30 | if [ "$1" = "=" ]; then 31 | eq="$1" 32 | shift 33 | rhs="$(_escape $@)" 34 | else 35 | rhs="$(_escape $@)" 36 | fi 37 | dc_log 7 's/.*('"$lhs"'\s*'"$eq"'\s*)[^#]+(.*)/\1'"$rhs"' \2/g' $cfg_file 38 | sed -ri 's/.*('"$lhs"'\s*'"$eq"'\s*)[^#]+(.*)/\1'"$rhs"' \2/g' $cfg_file 39 | } 40 | 41 | dc_replace() { 42 | local cfg_file=$1 43 | local old="$(_escape $2)" 44 | local new="$(_escape $3)" 45 | dc_log 7 's/'"$old"'/'"$new"'/g' $cfg_file 46 | sed -i 's/'"$old"'/'"$new"'/g' $cfg_file 47 | } 48 | 49 | dc_addafter() { 50 | local cfg_file=$1 51 | local startline="$(_escape $2)" 52 | local new="$(_escape $3)" 53 | dc_log 7 '/'"$startline"'/!{p;d;}; $!N;s/\n\s*$/\n'"$new"'\n/g' $cfg_file 54 | sed -i '/'"$startline"'/!{p;d;}; $!N;s/\n\s*$/\n'"$new"'\n/g' $cfg_file 55 | } 56 | 57 | dc_comment() { 58 | local cfg_file=$1 59 | local string="$2" 60 | dc_log 7 '/^'"$string"'/s/^/#/g' $cfg_file 61 | sed -i '/^'"$string"'/s/^/#/g' $cfg_file 62 | } 63 | 64 | dc_uncommentsection() { 65 | local cfg_file=$1 66 | local startline="$(_escape $2)" 67 | dc_log 7 '/^'"$startline"'$/,/^\s*$/s/^#*//g' $cfg_file 68 | sed -i '/^'"$startline"'$/,/^\s*$/s/^#*//g' $cfg_file 69 | } 70 | 71 | dc_removeline() { 72 | local cfg_file=$1 73 | local string="$2" 74 | dc_log 7 '/'"$string"'.*/d' $cfg_file 75 | sed -i '/'"$string"'.*/d' $cfg_file 76 | } 77 | 78 | dc_uniquelines() { 79 | local cfg_file=$1 80 | dc_log 7 '$!N; /^(.*)\n\1$/!P; D' $cfg_file 81 | sed -ri '$!N; /^(.*)\n\1$/!P; D' $cfg_file 82 | } 83 | 84 | 85 | # 86 | # Persist dirs 87 | # 88 | 89 | # 90 | # Make sure that we have the required directory structure in place under 91 | # DOCKER_PERSIST_DIR. 92 | # 93 | dc_persist_mkdirs() { 94 | local dirs=$@ 95 | for dir in $dirs; do 96 | mkdir -p ${DOCKER_PERSIST_DIR}${dir} 97 | done 98 | } 99 | 100 | # 101 | # Make sure that we have the required directory structure in place under 102 | # DOCKER_PERSIST_DIR. 103 | # 104 | dc_persist_dirs() { 105 | local srcdirs="$@" 106 | local dstdir 107 | if [ -n "$DOCKER_PERSIST_DIR" ]; then 108 | for srcdir in $srcdirs; do 109 | mkdir -p "$srcdir" 110 | dstdir="${DOCKER_PERSIST_DIR}${srcdir}" 111 | mkdir -p "$(dirname $dstdir)" 112 | mv -f "$srcdir" "$(dirname $dstdir)" 113 | ln -sf "$dstdir" "$srcdir" 114 | dc_log 5 "Moving $srcdir to $dstdir" 115 | done 116 | fi 117 | } 118 | 119 | # 120 | # mv dir to persist location and leave a link to it 121 | # 122 | dc_persist_mvdirs() { 123 | local srcdirs="$@" 124 | if [ -n "$DOCKER_PERSIST_DIR" ]; then 125 | for srcdir in $srcdirs; do 126 | if [ -e "$srcdir" ]; then 127 | local dstdir="${DOCKER_PERSIST_DIR}${srcdir}" 128 | local dsthome="$(dirname $dstdir)" 129 | if [ ! -d "$dstdir" ]; then 130 | dc_log 5 "Moving $srcdir to $dstdir" 131 | mkdir -p "$dsthome" 132 | mv "$srcdir" "$dsthome" 133 | ln -sf "$dstdir" "$srcdir" 134 | else 135 | dc_log 4 "$srcdir already moved to $dstdir" 136 | fi 137 | else 138 | dc_log 4 "Cannot find $srcdir" 139 | fi 140 | done 141 | fi 142 | } 143 | 144 | # 145 | # Conditionally change owner of files. 146 | # -a all 147 | # -r readable 148 | # -w writable 149 | # -x executable 150 | # 151 | dc_cond_chown() { 152 | dc_log 7 "Called with args: $@" 153 | OPTIND=1 154 | local find_opts="! -perm -404" 155 | while getopts ":arwx" opts; do 156 | case "${opts}" in 157 | a) find_opts="";; 158 | r) find_opts="! -perm -404";; 159 | w) find_opts="! -perm -606";; 160 | x) find_opts="! -perm -505";; 161 | esac 162 | done 163 | shift $((OPTIND -1)) 164 | local user=$1 165 | shift 166 | if id $user > /dev/null 2>&1; then 167 | for dir in $@; do 168 | if [ -n "$(find $dir ! -user $user $find_opts -print -exec chown -h $user: {} \;)" ]; then 169 | dc_log 5 "Changed owner to $user for some files in $dir" 170 | fi 171 | done 172 | else 173 | dc_log 3 "User $user is unknown." 174 | fi 175 | } 176 | 177 | # 178 | # Append entry if it is not already there. If mode is -i then append before last line. 179 | # 180 | dc_cond_append() { 181 | local mode filename lineraw lineesc 182 | case $1 in 183 | -i) mode=i; shift;; 184 | -a) mode=a; shift;; 185 | *) mode=a;; 186 | esac 187 | filename=$1 188 | shift 189 | lineraw=$@ 190 | lineesc="$(echo $lineraw | sed 's/[\";/*]/\\&/g')" 191 | if [ -e "$filename" ]; then 192 | if [ -z "$(sed -n '/'"$lineesc"'/p' $filename)" ]; then 193 | dc_log 7 "dc_cond_append append: $mode $filename $lineraw" 194 | case $mode in 195 | a) echo "$lineraw" >> $filename;; 196 | i) sed -i "$ i\\$lineesc" $filename;; 197 | esac 198 | else 199 | dc_log 4 "Avoiding duplication: $filename $lineraw" 200 | fi 201 | else 202 | dc_log 7 "dc_cond_append create: $mode $filename $lineraw" 203 | echo "$lineraw" >> $filename 204 | fi 205 | } 206 | 207 | dc_cpfile() { 208 | local suffix=$1 209 | shift 210 | local cfs=$@ 211 | for cf in $cfs; do 212 | cp "$cf" "$cf.$suffix" 213 | done 214 | } 215 | 216 | dc_mvfile() { 217 | local suffix=$1 218 | shift 219 | local cfs=$@ 220 | for cf in $cfs; do 221 | mv "$cf" "$cf.$suffix" 222 | done 223 | } 224 | 225 | # 226 | # Prune PID files 227 | # 228 | dc_prune_pidfiles() { 229 | local dirs=$@ 230 | for dir in $dirs; do 231 | if [ -n "$(find -H $dir -type f -name "*.pid" -exec rm {} \; 2>/dev/null)" ]; then 232 | dc_log 5 "Removed orphan pid files in $dir" 233 | fi 234 | done 235 | } 236 | 237 | # 238 | # Setup crontab entries 239 | # 240 | dc_crontab_entries() { 241 | local entries="$(eval echo \${!$DOCKER_CRONTAB_ENV*})" 242 | for entry in $entries; do 243 | [ -z "${changed+x}" ] && local changed= && sed -i '/^\s*[0-9*]/d' $DOCKER_CRONTAB_FILE 244 | echo "${!entry}" >> $DOCKER_CRONTAB_FILE 245 | dc_log 5 "Added entry ${!entry} in $DOCKER_CRONTAB_FILE" 246 | done 247 | } 248 | 249 | # 250 | # TLS/SSL Certificates [openssl] 251 | # 252 | dc_tls_setup_selfsigned_cert() { 253 | local cert=$1 254 | local key=$2 255 | if ([ ! -s $cert ] || [ ! -s $key ]); then 256 | dc_log 5 "Setup self-signed TLS certificate for host $HOSTNAME" 257 | openssl genrsa -out $key $TLS_KEYBITS 258 | openssl req -x509 -utf8 -new -batch -subj "/CN=$HOSTNAME" \ 259 | -days $TLS_CERTDAYS -key $key -out $cert 260 | fi 261 | } 262 | 263 | # 264 | # Configuration Lock 265 | # 266 | dc_lock_config() { 267 | if [ -f "$DOCKER_UNLOCK_FILE" ]; then 268 | rm $DOCKER_UNLOCK_FILE 269 | dc_log 5 "Removing unlock file, locking the configuration." 270 | elif [ -n "$FORCE_CONFIG" ]; then 271 | dc_log 5 "Configuration update was forced, since we got FORCE_CONFIG=$FORCE_CONFIG" 272 | else 273 | dc_log 5 "No unlock file found, so not touching configuration." 274 | fi 275 | } 276 | 277 | # 278 | # true if there is no unlock file or FORCE_CONFIG is not empty 279 | # 280 | dc_is_unlocked() { [ -f "$DOCKER_UNLOCK_FILE" ] || [ -n "$FORCE_CONFIG" ] ;} 281 | -------------------------------------------------------------------------------- /src/docker/bin/docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | # set -x 3 | # 4 | # This script need to run as PID 1 allowing it to receive signals from docker 5 | # 6 | # Usage: add the folowing lines in Dockerfile 7 | # ENTRYPOINT ["docker-entrypoint.sh"] 8 | # CMD runsvdir -P ${SVDIR} 9 | # 10 | 11 | # 12 | # Variables 13 | # 14 | DOCKER_ENTRY_DIR=${DOCKER_ENTRY_DIR-/etc/docker/entry.d} 15 | DOCKER_EXIT_DIR=${DOCKER_EXIT_DIR-/etc/docker/exit.d} 16 | SVDIR=${SVDIR-/etc/service} 17 | 18 | # 19 | # Source common functions. 20 | # 21 | . docker-common.sh 22 | . docker-config.sh 23 | 24 | # 25 | # Functions 26 | # 27 | 28 | # 29 | # run_parts dir 30 | # Read and execute commands from files in the _current_ shell environment 31 | # 32 | run_parts() { 33 | for file in $(find $1 -type f -executable 2>/dev/null|sort); do 34 | dc_log 7 run_parts: executing $file 35 | . $file 36 | done 37 | } 38 | 39 | # 40 | # If the service is running, send it the TERM signal, and the CONT signal. 41 | # If both files ./run and ./finish exits, execute ./finish. 42 | # After it stops, do not restart the service. 43 | # 44 | sv_down() { sv down ${SVDIR}/* ;} 45 | 46 | # 47 | # SIGTERM handler 48 | # docker stop first sends SIGTERM, and after a grace period, SIGKILL. 49 | # use exit code 143 = 128 + 15 -- SIGTERM 50 | # 51 | term_trap() { 52 | dc_log 4 "Got SIGTERM, so shutting down." 53 | run_parts "$DOCKER_EXIT_DIR" 54 | sv_down 55 | exit 143 56 | } 57 | 58 | 59 | # 60 | # Stage 0) Register signal handlers and redirect stderr 61 | # 62 | 63 | exec 2>&1 64 | trap 'kill $!; term_trap' SIGTERM 65 | 66 | # 67 | # Stage 1) run all entry scripts in $DOCKER_ENTRY_DIR 68 | # 69 | 70 | run_parts "$DOCKER_ENTRY_DIR" 71 | 72 | # 73 | # Stage 2) run provided arguments in the background 74 | # Start services with: runsvdir -P ${SVDIR} 75 | # 76 | 77 | "$@" & 78 | 79 | # 80 | # Stage 3) wait forever so we can catch the SIGTERM 81 | # 82 | while true; do 83 | tail -f /dev/null & wait $! 84 | done 85 | -------------------------------------------------------------------------------- /src/docker/bin/docker-service.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # docker-service.sh 4 | # 5 | . docker-common.sh 6 | 7 | # use /etc/service if $SVDIR not already defined 8 | SVDIR=${SVDIR-/etc/service} 9 | DOCKER_SVLOG_DIR=${DOCKER_SVLOG_DIR-/var/log/sv} 10 | DOCKER_RUN_DIR=${DOCKER_RUN_DIR-/var/run} 11 | 12 | # 13 | # Define helpers 14 | # 15 | usage() { 16 | cat <<-!cat 17 | NAME 18 | docker-service.sh 19 | 20 | SYNOPSIS 21 | docker-service.sh [-d] [-f] [-h] [-l] [-n name] [-s file] [-q] command [args] 22 | 23 | OPTIONS 24 | -d default down 25 | -f remove lingering pid file at start up 26 | -h print this text and exit 27 | -l activate logging (svlogd) 28 | -n name use this name instead of command 29 | -s file source file 30 | -u user run command as this user 31 | -q send stdout and stderr to /dev/null 32 | 33 | EXAMPLES 34 | docker-service.sh "kopano-dagent -l" "-d kopano-grapi serve" 35 | "-q -s /etc/apache2/envvars apache2 -DFOREGROUND -DNO_DETACH -k start" 36 | 37 | !cat 38 | } 39 | 40 | base_name() { local base=${1##*/}; echo ${base%%.*} ;} 41 | 42 | pid_name() { 43 | local dir_name=${1%%-*} 44 | local pid_name=${1##*-} 45 | echo "${DOCKER_RUN_DIR}/${dir_name}/${pid_name}.pid" 46 | } 47 | 48 | add_opt() { 49 | if [ -z "$options" ]; then 50 | options=$1 51 | else 52 | options="$options,$1" 53 | fi 54 | } 55 | 56 | # 57 | # Define main function 58 | # 59 | 60 | init_service() { 61 | local redirstd= 62 | local clearpid= 63 | local sourcefile= 64 | local setuser= 65 | local sv_name cmd runsv_dir svlog_dir sv_log sv_down sv_force options 66 | dc_log 7 "Called with args $@" 67 | OPTIND=1 68 | while getopts ":dfhln:s:u:q" opts; do 69 | case "${opts}" in 70 | d) sv_down="down"; add_opt "down";; 71 | f) sv_force="force"; add_opt "force";; 72 | h) usage; exit;; 73 | l) sv_log="log"; add_opt "log";; 74 | n) sv_name="${OPTARG}"; add_opt "name";; 75 | s) sourcefile=". ${OPTARG}"; add_opt "source";; 76 | u) sv_user="${OPTARG}"; add_opt "user";; 77 | q) redirstd="exec >/dev/null"; add_opt "quiet";; 78 | esac 79 | done 80 | shift $((OPTIND -1)) 81 | cmd=$1 82 | cmd_path=$(which "$cmd") 83 | sv_name=${sv_name-$(base_name $cmd)} 84 | runsv_dir=$SVDIR/$sv_name 85 | svlog_dir=$DOCKER_SVLOG_DIR/$sv_name 86 | if [ -n "$sv_force" ]; then 87 | forcepid="$(echo rm -f $(pid_name $sv_name)*)" 88 | fi 89 | if [ -n "$sv_user" ]; then 90 | setuser="chpst -u $sv_name" 91 | fi 92 | shift 93 | if [ ! -z "$cmd_path" ]; then 94 | dc_log 5 "Setting up ($sv_name) options ($options) cmd ($cmd_path) args ($@)" 95 | mkdir -p $runsv_dir 96 | cat <<-!cat > $runsv_dir/run 97 | #!/bin/sh -e 98 | exec 2>&1 99 | $forcepid 100 | $redirstd 101 | $sourcefile 102 | exec $setuser $cmd_path $@ 103 | !cat 104 | chmod +x $runsv_dir/run 105 | if [ -n "$sv_down" ]; then 106 | touch $runsv_dir/down 107 | fi 108 | if [ -n "$sv_log" ]; then 109 | mkdir -p $runsv_dir/log $svlog_dir 110 | cat <<-!cat > $runsv_dir/log/run 111 | #!/bin/sh 112 | exec svlogd -tt $svlog_dir 113 | !cat 114 | chmod +x $runsv_dir/log/run 115 | fi 116 | else 117 | dc_log 4 "Cannot find command: $cmd" 118 | fi 119 | } 120 | 121 | # 122 | # run 123 | # 124 | 125 | for args in "$@" ; do 126 | init_service $args 127 | done 128 | -------------------------------------------------------------------------------- /src/docker/entry.d/20-docker-print-versions: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # 20-docker-print-versions 4 | # 5 | dc_pkg_versions asterisk 6 | -------------------------------------------------------------------------------- /src/docker/entry.d/50-docker-update-loglevel: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # 50-docker-update-loglevel 4 | # 5 | # If SYSLOG_LEVEL is not empty update syslog level 6 | # 7 | dc_update_loglevel 8 | -------------------------------------------------------------------------------- /src/notused/bin/astqueue.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Used for testing 3 | # 4 | 5 | source=${1-+15017122661} 6 | destexten=${2-+15558675310} 7 | message="${3-Local time is $(date)}" 8 | 9 | astspooldir=/var/spool/asterisk/outgoing 10 | message_context=dp_entry_text_in 11 | exten_context=dp_entry_answer 12 | 13 | maxretry=100 14 | retryint=30 15 | d_unique=$(date +%s) 16 | d_friendly=$(date +%T_%D) 17 | myrandom=$[ ( $RANDOM % 1000 ) + 1 ] 18 | 19 | filename="$destexten-$d_unique.$myrandom.call" 20 | 21 | cat <<-!cat > $astspooldir/$filename 22 | Channel: Local/${destexten}@${exten_context} 23 | CallerID: $source 24 | Maxretries: $maxretry 25 | RetryTime: $retryint 26 | Context: $message_context 27 | Extension: $destexten 28 | Priority: 1 29 | Setvar: MESSAGE(body)=$message 30 | Setvar: MESSAGE(to)=$destexten 31 | Setvar: MESSAGE(from)=$source 32 | !cat 33 | -------------------------------------------------------------------------------- /src/notused/bin/relpath: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | relpath() { 4 | there="$1" 5 | here="$(dirname $2)" 6 | root="" 7 | if [ ! -z $(echo "$there" | grep "^$here") ]; then 8 | root="./" 9 | else while [ -z $(echo "$there" | grep "^$here") ]; do 10 | here=${here%/*} 11 | root=${root}../ 12 | done 13 | fi 14 | echo $root$(echo "$there" | sed "s|^$here/||g") 15 | } 16 | 17 | # 18 | # run 19 | # 20 | 21 | relpath "$@" 22 | -------------------------------------------------------------------------------- /src/notused/bin/voipbl.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # This script creates an acl-blacklist.conf 4 | 5 | DOCKER_VOIPBL_FILE=${DOCKER_VOIPBL_FILE-/srv/var/spool/asterisk/acl/acl-blacklist.conf} 6 | 7 | wget -qO - http://www.voipbl.org/update/ | 8 | sed 's/^/deny=/' > $DOCKER_VOIPBL_FILE 9 | -------------------------------------------------------------------------------- /src/notused/entry.d/20-cronjob-voipbl: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # 20-cronjob-voipbl 4 | # 5 | # DO NOT USE: blacklist is too long and asterisk chokes 6 | # 7 | # If DOCKER_ACL_CONF contains #include $DOCKER_VOIPBL_CONF create a 8 | # VOIPBL cronjob 9 | # Need to run this before 50_seed-asterisk-conf 10 | # 11 | DOCKER_VOIPBL_URL=${DOCKER_VOIPBL_URL-http://www.voipbl.org/update/} 12 | DOCKER_VOIPBL_CONF=${DOCKER_VOIPBL_CONF-/var/spool/asterisk/acl/acl-blacklist.conf} 13 | DOCKER_VOIPBL_CRON=${DOCKER_VOIPBL_CRON-/etc/periodic/daily/voipbl} 14 | DOCKER_ACL_CONF=${DOCKER_ACL_CONF-$DOCKER_CONF_DIR/acl.conf} 15 | 16 | 17 | # 18 | # If there is no DOCKER_ACL_CONF file create it 19 | # 20 | if [ ! -f "$DOCKER_ACL_CONF" ]; then 21 | cat <<-!cat > $DOCKER_ACL_CONF 22 | [acl_blacklist] 23 | #include $DOCKER_VOIPBL_CONF 24 | !cat 25 | fi 26 | 27 | # 28 | # If DOCKER_ACL_CONF contains #include $DOCKER_VOIPBL_CONF then 29 | # create the DOCKER_VOIPBL_CRON cronjob which downloads the voip black list 30 | # into DOCKER_VOIPBL_FILE 31 | # If DOCKER_VOIPBL_FILE does not exist run cronjob now 32 | # 33 | if grep -q "#include $DOCKER_VOIPBL_CONF" $DOCKER_ACL_CONF; then 34 | mkdir -p $(dirname $DOCKER_VOIPBL_CONF) 35 | cat <<-!cat > $DOCKER_VOIPBL_CRON 36 | #!/bin/sh 37 | wget -qO - $DOCKER_VOIPBL_URL | 38 | sed 's/^[0-9]/deny = &/' > $DOCKER_VOIPBL_CONF 39 | !cat 40 | chmod a+x $DOCKER_VOIPBL_CRON 41 | if [ ! -f "$DOCKER_VOIPBL_CONF" ]; then 42 | $DOCKER_VOIPBL_CRON 43 | fi 44 | fi 45 | 46 | 47 | -------------------------------------------------------------------------------- /src/notused/php/amiupdateconfig.class.inc: -------------------------------------------------------------------------------- 1 | amiUpdate('Append',['Value-000000' => $ip]); 7 | * $ami->amiUpdate('Delete',['Match-000000' => $ip]); 8 | * $ami->amiUpdate('Delete'); 9 | 10 | */ 11 | require_once 'ami-class.php'; 12 | 13 | class AmiUpdateConfig extends \PHPAMI\Ami { 14 | const DEFAULT_CONF_FILE = '/etc/asterisk/autoban.conf'; 15 | const DEFAULT_CONF_VALS = [ 16 | 'Action' => 'UpdateConfig', 17 | 'ActionID' => 0, 18 | 'SrcFilename' => 'pjsip.conf', 19 | 'DstFilename' => 'pjsip.conf', 20 | 'Reload' => 'res_pjsip', 21 | 'PreserveEffectiveContext' => false, 22 | 'Action-000000' => 'Append', 23 | 'Cat-000000' => 'autoban', 24 | 'Var-000000' => 'deny', 25 | 'Value-000000' => null, 26 | 'Match-000000' => null, 27 | 'Line-000000' => null 28 | ]; 29 | public function __construct($config = null, array $optconfig = []) { 30 | parent::__construct($config,$optconfig); 31 | if (is_string($config) !== true) { 32 | $config = self::DEFAULT_CONF_FILE; 33 | } 34 | $this->config['amiaction'] = self::DEFAULT_CONF_VALS; 35 | if (file_exists($config) === true) { 36 | $config_ini = parse_ini_file($config,true); 37 | $this->config['amiaction'] = array_merge($this->config['amiaction'],$config_ini['amiaction']); 38 | } 39 | } 40 | public function amiUpdate($action,$args = []) { 41 | $parameters = array_merge($this->config['amiaction'], 42 | [ 'Action-000000' => $action ], $args); 43 | $res = $this->sendRequest('UpdateConfig',$parameters); 44 | if ($res['Response'] !== 'Success') { 45 | $this->log("$action failed.",self::LOG_ERROR); 46 | var_dump($parameters); 47 | var_dump($res); 48 | return false; 49 | } 50 | $this->log("$action Success",self::LOG_WARN); 51 | return true; 52 | } 53 | public function amiGetIp($parameters) { 54 | if (is_array($parameters) && array_key_exists('RemoteAddress',$parameters)) { 55 | $address = explode('/',$parameters['RemoteAddress']); 56 | $ip = $address[2]; 57 | return $ip; 58 | } else { 59 | return false; 60 | } 61 | } 62 | public function amiGetValue($parameters,$key) { 63 | if (is_array($parameters) && array_key_exists($key,$parameters)) { 64 | return $parameters[$key]; 65 | } else { 66 | return false; 67 | } 68 | } 69 | } 70 | ?> 71 | -------------------------------------------------------------------------------- /src/privatedial/README.md: -------------------------------------------------------------------------------- 1 | doc/privatedial.md -------------------------------------------------------------------------------- /src/privatedial/bin/minivm-send: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # /usr/local/bin/minivm-send 3 | # 4 | #set -x 5 | # 6 | script_name=$(basename $0) 7 | help() { 8 | cat<<-!cat 9 | DESCRIPTION 10 | This script simplifies smtps connections for sendmail 11 | USAGE 12 | $script_name -H [OPTIONS] < message 13 | OPTIONS 14 | -e Also send log messages to stdout 15 | -f For use in MAIL FROM 16 | -H Mail host/ip and port 17 | -h Print this text 18 | -P Choose from: smtp, smtps, tls, starttls 19 | -p Mail host authentication clear text password 20 | -u Mail host authentication username 21 | !cat 22 | } 23 | 24 | # 25 | # Arguments 26 | # 27 | sendmail_args=("-t") 28 | logger_args=("-t" "$script_name[$$]") 29 | while getopts ef:H:hP:p:u: option; do 30 | case "${option}" in 31 | e) logger_args+=("-s") ;; 32 | f) sendmail_args+=("-f" "${OPTARG}") ;; 33 | H) sendmail_host="${OPTARG}" ;; 34 | h) help; exit 0 ;; 35 | P) sendmail_proto="${OPTARG}" ;; 36 | p) sendmail_args+=("-ap${OPTARG}") ;; 37 | u) sendmail_args+=("-au${OPTARG}") ;; 38 | esac 39 | done 40 | 41 | # 42 | # Protocol 43 | # 44 | case "$sendmail_proto" in 45 | starttls) 46 | sendmail_args+=("-H" "openssl s_client -quiet -starttls smtp -connect $sendmail_host") ;; 47 | smtps|tls) 48 | sendmail_args+=("-H" "openssl s_client -quiet -connect $sendmail_host") ;; 49 | smtp|*) 50 | sendmail_args+=("-S" "$sendmail_host") ;; 51 | esac 52 | 53 | # 54 | # Send email 55 | # Asterisk pass the message via stdin to this script and 56 | # that stream is inherited here 57 | # 58 | sendmail_output=$(sendmail "${sendmail_args[@]}" 2>&1) 59 | sendmail_exitval=$? 60 | sendmail_cmd="sendmail ${sendmail_args[@]}" 61 | 62 | # 63 | # Log sendmail status and exit 64 | # 65 | if [ $sendmail_exitval -eq 0 ]; then 66 | logger "${logger_args[@]}" -p mail.info "INFO: Successfully sent mail ($sendmail_host)" 67 | else 68 | logger "${logger_args[@]}" -p mail.err "ERROR: Unable to send mail ($sendmail_host)" 69 | logger "${logger_args[@]}" -p mail.err "$sendmail_cmd" 70 | logger "${logger_args[@]}" -p mail.err "$sendmail_output" 71 | fi 72 | exit $sendmail_exitval 73 | -------------------------------------------------------------------------------- /src/privatedial/config/extensions.conf: -------------------------------------------------------------------------------- 1 | ;-------------------------------- PrivateDial ---------------------------------- 2 | ; Filename: extensions.conf 3 | ; This file is an Asterisk configuration file, part of the PrivateDial suite 4 | ; It is loaded by the pbx_config.so module. 5 | ; 6 | 7 | ;-------------------------------- general -------------------------------------- 8 | ; This category loads include files. 9 | ; 10 | [general] 11 | #tryinclude extensions_local.conf 12 | 13 | ;-------------------------------- globals -------------------------------------- 14 | ; Global options are defined in include files. 15 | ; 16 | [globals](+) 17 | ;-------------------------------- sms 18 | ; Full path to SMS app 19 | APP_SMS = /usr/local/bin/websms 20 | 21 | ;-------------------------------- entry contexts ------------------------------- 22 | ; Calls enter the dialplan in one of these entries 23 | ; Local additions to these entries are defined in include files. 24 | ; 25 | [dp_entry_call_inout](+) 26 | include => dp_lookup 27 | include => dp_ivr_recgreet 28 | include => dp_call_inout 29 | 30 | [dp_entry_call_in](+) 31 | include => dp_lookup 32 | include => dp_call_in 33 | 34 | [dp_entry_text_inout](+) 35 | include => dp_lookup 36 | include => dp_text_inout 37 | 38 | [dp_entry_text_in](+) 39 | include => dp_lookup 40 | include => dp_text_in 41 | 42 | [dp_entry_answer](+) 43 | include => dp_lookup 44 | include => dp_answer 45 | 46 | 47 | ;-------------------------------- action contexts ------------------------------ 48 | 49 | ;-------------------------------- dp_lookup 50 | ; hints are placed here see hint_exten in pjsip_wizard.conf 51 | ; 52 | ; Rewrite E.123 53 | ; More than 6 characters and first is 0; assume national number so add + and country code 54 | ; More than 6 characters and first is 1-9, assume international number so add + 55 | ; 56 | [dp_lookup] 57 | exten => _0ZXXXXXX.,1,Goto(${CONTEXT},+${GLOBAL(CONTRY_CODE)}${EXTEN:1},1) 58 | exten => _ZXXXXXX.,1,Goto(${CONTEXT},+${EXTEN},1) 59 | 60 | [dp_call_inout] 61 | exten => _[+0-9].,1,NoOp() 62 | same => n,Gosub(sub_dial_term,s,1(${HINT})) 63 | same => n,Gosub(sub_voicemail,s,1(${HINT})) 64 | same => n,Gosub(sub_dial_trunk,${EXTEN},1(${HINT})) 65 | same => n,Hangup() 66 | 67 | [dp_call_in] 68 | exten => _[+0-9].,1,NoOp() 69 | same => n,Gosub(sub_dial_term,s,1(${HINT})) 70 | same => n,Gosub(sub_voicemail,s,1(${HINT})) 71 | same => n,Hangup() 72 | 73 | [dp_text_inout] 74 | exten => _[+0-9].,1,NoOp() 75 | same => n,Gosub(sub_rewrite_from,s,1) 76 | same => n,Gosub(sub_text_term,s,1(${HINT})) 77 | same => n,Gosub(sub_text_trunk,${EXTEN},1(${HINT})) 78 | same => n,Hangup() 79 | 80 | [dp_text_in] 81 | exten => _[+0-9].,1,NoOp() 82 | same => n,Gosub(sub_decode_body,s,1) 83 | same => n,Gosub(sub_text_term,s,1(${HINT})) 84 | same => n,Hangup() 85 | 86 | [dp_answer] 87 | ;DEVICE_STATE = UNKNOWN | NOT_INUSE | INUSE | BUSY | INVALID | UNAVAILABLE | RINGING | RINGINUSE | ONHOLD 88 | exten => _[+0-9].,1,Goto(dev-${DEVICE_STATE(${HINT})}) 89 | same => n(dev-NOT_INUSE),NoOp() 90 | same => n(dev-INUSE),NoOp() 91 | same => n(dev-RINGING),NoOp() 92 | same => n(dev-RINGINUSE),NoOp() 93 | same => n(dev-ONHOLD),NoOp() 94 | same => n(dev-UNAVAILABLE),NoOp() 95 | same => n,Answer() 96 | same => n(dev-UNKNOWN),NoOp() 97 | same => n(dev-INVALID),NoOp() 98 | same => n,Hangup() 99 | 100 | [dp_hangup] 101 | exten => _[+0-9].,1,Hangup() 102 | 103 | ;-------------------------------- dp_ivr_recgreet 104 | ; Record personal voicemail greeting messages interactive voice response 105 | ; 106 | [dp_ivr_recgreet] 107 | exten => _X,1,Verbose(3, "New caller, ${CALLERID(num)} dialed into the IVR.") 108 | same => n,Set(endpoint=${CHANNEL(endpoint)}) 109 | same => n,Set(mailboxes=${PJSIP_ENDPOINT(${endpoint},mailboxes)}) 110 | same => n,Gotoif(${ISNULL(${endpoint})}?hangup:) 111 | same => n,Answer() 112 | same => n,Set(mailpath=${MINIVMACCOUNT(${mailboxes}:path)}) 113 | same => n,Verbose(3, "Mailbox: ${mailboxes}, Creating new path: ${mailpath}") 114 | same => n,System(mkdir -p ${mailpath}) 115 | same => n,Goto(exten-${EXTEN}) 116 | 117 | same => n(exten-1),Verbose(3, "Caller ${endpoint} Record the temporary greeting.") 118 | same => n,MinivmAccMess(${mailboxes},t) 119 | same => n,Goto(exten-9) 120 | 121 | same => n(exten-2),Verbose(3, "Caller ${endpoint} Record the unavailable greeting.") 122 | same => n,MinivmAccMess(${mailboxes},u) 123 | same => n,Goto(exten-9) 124 | 125 | same => n(exten-3),Verbose(3, "Caller ${endpoint} Record the busy greeting.") 126 | same => n,MinivmAccMess(${mailboxes},b) 127 | same => n,Goto(exten-9) 128 | 129 | same => n(exten-4),Verbose(3, "Caller ${endpoint)} Record the account name.") 130 | same => n,MinivmAccMess(${mailboxes},n) 131 | same => n,Goto(exten-9) 132 | 133 | same => n(exten-5),Verbose(3, "MusicOnHold") 134 | same => n,MusicOnHold() 135 | same => n,Goto(exten-9) 136 | 137 | same => n(exten-6),Verbose(3, "Echo") 138 | same => n,Playback(demo-echotest) 139 | same => n,Echo() 140 | same => n,Playback(demo-echodone) 141 | same => n,Playback(vm-goodbye) 142 | same => n,Goto(exten-9) 143 | 144 | same => n(exten-9),Background(basic-pbx-ivr-main) 145 | same => n,WaitExten(10) 146 | same => n,Background(basic-pbx-ivr-main) 147 | same => n(hangup),Hangup() 148 | 149 | exten => i,1,Playback(option-is-invalid) 150 | same => n,Goto(9,exten-9) 151 | 152 | exten => t,1,Playback(are-you-still-there) 153 | same => n,Goto(9,exten-9) 154 | 155 | ;-------------------------------- subroutines ---------------------------------- 156 | ; Syntax: Gosub(context,extension,priority(${ARG1},${ARG3},${ARG4})) 157 | ; 158 | 159 | ;-------------------------------- sub_dial_term 160 | ; Dial user subroutine for PJSIP 161 | ; Dial all pjsip contacts of an endpoint 162 | ; Usage: Gosub(sub_dial_term,s,1(${HINT})) 163 | ; ${ARG1} - pjsip tech/endpoint: ${HINT}, eg PJSIP/myuser 164 | ; 165 | ; Implementation details 166 | ; Dial all registered contacts of the endpoint. Return if there are none. 167 | ; Do not hangup, so that we can pass the call to sub_voicemail 168 | ; 169 | [sub_dial_term] 170 | exten => s,1,Set(LOCAL(endpoint)=${ARG1:6}) ; strip PJSIP/ from endpoint name 171 | same => n,Gotoif(${ISNULL(${endpoint})}?return:) 172 | same => n,Set(LOCAL(contacts)=${PJSIP_DIAL_CONTACTS(${endpoint})}) 173 | same => n,Gotoif(${ISNULL(${contacts})}?return:) 174 | same => n,Verbose(2, "Call from CID: ${CALLERID(all)}, dialing all contacts of endpoint: ${endpoint}.") 175 | same => n,Dial(${contacts}${GLOBAL(DIAL_TIMEOUT)}) 176 | same => n(return),Return() 177 | 178 | ;-------------------------------- sub_dial_trunk 179 | ; Dial trunk subroutine for PJSIP 180 | ; Usage: Gosub(sub_dial_trunk,${EXTEN},1(${HINT})) 181 | ; ${EXTEN} - Extension, eg 0046735698294 182 | ; ${ARG1} - pjsip tech/endpoint: ${HINT}, eg PJSIP/myuser 183 | ; 184 | ; Implementation details 185 | ; Dial EXTEN using ${TRUNK_ENDPOINT} which is defined in endpoint using set_var. 186 | ; In keeping with coding style, do not hangup. 187 | ; The channel variable ${TRUNK_ENDPOINT} holds the SIP trunk endpoint and 188 | ; needs be set on endpoints. This allows mutiple SIP trunk endpoints to be used. 189 | ; 190 | [sub_dial_trunk] 191 | exten => _[+0-9].,1,NoOP(Dialing out originating CID ${CALLERID(all)}) 192 | same => n,Dial(PJSIP/${EXTEN}@${TRUNK_ENDPOINT}) 193 | same => n(return),Return() 194 | 195 | ;-------------------------------- sub_voicemail 196 | ; Voicemail subroutine: 197 | ; Usage: Gosub(sub_voicemail,s,1(${HINT})) 198 | ; ${ARG1} - pjsip tech/endpoint: ${HINT}, eg PJSIP/myuser 199 | ; ${ARG2} - Status of the call: ${DIALSTATUS}, one of: 200 | ; CHANUNAVAIL, CONGESTION, BUSY, NOANSWER, ANSWER, CANCEL, DONTCALL, TORTURE 201 | ; 202 | ; Implementation details 203 | ; Depending on DIALSTATUS direct caller to voice mail and then hangup. 204 | ; Only return from here if callee has no endpoint, in which case we assume 205 | ; callee is external. 206 | ; MiniVM does not work unless there is a mailbox directory, so always create it. 207 | ; 208 | [sub_voicemail] 209 | exten => s,1,Set(LOCAL(endpoint)=${ARG1:6}) ; strip PJSIP/ from endpoint name 210 | same => n,Gotoif(${ISNULL(${endpoint})}?return:) 211 | same => n,Set(LOCAL(mailboxes)=${PJSIP_ENDPOINT(${endpoint},mailboxes)}) 212 | same => n,Gotoif(${ISNULL(${mailboxes})}?mailbox-NULL:) 213 | same => n,Set(mailpath=${MINIVMACCOUNT(${mailboxes}:path)}) 214 | same => n,System(mkdir -p ${mailpath}) 215 | same => n,Goto(dial-${DIALSTATUS}) 216 | same => n(return),Return() 217 | 218 | same => n(mailbox-NULL),NoOp() 219 | same => n,Playback(please-try-call-later) 220 | same => n,Hangup() 221 | 222 | same => n(dial-CHANUNAVAIL),NoOp() 223 | same => n(dial-CONGESTION),NoOp() 224 | same => n(dial-NOANSWER),NoOp() 225 | same => n(dial-),NoOp() 226 | ; same => n,MinivmGreet(${mailboxes},u) 227 | same => n,Goto(mail-RECORD) 228 | 229 | same => n(dial-BUSY),NoOp() 230 | ; same => n,MinivmGreet(${mailboxes},b) 231 | 232 | same => n(mail-RECORD),NoOp() 233 | same => n,MinivmGreet(${mailboxes},s) 234 | same => n,MinivmRecord(${mailboxes}${GLOBAL(VOICEMAIL_RECGAINDB)}) 235 | ; If the caller hangs up after the recording, 236 | ; the only way to send the email and clean up is to execute in the "h" 237 | same => n,Hangup() 238 | 239 | same => n(dial-CANCEL),NoOp() 240 | same => n(dial-DONTCALL),NoOp() 241 | same => n(dial-TORTURE),NoOp() 242 | same => n(dial-ANSWER),NoOp() 243 | same => n,Hangup() 244 | 245 | ; send email and clean up 246 | exten => h,1,Goto(recd-${MVM_RECORD_STATUS}) 247 | 248 | same => n(recd-SUCCESS),NoOp() 249 | same => n(recd-USEREXIT),NoOp() 250 | same => n,MinivmNotify(${mailboxes}${GLOBAL(VOICEMAIL_TEMPLATE)}) 251 | same => n,Goto(mail-${MVM_NOTIFY_STATUS}) 252 | 253 | same => n(mail-SUCCESS),NoOp() 254 | same => n,MinivmDelete() 255 | same => n,Goto(recd-) 256 | 257 | same => n(mail-FAILED),NoOp() 258 | same => n(recd-FAILED),NoOp() 259 | same => n,Playback(vm-incorrect-mailbox) 260 | 261 | same => n(recd-),NoOp() 262 | same => n,Hangup() 263 | 264 | ;-------------------------------- sub_rewrite_from 265 | ; Rewrite the MESSAGE(from) subroutine for PJSIP 266 | ; Usage: Gosub(sub_rewrite_from,s,1) 267 | ; 268 | ; Implementation details 269 | ; Some clients sets the MESSAGE(from) to endpoint@domain 270 | ; But we want the MESSAGE(from) to be set to the endpoint extension, ie, its hint 271 | ; so assume the MESSAGE(from) is of the form endpoint@domain and 272 | ; try to look up the endpoint's hint. 273 | ; if successful rewrite the MESSAGE(from) 274 | ; 275 | [sub_rewrite_from] 276 | exten => s,1,Verbose(3,"Original message from ${MESSAGE(from)}") 277 | same => n,Set(LOCAL(endpoint)=${CUT(MESSAGE(from),@,1)}) 278 | same => n,Set(LOCAL(endpoint)=${CUT(endpoint,:,2)}) 279 | same => n,Gotoif(${ISNULL(${endpoint})}?return:) 280 | same => n,Set(LOCAL(from)=${PJSIP_ENDPOINT(${endpoint},@hint_exten)}) 281 | same => n,Gotoif(${ISNULL(${from})}?return:) 282 | same => n,Set(MESSAGE(from)=${from}) 283 | same => n,Set(WEBSMS_INDEX=${PJSIP_ENDPOINT(${endpoint},WEBSMS_INDEX)}) 284 | same => n,Verbose(3,"Updated message from ${MESSAGE(from)}, WEBSMS_INDEX: ${WEBSMS_INDEX}") 285 | same => n(return),Return() 286 | 287 | ;-------------------------------- sub_decode_body 288 | ; URL decode the MESSAGE(body) subroutine 289 | ; Usage: Gosub(sub_decode_body,s,1) 290 | ; 291 | ; Implementation details 292 | ; If MESSAGE_ENCODE = rfc3986 then the MESSAGE(body) is URL encoded 293 | ; (using RFC3986 which supersedes RFC2396) in the callfile, so decode it here. 294 | ; 295 | [sub_decode_body] 296 | exten => s,1,Gotoif(${ISNULL(${MESSAGE_ENCODE})}?return:) 297 | same => n,GotoIf($[ ${MESSAGE_ENCODE} != rfc3986 ]?return:) 298 | same => n,Verbose(3,"Original message body ${MESSAGE(body)}") 299 | same => n,Set(MESSAGE(body)=${URIDECODE(${MESSAGE(body)})}) 300 | same => n,Verbose(3,"Updated message body ${MESSAGE(body)}") 301 | same => n(return),Return() 302 | 303 | ;-------------------------------- sub_text_term 304 | ; Instant messaging subroutine for PJSIP 305 | ; Send message to all pjsip contacts of an endpoint 306 | ; Usage: Gosub(sub_text_term,s,1(${HINT})) 307 | ; ${EXTEN} - Extension, eg 0735698294 308 | ; ${ARG1} - pjsip tech/endpoint: ${HINT}, eg PJSIP/myuser 309 | ; 310 | ; Implementation details 311 | ; PJSIP_DIAL_CONTACTS() return all contact URLs separated by an "&", eg: 312 | ; PJSIP/myuser/sip:myuser@10.10.10.100:61863;option=value&PJSIP/myuser/sip:myuser@217.103.237.202:35678;option=value 313 | ; Within a While() loop, we cut this string at the "&" using ${SHIFT(contacts,&):6}. 314 | ; The ":6" strips off the initial "PJSIP/". 315 | ; MessageSend() needs the URL to be slightly reformatted, eg 316 | ; pjsip:myuser/sip:myuser@10.10.10.100:61863;option=value 317 | ; so we simply prepend with "pjsip:" 318 | ; MessageSend() accepts but ignores any provided options 319 | ; 320 | [sub_text_term] 321 | exten => s,1,Verbose(2, "Text User, To: ${MESSAGE(to)}, Hint: ${ARG1}, From: ${MESSAGE(from)}, CID: ${CALLERID(all)}, Body: ${MESSAGE(body)}") 322 | same => n,Set(LOCAL(endpoint)=${CUT(ARG1,/,2)}) 323 | same => n,Gotoif(${ISNULL(${endpoint})}?return:) 324 | same => n,Set(LOCAL(contacts)=${PJSIP_DIAL_CONTACTS(${endpoint})}) 325 | same => n,While($["${SET(contact=${SHIFT(contacts,&):6})}" != ""]) 326 | same => n,MessageSend(pjsip:${contact},${MESSAGE(from)}) 327 | same => n,Verbose(2, "Send status is ${MESSAGE_SEND_STATUS}") 328 | same => n,EndWhile 329 | same => n(return),Return() 330 | 331 | ;-------------------------------- sub_text_trunk 332 | ; Instant messaging subroutine for PJSIP 333 | ; Send message to trunk via curl 334 | ; Usage: Gosub(sub_text_trunk,${EXTEN},1(${HINT})) 335 | ; ${EXTEN} - Extension, eg 0735698294 336 | ; ${ARG1} - pjsip tech/endpoint: ${HINT}, eg PJSIP/myuser 337 | ; 338 | ; Implementation details 339 | ; To distinguish between local and external numbers we use the HINT 340 | ; If the HINT is null we assume that it is an external number 341 | ; AGISTATUS is one of SUCCESS, FAILURE, NOTFOUND, HANGUP 342 | ; The channel variable ${WEBSMS_INDEX} can be set on endpoints to select which 343 | ; configuration to use. 344 | ; 345 | [sub_text_trunk] 346 | exten => _[+0-9].,1,Verbose(2, "Text Out, To: ${MESSAGE(to)}, Hint: ${ARG1}, From: ${MESSAGE(from)}, CID: ${CALLERID(all)}, Body: ${MESSAGE(body)}, using Index: ${WEBSMS_INDEX}") 347 | same => n,Set(LOCAL(endpoint)=${CUT(ARG1,/,2)}) 348 | same => n,Gotoif(${ISNULL(${endpoint})}?:return) 349 | same => n,AGI(${GLOBAL(APP_SMS)},${EXTEN},${MESSAGE(from)},${QUOTE(${MESSAGE(body)})},${WEBSMS_INDEX}) 350 | same => n,Verbose(2, "AGI status is ${AGISTATUS}") 351 | same => n(return),Return() 352 | -------------------------------------------------------------------------------- /src/privatedial/config/extensions_local.conf: -------------------------------------------------------------------------------- 1 | ;-------------------------------- PrivateDial ---------------------------------- 2 | ; Filename: extensions_local.conf 3 | ; This file is an Asterisk configuration file, part of the PrivateDial suite 4 | ; It is included by the extensions.conf file. 5 | ; 6 | 7 | ;-------------------------------- globals -------------------------------------- 8 | ; include file providing dialing texting options used in context globals 9 | ; 10 | 11 | ;-------------------------------- dialing 12 | 13 | [globals] 14 | CONTRY_CODE = 46 15 | DIAL_TIMEOUT =,30 16 | 17 | ;-------------------------------- voice mail 18 | 19 | VOICEMAIL_TEMPLATE =,en_US_email 20 | VOICEMAIL_RECGAINDB =,g(12) 21 | 22 | ;-------------------------------- entries -------------------------------------- 23 | ; Calls enter the dialplan in one of these entries 24 | ; 25 | 26 | [dp_entry_call_inout] 27 | 28 | [dp_entry_call_in] 29 | 30 | [dp_entry_text_inout] 31 | 32 | [dp_entry_text_in] 33 | 34 | [dp_entry_answer] 35 | -------------------------------------------------------------------------------- /src/privatedial/config/minivm.conf: -------------------------------------------------------------------------------- 1 | ;-------------------------------- PrivateDial ---------------------------------- 2 | ; Filename: minivm.conf 3 | ; This file is an Asterisk configuration file, part of the PrivateDial suite. 4 | ; It is loaded by the app_minivm.so module. 5 | ; 6 | 7 | [general] 8 | ; DESCRIPTION 9 | ; This script simplifies smtps connections for sendmail 10 | ; USAGE 11 | ; minivm-send -H [OPTIONS] < message 12 | ; OPTIONS 13 | ; -e Also send log messages to stdout 14 | ; -f For use in MAIL FROM 15 | ; -H Mail host/ip and port 16 | ; -h Print this text 17 | ; -P Choose from: smtp, smtps, tls, starttls 18 | ; -p Mail host authentication clear text password 19 | ; -u Mail host authentication username 20 | ; 21 | mailcmd = minivm-send -H mx.example.com:587 -P starttls -u username -p password -f voicemail-noreply@example.com 22 | 23 | format = wav49 24 | 25 | ;logfile = /var/log/asterisk/minivm.log 26 | ;minmessage = 3 ; asterisk crashes if this is set? :O 27 | 28 | [template-en_US_email] 29 | fromemail = voicemail-noreply@example.com 30 | fromaddress = voicemail 31 | messagebody = ${MVM_CALLERID} left you a ${MVM_DUR} long message on ${MVM_DATE} UTC, see attachment. 32 | subject = [voicemail] from ${MVM_CALLERID} 33 | charset = iso-8859-1 34 | attachmedia = yes 35 | dateformat = %A, %d %B %Y at %H:%M:%S 36 | locale = sv_SE 37 | -------------------------------------------------------------------------------- /src/privatedial/config/pjsip.conf: -------------------------------------------------------------------------------- 1 | ;-------------------------------- PrivateDial ---------------------------------- 2 | ; Filename: pjsip.conf 3 | ; This file is an Asterisk configuration file, part of the PrivateDial suite. 4 | ; It is loaded by the chan_pjsip.so module. 5 | ; 6 | 7 | ;-------------------------------- global --------------------------------------- 8 | 9 | [global] 10 | type = global 11 | user_agent = Platform PBX 12 | 13 | ;-------------------------------- includes ------------------------------------- 14 | 15 | #tryinclude pjsip_transport.conf 16 | -------------------------------------------------------------------------------- /src/privatedial/config/pjsip_endpoint.conf: -------------------------------------------------------------------------------- 1 | ;-------------------------------- PrivateDial ---------------------------------- 2 | ; Filename: pjsip_endpoint.conf 3 | ; This file is an Asterisk configuration file, part of the PrivateDial suite. 4 | ; It is included by the pjsip_wizard.conf file 5 | ; 6 | 7 | ;-------------------------------- templates ------------------------------------ 8 | 9 | [w_term:mydoe](!,w_term_io) 10 | endpoint/set_var = TRUNK_ENDPOINT=itsp:mydoe 11 | endpoint/set_var = WEBSMS_INDEX= 12 | 13 | ;-------------------------------- sip trunks ----------------------------------- 14 | 15 | [itsp:mydoe](w_trunk) 16 | remote_hosts = sip.mydoe.com 17 | sends_auth = yes 18 | sends_registrations = yes 19 | outbound_auth/username = username 20 | outbound_auth/password = password 21 | 22 | ;-------------------------------- sip terminals -------------------------------- 23 | 24 | [john.doe](w_term:mydoe) 25 | hint_exten = +12025550160 26 | endpoint/callerid = John Doe <+12025550160> 27 | endpoint/mailboxes = john.doe@example.com 28 | inbound_auth/username = john.doe 29 | inbound_auth/password = password 30 | 31 | [jane.doe](w_term:mydoe) 32 | hint_exten = +12025550183 33 | endpoint/callerid = Jane Doe <+12025550183> 34 | endpoint/mailboxes = jane.doe@example.com 35 | inbound_auth/username = jane.doe 36 | inbound_auth/password = password 37 | -------------------------------------------------------------------------------- /src/privatedial/config/pjsip_transport.conf: -------------------------------------------------------------------------------- 1 | ;-------------------------------- PrivateDial ---------------------------------- 2 | ; Filename: pjsip_transport.conf 3 | ; This file is an Asterisk configuration file, part of the PrivateDial suite 4 | ; It is included by the pjsip.conf file. 5 | ; 6 | 7 | ;-------------------------------- transports ----------------------------------- 8 | 9 | [t_wan](!) 10 | type = transport 11 | bind = 0.0.0.0:5060 12 | domain = example.com 13 | external_signaling_address = sip.example.com 14 | external_media_address = sip.example.com 15 | tos = cs3 16 | cos = 3 17 | 18 | [udp](t_wan) 19 | protocol = udp 20 | 21 | [tcp](t_wan) 22 | protocol = tcp 23 | 24 | [tls](t_wan) 25 | bind = 0.0.0.0:5061 26 | cert_file = /etc/ssl/asterisk/cert.pem 27 | priv_key_file = /etc/ssl/asterisk/priv_key.pem 28 | protocol = tls 29 | method=tlsv1_2 30 | -------------------------------------------------------------------------------- /src/privatedial/config/pjsip_wizard.conf: -------------------------------------------------------------------------------- 1 | ;-------------------------------- PrivateDial ---------------------------------- 2 | ; Filename: pjsip_wizard.conf 3 | ; This file is an Asterisk configuration file, part of the PrivateDial suite. 4 | ; It is loaded by the res_pjsip_config_wizard.so module. 5 | ; 6 | 7 | ;-------------------------------- templates ------------------------------------ 8 | 9 | [_qos](!) 10 | endpoint/tos_audio = ef 11 | endpoint/tos_video = af41 12 | endpoint/cos_audio = 5 13 | endpoint/cos_video = 4 14 | 15 | [_nat](!) 16 | endpoint/rewrite_contact = yes 17 | endpoint/direct_media = no 18 | endpoint/rtp_symmetric = yes 19 | 20 | [_sdes](!) 21 | endpoint/media_encryption_optimistic = yes 22 | endpoint/media_encryption = sdes 23 | 24 | [_dtls](!) 25 | endpoint/media_encryption_optimistic = yes 26 | endpoint/media_encryption = dtls 27 | endpoint/dtls_auto_generate_cert = yes 28 | 29 | [_term](!) 30 | accepts_auth = yes 31 | accepts_registrations = yes 32 | endpoint/allow = !all,ulaw,h263p,h263,h264 33 | endpoint/bind_rtp_to_media_address = yes 34 | aor/max_contacts = 10 35 | aor/remove_existing = yes 36 | aor/minimum_expiration = 120 37 | aor/qualify_frequency = 60 38 | 39 | [_trunk](!) 40 | endpoint/allow = !all,ulaw 41 | endpoint/allow_subscribe = no 42 | aor/qualify_frequency = 60 43 | 44 | [_hint](!) 45 | has_hint = yes 46 | hint_context = dp_lookup 47 | 48 | [_in](!) 49 | endpoint/context = dp_entry_call_in 50 | endpoint/message_context = dp_entry_text_in 51 | 52 | [_inout](!) 53 | endpoint/context = dp_entry_call_inout 54 | endpoint/message_context = dp_entry_text_inout 55 | 56 | [w_term_io](!,_nat,_qos,_sdes,_hint,_term,_inout) 57 | type = wizard 58 | 59 | [w_term_i](!,_nat,_qos,_sdes,_hint,_term,_in) 60 | type = wizard 61 | 62 | [w_trunk](!,_nat,_qos,_trunk,_in) 63 | type = wizard 64 | 65 | ;-------------------------------- includes ------------------------------------- 66 | 67 | #tryinclude pjsip_endpoint.conf 68 | -------------------------------------------------------------------------------- /src/privatedial/doc/privatedial.md: -------------------------------------------------------------------------------- 1 | # PrivateDial 2 | 3 | PrivateDial is a suite of [Asterisk configuration files](https://wiki.asterisk.org/wiki/display/AST/Asterisk+Configuration+Files). This configuration is tailored to residential use cases, supporting the capabilities of mobile smart phones, that is, voice, video, instant messaging or SMS, and voice mail delivered by email. 4 | 5 | It uses the [PJSIP](https://www.pjsip.org/) [channel driver](https://wiki.asterisk.org/wiki/display/AST/Configuring+res_pjsip) and therefore natively support simultaneous connection of several soft-phones to each user account/endpoint. 6 | 7 | The underlying design idea is to separate the dial plan functionality from the user data. To achieve this all user specific data has been pushed out from the main `extensions.conf` file. 8 | 9 | ## Features 10 | 11 | Feature list follows below 12 | 13 | - Calls and SMS between local endpoints. 14 | - ITSP originating (incoming) SIP voice calls. 15 | - ITSP termination (outgoing) SIP voice call. 16 | - WebSMS; SMS to and from ITSP. 17 | - [MiniVoiceMail](https://wiki.asterisk.org/wiki/display/AST/Asterisk+16+Application_MinivmRecord) 18 | 19 | 20 | ## Configuration files 21 | 22 | The suite of Asterisk configuration files making up PrivateDial is summarized below. 23 | 24 | ### Configuration files overview 25 | 26 | The configuration files making up PrivateDial are tabulated below. 27 | 28 | | File name | Description | 29 | | --------------------- | ------------------------------------------------------------ | 30 | | extensions.conf | The dial plan, defining the data flow of calls and messages | 31 | | extensions_local.conf | Use case specific global variables used in extensions.conf | 32 | | minivm.conf | Define mail sever URL and authentication credentials which voice mail email notifications will be sent | 33 | | pjsip.conf | Use case specific global variables used by the PJSIP driver | 34 | | pjsip_transport.conf | Defines SIP transport, protocol, port, host URL | 35 | | pjsip_wizard.conf | Defines templates for sip trunk and soft-phone endpoints | 36 | | pjsip_endpoint.conf | Defines sip trunk and soft-phone endpoints | 37 | 38 | When configuring the asterisk sever the following files often needs to be updated: `pjsip_transport.conf` and `minivm.conf`. The remaining task is, once the severer has been configured, to add and maintain sip trunk and soft-phone endpoints, which is kept in `pjsip_endpoint.conf`. 39 | 40 | ## Usage 41 | 42 | ### SIP Trunk 43 | 44 | PJSIP endpoints are defined using the [PJSIP Wizard](https://wiki.asterisk.org/wiki/display/AST/PJSIP+Configuration+Wizard) in the configuration file `pjsip_endpoint.conf`. For convenience the template, `w_trunk` has been defined in `pjsip_wizard.conf`. 45 | 46 | Add an endpoint entry in `pjsip_endpoint.conf` based on the setup instructions provided by your trunk provider. This entry also hold your authentication credentials. 47 | 48 | `pjsip_endpoint.conf` 49 | 50 | ```ini 51 | [itsp:mydoe](w_trunk) 52 | remote_hosts = sip.mydoe.com 53 | sends_auth = yes 54 | sends_registrations = yes 55 | outbound_auth/username = username 56 | outbound_auth/password = password 57 | ``` 58 | 59 | With some ITSP SIP servers you need to explicitly state which transport to use. In such case add the folowing to the section above; `transport = udp`, for UDP. 60 | 61 | ### SIP Users 62 | 63 | PJSIP endpoints are defined using the [PJSIP Wizard](https://wiki.asterisk.org/wiki/display/AST/PJSIP+Configuration+Wizard) in the configuration file `pjsip_endpoint.conf`. For convenience the template, `w_term_io` and `w_term_i` has been defined in `pjsip_wizard.conf`. 64 | 65 | Add an endpoint entry in `pjsip_endpoint.conf` for each user. Each user can simultaneously connect with several soft-phones, using the same account. 66 | 67 | `pjsip_endpoint.conf` 68 | 69 | ```ini 70 | [w_term:mydoe](!,w_term_io) 71 | endpoint/set_var = TRUNK_ENDPOINT=itsp:mydoe 72 | endpoint/set_var = WEBSMS_INDEX= 73 | 74 | [john.doe](w_term:mydoe) 75 | hint_exten = +12025550160 76 | endpoint/callerid = John Doe <+12025550160> 77 | endpoint/mailboxes = john.doe@example.com 78 | inbound_auth/username = john.doe 79 | inbound_auth/password = password 80 | ``` 81 | 82 | You also need to configure WebSMS for SMS to work, see separate documentation. 83 | 84 | ### Outgoing SMTP email server 85 | 86 | PrivateDial use [MiniVoiceMail](https://wiki.asterisk.org/wiki/display/AST/Asterisk+16+Application_MinivmRecord) to deliver voice mail messages via email with attached sound files. For this to work a separate SMTP email server need to have been set up. This can for example be achieved by using the image [mlan/postfix](https://hub.docker.com/repository/docker/mlan/postfix). With a functional email server, configure MiniVM to connect to it by providing its URL and authentication credentials in `minivm.conf` 87 | 88 | `minivm.conf` 89 | 90 | ```ini 91 | [general] 92 | mailcmd = minivm-send -H mx.example.com:587 -P starttls -u username -p password -f voicemail-noreply@example.com 93 | ``` 94 | 95 | ### SIP Networking 96 | 97 | Here we describe 3 aspects of SIP networking that often needs to be addressed. Communication with devices on local networks, Intrusion prevention using non-standard ports. Privacy using encryption. 98 | 99 | #### Network Address Translation (NAT) 100 | 101 | When communicating with devices on local networks a more elaborate mechanism using (NAT) needs to be configured allowing server and client locate each other. Assuming that the SIP server has the following external URL; `sip.example.com`, make sure to update `pjsip_transport.conf` so it includes the snippet below. 102 | 103 | `pjsip_transport.conf` 104 | 105 | ```ini 106 | [t_wan](!) 107 | type = transport 108 | bind = 0.0.0.0:5060 109 | domain = example.com 110 | external_signaling_address = sip.example.com 111 | external_media_address = sip.example.com 112 | ``` 113 | 114 | #### Custom SIP ports 115 | 116 | When using non-standard ports the amount of attacks drop significantly, so it might be considered whenever practical. When changing port numbers they need to be updated both for docker and asterisk. To exemplify, assume we want to use 5560 for UDP and TCP and 5561 for TLS, in which case we update the configuration in two places: 117 | 118 | - docker/docker-compose, eg, `docker run -p "5560-5561:5560-5561" -p"5560:5560/udp" ...` 119 | - asterisk transport in `pjsip_transport.conf` 120 | 121 | `pjsip_transport.conf` 122 | 123 | ```ini 124 | [t_wan](!) 125 | type = transport 126 | bind = 0.0.0.0:5560 127 | ... 128 | [tls](t_wan) 129 | bind = 0.0.0.0:5561 130 | ... 131 | ``` 132 | 133 | #### TLS Certificate and key 134 | 135 | To enable encryption of both the session and data packages (TLS and SDES SRTP) a [TLS/SSL server certificate](https://en.wikipedia.org/wiki/Public_key_certificate) and key are needed. If the certificate and key do not exist when the container starts a [self-signed certificate](https://en.wikipedia.org/wiki/Self-signed_certificate) and private key are automatically generated. The default file names for these are defined below. Should the certificate and key be available be other means they can be copied to the container using this names. If other file names are referred also update their names in `pjsip_transport.conf`. 136 | 137 | `pjsip_transport.conf` 138 | 139 | ```ini 140 | [tls](t_wan) 141 | cert_file = /etc/ssl/asterisk/cert.pem 142 | priv_key_file = /etc/ssl/asterisk/priv_key.pem 143 | ``` 144 | 145 | There is also a mechanism to use ACME lets encrypt certificates, which also use these file names. 146 | 147 | ## Implementation 148 | 149 | ### Dialplan 150 | 151 | The PrivateDial has its dialplan contexts organized in 3 levels. The entry, action and subroutine contexts. A SIP event will trigger the execution of the PrivateDial dial plan staring on one of the entry contexts. The entry contexts include some of the action contexts, and the action contexts call the subroutines. 152 | 153 | #### Entry context 154 | 155 | The entry contexts are used to grant more access to users calling or texting as compared to external trunk calls or texts. All entry context start with including the `dp_lookup` context so that extension hints are always available. 156 | 157 | ```ini 158 | [dp_entry_call_inout](+) 159 | include => dp_lookup 160 | include => dp_ivr_recgreet 161 | include => dp_call_inout 162 | 163 | [dp_entry_call_in](+) 164 | include => dp_lookup 165 | include => dp_call_in 166 | 167 | [dp_entry_text_inout](+) 168 | include => dp_lookup 169 | include => dp_text_inout 170 | 171 | [dp_entry_text_in](+) 172 | include => dp_lookup 173 | include => dp_text_in 174 | 175 | [dp_entry_answer](+) 176 | include => dp_lookup 177 | include => dp_answer 178 | ``` 179 | 180 | #### Action context 181 | 182 | The action contexts calls the subroutines. Most subroutines use the `${HINT}` channel variable to identify the endpoint so `${EXTEN}` is set to the special `s`. Each subroutine is called in its turn and the call is not hung up until all subroutine calls has been made. 183 | 184 | ```ini 185 | [dp_lookup] 186 | ; hints are placed here see hint_exten in pjsip_wizard.conf 187 | exten => _0ZXXXXXX.,1,Goto(${CONTEXT},+${GLOBAL(CONTRY_CODE)}${EXTEN:1},1) 188 | exten => _ZXXXXXX.,1,Goto(${CONTEXT},+${EXTEN},1) 189 | 190 | [dp_call_inout] 191 | exten => _[+0-9].,1,NoOp() 192 | same => n,Gosub(sub_dial_term,s,1(${HINT})) 193 | same => n,Gosub(sub_voicemail,s,1(${HINT})) 194 | same => n,Gosub(sub_dial_trunk,${EXTEN},1(${HINT})) 195 | same => n,Hangup() 196 | 197 | [dp_call_in] 198 | exten => _[+0-9].,1,NoOp() 199 | same => n,Gosub(sub_dial_term,s,1(${HINT})) 200 | same => n,Gosub(sub_voicemail,s,1(${HINT})) 201 | same => n,Hangup() 202 | 203 | [dp_text_inout] 204 | exten => _[+0-9].,1,NoOp() 205 | same => n,Gosub(sub_rewrite_from,s,1) 206 | same => n,Gosub(sub_text_term,s,1(${HINT})) 207 | same => n,Gosub(sub_text_trunk,${EXTEN},1(${HINT})) 208 | same => n,Hangup() 209 | 210 | [dp_text_in] 211 | exten => _[+0-9].,1,NoOp() 212 | same => n,Gosub(sub_decode_body,s,1) 213 | same => n,Gosub(sub_text_term,s,1(${HINT})) 214 | same => n,Hangup() 215 | 216 | [dp_answer] 217 | exten => _[+0-9].,1,Goto(dev-${DEVICE_STATE(${HINT})}) 218 | same => n(dev-NOT_INUSE),NoOp() 219 | same => n(dev-INUSE),NoOp() 220 | same => n(dev-RINGING),NoOp() 221 | same => n(dev-RINGINUSE),NoOp() 222 | same => n(dev-ONHOLD),NoOp() 223 | same => n(dev-UNAVAILABLE),NoOp() 224 | same => n,Answer() 225 | same => n(dev-UNKNOWN),NoOp() 226 | same => n(dev-INVALID),NoOp() 227 | same => n,Hangup() 228 | ``` 229 | 230 | #### Subroutine context 231 | 232 | The file `extension.conf` include some in line documentation of the subroutines. 233 | 234 | Subroutines does not hang up but instead returns the data flow to the calling context. (CHECK with sub_voicemail). 235 | 236 | Most subroutines use the `${HINT}` channel variable to identify the endpoint so `${EXTEN}` is set to the special `s`. 237 | 238 | When calling and texting endpoints an attempts are made to contact all contacts of the endpoints, such that for inbound calls all registered contacts (smart-pones) will ring and also receive inbound SMS. 239 | 240 | ### Presence 241 | 242 | Asterisk support [presence state](https://wiki.asterisk.org/wiki/display/AST/Presence+State) an indicator that conveys the state of an endpoint to other endpoints. But, unfortunately, presence is only supported for Sangama/Digium phones, not for softphones. Consequentely, no attmpt has been made to support presence state in PrivateDial. 243 | -------------------------------------------------------------------------------- /src/websms/README.md: -------------------------------------------------------------------------------- 1 | doc/websms.md -------------------------------------------------------------------------------- /src/websms/config/websms.conf: -------------------------------------------------------------------------------- 1 | ; 2 | ; websms.conf 3 | ; 4 | ; This file hold user customization of http sms 5 | ; originating and termination service. 6 | ; 7 | 8 | [websms] 9 | url_host = https://api.example.com 10 | url_path = /sms/send/ 11 | auth_user = user 12 | auth_secret = secret 13 | 14 | [websmsd] 15 | url_path [1] = /hook/1 16 | resp_ack [1] = "SMS received on /hook/1" 17 | url_path [2] = /hook/2 18 | resp_ack [2] = "SMS received on /hook/2" 19 | 20 | [astqueue] 21 | channel_context = dp_entry_answer 22 | context = dp_entry_text_in 23 | -------------------------------------------------------------------------------- /src/websms/doc/websms.conf.sample: -------------------------------------------------------------------------------- 1 | ; 2 | ; websms.conf.sample 3 | ; 4 | ; This file hold user customization of the websms originate and termination. 5 | ; This is a php ini file. Luckily its syntax is similar to other asterisk conf files. 6 | ; "yes" and "no" have to be within quotation marks otherwise they will be 7 | ; interpreted as Boolean. 8 | ; 9 | 10 | ;[websms] 11 | ;auth_method = basic ;eg "plain", "zadarma" method to authenticate sms request 12 | ;auth_secret = "" ;authentication password/secret 13 | ;auth_user = "" ;authentication username/key 14 | ;key_body = Body ;http POST key name holding the sms message 15 | ;key_from = From ;http POST key name holding sms originating phone number 16 | ;key_secret = "" ;http POST key name holding the password/secret when using auth_method=plain 17 | ;key_to = To ;http POST key name holding sms destination phone number 18 | ;key_user = "" ;http POST key name holding the username/key when using auth_method=plain 19 | ;resp_check = "" ;http POST key=value to check, eg "status=success" 20 | ;url_host = http://localhost ;scheme and host, eg https://api.expmple.com 21 | ;url_path = / ;complete url will be 22 | ;val_numform = "" ;eg E164; omit leading + and 0 in phone numbers, or E123 23 | ;val_static = "" ;comma separated key=value pairs to add 24 | ;val_unicode = "" ;set to "UCS-2" to limit Unicode characters to U+FFFF 25 | 26 | ;[websmsd] 27 | ;key_body = Body ;http POST key name holding the sms message 28 | ;key_echo = "" ;some ITSP test that the client respond by echoing it value, eg "zd_echo" 29 | ;key_from = From ;http POST key name holding sms origination phone number 30 | ;key_to = To ;http POST key name holding sms destination phone number 31 | ;prox_addr = 172.16.0.0/12,192.168.0.0/16 ;Trust "prox_header" from these IPs, eg 10.0.0.0/8 32 | ;prox_header = HTTP_X_FORWARDED_FOR ;Behind a proxy this header hold the real client IP 33 | ;remt_addr = "" ;if defined, only listed addrs are accepted, eg 185.45.152.42,3.104.90.0/24,3.1.77.0/24 34 | ;resp_ack = "" ;report success, eg, "" 35 | ;url_path = "" ;if defined, only listed URIs are accepted, eg /,/trunk1. URIs must start with "/". 36 | 37 | ;[astqueue] 38 | ;outgoingdir = /var/spool/asterisk/outgoing ;directory where asterisk picks up call files 39 | ;stagingdir = /var/spool/asterisk/staging ;create call file here and then move to outgoing 40 | ;waittime = 45 ;how many seconds to wait for an answer before the call fails 41 | ;maxretries = 0 ;number of retries before failing. 0 = don't retry if fails 42 | ;retrytime = 300 ;how many seconds to wait before retry 43 | ;archive = "no" ;"yes" = save call file to /var/spool/asterisk/outgoing_done 44 | ;channel_context = default ;dialplan context to answer the call, ie set up the channel 45 | ;context = default ;dialplan context to handle the sms 46 | ;priority = 1 ;dialplan priority to handle the sms 47 | ;message_encode = rfc3986 ;only single line allowed in call file so url-encoding message 48 | -------------------------------------------------------------------------------- /src/websms/entry.d/30-websms-migrate: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # 30-websms-migrate 4 | # 5 | # Try to make configs compatible with new version if MIGRATE_CONFIG is defined. 6 | # Set MIGRATE_CONFIG=1 2 3 to list of fixes or MIGRATE_CONFIG=all to attempt all fixes. 7 | # 8 | APP_SMS_OLD=/usr/share/php7/websms.php 9 | APP_SMS_NEW=/usr/local/bin/websms 10 | 11 | websms_apply_migrate_fixes() { 12 | local applied 13 | if [ -n "$MIGRATE_CONFIG" ]; then 14 | for fix in ${MIGRATE_CONFIG/all/1}; do # list all fixes here 15 | case $fix in 16 | 1) # Make sure websms application can be found 17 | websms_migrate_php FIX 18 | ;; 19 | *) fix= ;; 20 | esac 21 | if [ -n "$fix" ]; then 22 | applied="$applied $fix" 23 | fi 24 | done 25 | if [ -n "$applied" ]; then 26 | dc_log 5 "Applied fixes;$applied to configuration since MIGRATE_CONFIG=$MIGRATE_CONFIG" 27 | fi 28 | fi 29 | } 30 | 31 | websms_notify_compat_issues() { 32 | websms_migrate_php 33 | } 34 | 35 | websms_migrate_php() { 36 | if grep "^APP_SMS = $APP_SMS_OLD" $DOCKER_CONF_DIR/extensions.conf >/dev/null; then 37 | if [ "$1" = "FIX" ]; then 38 | sed -i 's|'$APP_SMS_OLD'|'$APP_SMS_NEW'|g' $DOCKER_CONF_DIR/extensions.conf 39 | dc_log 5 "Setting APP_SMS = $APP_SMS_NEW in $DOCKER_CONF_DIR/extensions.conf" 40 | else 41 | dc_log 4 "WEBSMS might not work. Found incompatible APP_SMS = $APP_SMS_OLD in $DOCKER_CONF_DIR/extensions.conf. Use APP_SMS = $APP_SMS_NEW or set MIGRATE_CONFIG=all to fix." 42 | fi 43 | fi 44 | } 45 | # 46 | # run 47 | # 48 | websms_apply_migrate_fixes 49 | websms_notify_compat_issues 50 | -------------------------------------------------------------------------------- /src/websms/entry.d/50-websms-update-port: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # 50-websms-update-port 4 | # 5 | # 6 | # 7 | source docker-common.sh 8 | 9 | if $(echo $WEBSMSD_PORT | grep -qE "^[0-9]+$"); then 10 | dc_log 5 "websmsd will listen to port=$WEBSMSD_PORT." 11 | docker-service.sh "-n websmsd php -S 0.0.0.0:$WEBSMSD_PORT -t $DOCKER_PHP_DIR websmsd.php" 12 | else 13 | dc_log 5 "websmsd will be disabled since there was no valid port=$WEBSMSD_PORT." 14 | touch ${SVDIR}/websmsd/down 15 | fi 16 | -------------------------------------------------------------------------------- /src/websms/php/astqueue.class.inc: -------------------------------------------------------------------------------- 1 | '/var/spool/asterisk/outgoing', 22 | 'stagingdir' => '/var/spool/asterisk/staging', 23 | 'filemode' => null, 24 | 'fileowner' => null, 25 | 'waittime' => 45, 26 | 'maxretries' => 0, 27 | 'retrytime' => 300, 28 | 'archive' => 'no', 29 | 'channel_context' => 'default', 30 | 'context' => 'default', 31 | 'priority' => 1, 32 | 'message_encode' => 'rfc3986', 33 | ]; 34 | const CALLFILE_SYNTAX = [ 35 | 'Channel' => 'channel', 36 | 'CallerID' => 'callid', 37 | 'WaitTime' => 'config', 38 | 'MaxRetries' => 'config', 39 | 'RetryTime' => 'config', 40 | 'Account' => 'config', 41 | 'Application' => 'config', 42 | 'Data' => 'config', 43 | 'Context' => 'config', 44 | 'Extension' => 'to', 45 | 'Priority' => 'config', 46 | 'Archive' => 'config', 47 | ]; 48 | const CALLFILE_SETVAR = [ 49 | 'MESSAGE(to)' => 'to', 50 | 'MESSAGE(from)' => 'from', 51 | 'MESSAGE(body)' => 'body', 52 | 'MESSAGE_ENCODE' => 'config', 53 | ]; 54 | private $config; 55 | public $debug = false; 56 | public function __construct($config = null, array $optconfig = []) { 57 | if (is_string($config) !== true) { 58 | $config = self::DEFAULT_CONF_FILE; 59 | } 60 | $this->config['astqueue'] = self::DEFAULT_CONF_VALS; 61 | if (file_exists($config) === true) { 62 | $config_ini = parse_ini_file($config,true); 63 | $this->config['astqueue'] = array_merge($this->config['astqueue'], 64 | $config_ini['astqueue']); 65 | } 66 | foreach ($optconfig as $var => $val) { 67 | $this->config['astqueue'][$var] = $val; 68 | } 69 | $this->mkdir($this->config['astqueue']['outgoingdir']); 70 | $this->mkdir($this->config['astqueue']['stagingdir']); 71 | } 72 | /*-------------------------------------------------------------------------- 73 | Generate call file with message 74 | @param array $data, eg ['to'=>'+1...60','from'=>'+1...25','body'=>'Hello!'] 75 | $data['to'] : The SMS destination 76 | $data['from'] : The SMS originator 77 | $data['body'] : The SMS message 78 | @return boolean true if successful otherwise false 79 | */ 80 | public function text($data) { 81 | if (isset($data['to']) && isset($data['from']) && isset($data['body'])) { 82 | $lines = $this->gen_lines(Self::CALLFILE_SYNTAX,$data); 83 | $lines .= $this->gen_lines(Self::CALLFILE_SETVAR,$data,true); 84 | $basename = $this->gen_basename($data); 85 | return $this->write($basename, $lines); 86 | } else { 87 | return false; 88 | } 89 | } 90 | /*-------------------------------------------------------------------------- 91 | Generate call file with call 92 | @param string $exten callee phone number, eg '+12025550160' 93 | @param string $callid caller phone number, eg '+12025550125' 94 | @return boolean true if successful otherwise false 95 | */ 96 | public function call($exten,$callid) { 97 | if (isset($exten) && isset($callid)) { 98 | $data = ['to'=>$exten,'from'=>$callid]; 99 | $lines = $this->gen_lines(Self::CALLFILE_SYNTAX,$message); 100 | $basename = $this->gen_basename($data); 101 | return $this->write($basename, $lines); 102 | } else { 103 | return false; 104 | } 105 | } 106 | /*-------------------------------------------------------------------------- 107 | @param array $syntax, eg ['Channel'=>'channel','CallerID'=>'callid',...] 108 | @param array $data, eg ['to'=>'+1...60','from'=>'+1...25','body'=>'Hello!'] 109 | @param boolean $setvar use setvar syntax if true 110 | @return string lines 111 | */ 112 | private function gen_lines($syntax,$data,$setvar = false) { 113 | $return = null; 114 | $to = $data['to']; 115 | $from = $data['from']; 116 | $body = $data['body']; 117 | foreach ($syntax as $key => $type) { 118 | switch ($type) { 119 | case 'to': 120 | $return .= $this->gen_line($key,$to,$setvar); 121 | break; 122 | case 'from': 123 | $return .= $this->gen_line($key,$from,$setvar); 124 | break; 125 | case 'body': 126 | $return .= $this->gen_line($key,$this->gen_body($body),true); 127 | break; 128 | case 'channel': 129 | $return .= $this->gen_line($key,$this->gen_channel($to)); 130 | break; 131 | case 'callid': 132 | $return .= $this->gen_line($key,$this->gen_callid($from)); 133 | break; 134 | case 'config': 135 | $return .= $this->gen_line($key,$this->gen_config($key),$setvar); 136 | break; 137 | } 138 | } 139 | return $return; 140 | } 141 | /*-------------------------------------------------------------------------- 142 | @param string $key, eg 'archive' 143 | @param string $value, eg 'yes' 144 | @param boolean $setvar use setvar syntax if true 145 | @return string or null, eg 'archive: yes' 146 | */ 147 | private function gen_line($key,$value,$setvar = false) { 148 | if (!isset($key) || !isset($value)) return null; 149 | if ($setvar) { 150 | return sprintf('setvar: %s=%s', $key, $value).PHP_EOL; 151 | } else { 152 | return sprintf('%s: %s', $key, $value).PHP_EOL; 153 | } 154 | } 155 | /*-------------------------------------------------------------------------- 156 | Get config value 157 | @param string $key, eg 'waittime' 158 | @return mixed config value, eg 45 159 | */ 160 | public function gen_config($key) { 161 | $key_lower = strtolower($key); 162 | if(array_key_exists($key_lower,$this->config['astqueue'])) { 163 | return $this->config['astqueue'][$key_lower]; 164 | } else { 165 | return null; 166 | } 167 | } 168 | /*-------------------------------------------------------------------------- 169 | Generate channel string 170 | @param string $exten callee phone number, eg '+12025550160' 171 | @return string channel 172 | */ 173 | private function gen_channel($exten) { 174 | return sprintf('Local/%s@%s',$exten,$this->config['astqueue']['channel_context']); 175 | } 176 | /*-------------------------------------------------------------------------- 177 | Generate callid string 178 | @param string $number caller phone number, eg '+12025550183' 179 | @param string $display caller display, eg 'Jane Doe' 180 | @return string callid, eg '"Jane Doe" <+12025550183>' 181 | */ 182 | private function gen_callid($number,$display = '') { 183 | return sprintf('"%s" <%s>',$display,$number); 184 | } 185 | /*-------------------------------------------------------------------------- 186 | Generate call file basename. 187 | Format: ..<3 digit random>.call 188 | @param array $data['to'] callee phone number, eg '+12025550160' 189 | @return void 190 | */ 191 | private function gen_basename($data) { 192 | return sprintf("%s.%d.%03d.call",$data['to'],time(),rand(0,999)); 193 | } 194 | /*-------------------------------------------------------------------------- 195 | The message cannot span multiple lines in an Asterisk call file. To work 196 | around that we encode the message (RFC3986, which supersedes RFC2396). 197 | @param string $string to escape 198 | @return string escaped string 199 | */ 200 | private function gen_body($string) { 201 | if ($this->config['astqueue']['message_encode'] === 'rfc3986') { 202 | return rawurlencode($string); 203 | } else { 204 | return $string; 205 | } 206 | } 207 | /*-------------------------------------------------------------------------- 208 | Create new call file in the staging directory. 209 | Using staging directory to avoid Asterisk reading an unfinished file. 210 | Move the call file to the outgoing directory, so that Asterisk pick it up. 211 | @param string $basename 212 | @param string $lines file contents 213 | @return boolean true if successful otherwise false 214 | */ 215 | private function write($basename, $lines) { 216 | $stagingfile = $this->config['astqueue']['stagingdir'].'/'.$basename; 217 | $outgoingfile = $this->config['astqueue']['outgoingdir'].'/'.$basename; 218 | if (file_put_contents($stagingfile,$lines) === false) { 219 | trigger_error("unable to open call file ($stagingfile)", E_USER_WARNING); 220 | return false; 221 | } 222 | if (rename($stagingfile, $outgoingfile) === false) { 223 | trigger_error("unable to move file ($stagingfile) to file ($outgoingfile)", 224 | E_USER_WARNING); 225 | return false; 226 | } 227 | return true; 228 | } 229 | /*-------------------------------------------------------------------------- 230 | @param string $dir full path to directory 231 | @return void 232 | */ 233 | private function mkdir($dir) { 234 | if (!is_dir($dir) && !mkdir($dir)) 235 | trigger_error("unable create directory ($dir)", E_USER_WARNING); 236 | } 237 | /*-------------------------------------------------------------------------- 238 | NOT WORKING DUE TO PHP NOT HAVING ROOT ASSESS 239 | */ 240 | private function chmod() { 241 | $outgoingfile = $this->config['astqueue']['outgoingdir'].'/'.$this->name; 242 | if (!empty($this->config['astqueue']['filemode'])) { 243 | chmod($outgoingfile, octdec($this->config['astqueue']['filemode'])); 244 | } 245 | if (!empty($this->config['astqueue']['fileowner'])) { 246 | chmod($outgoingfile, chown($this->config['astqueue']['fileowner'])); 247 | } 248 | } 249 | /*-------------------------------------------------------------------------- 250 | Print variable if $debug or $this->debug is true 251 | @param mixed $var 252 | @param boolean $debug 253 | @return void 254 | */ 255 | public function debug($var, $debug = false) { 256 | if($debug || $this->debug) { 257 | var_dump($var); 258 | } 259 | } 260 | } 261 | ?> 262 | -------------------------------------------------------------------------------- /src/websms/php/websms.class.inc: -------------------------------------------------------------------------------- 1 | 6 | listen() 7 | permitted() 8 | echo() 9 | post_mess($post) 10 | answer($status) 11 | 12 | 13 | 14 | query($args) 15 | args_mess($args) 16 | curl_init() 17 | curl_data($post) 18 | curl_auth($post) 19 | curl_send() 20 | check($response) 21 | */ 22 | class Websms { 23 | const DEFAULT_CONF_FILE = '/etc/asterisk/websms.conf'; 24 | const DEFAULT_CONF_VALS = [ 25 | 'websms' => [ 26 | 'auth_method' => "basic", 27 | 'auth_secret' => null, 28 | 'auth_user' => null, 29 | 'key_body' => "Body", 30 | 'key_from' => "From", 31 | 'key_secret' => null, 32 | 'key_to' => "To", 33 | 'key_user' => null, 34 | 'resp_check' => null, 35 | 'url_host' => "http://localhost", 36 | 'url_path' => "/", 37 | 'val_numform' => null, 38 | 'val_static' => null, 39 | 'val_unicode' => null, 40 | ], 41 | 'websmsd' => [ 42 | 'key_body' => "Body", 43 | 'key_echo' => null, 44 | 'key_from' => "From", 45 | 'key_to' => "To", 46 | 'prox_addr' => "172.16.0.0/12,192.168.0.0/16", 47 | 'prox_header' => "HTTP_X_FORWARDED_FOR", 48 | 'remt_addr' => null, 49 | 'resp_ack' => "", 50 | 'url_path' => null, 51 | ] 52 | ]; 53 | private $config; 54 | public $index; 55 | private $curl; 56 | public $debug = false; 57 | public function __construct($config = null, array $optconfig = []) { 58 | if (is_string($config) !== true) { 59 | $config = self::DEFAULT_CONF_FILE; 60 | } 61 | $this->config = self::DEFAULT_CONF_VALS; 62 | if (file_exists($config) === true) { 63 | $config_ini = parse_ini_file($config,true); 64 | if (!empty($config_ini['websmsd'])) 65 | $this->config['websmsd'] = array_merge($this->config['websmsd'], 66 | $config_ini['websmsd']); 67 | if (!empty($config_ini['websms'])) 68 | $this->config['websms'] = array_merge($this->config['websms'], 69 | $config_ini['websms']); 70 | } 71 | foreach ($optconfig as $var => $val) { 72 | $this->config['websms'][$var] = $val; 73 | } 74 | } 75 | /*-------------------------------------------------------------------------- 76 | Read the post header data. 77 | @return mixed array message or boolean false 78 | */ 79 | public function rx_query() { 80 | if ($this->identify_remote()) { 81 | $this->if_echo_exit(); 82 | return $this->message_data(); 83 | } else return false; 84 | } 85 | /*-------------------------------------------------------------------------- 86 | Respond with a status message. 87 | @param Boolean $status 88 | @param array $message to, from, and body. 89 | @return void 90 | */ 91 | public function ack_query($status = true, $message) { 92 | if ($status) { 93 | trigger_error(sprintf( 94 | "Inbound SMS accepted, to (%s) from (%s)", $message['to'], 95 | $message['from']), E_USER_NOTICE); 96 | echo $this->config('websmsd','resp_ack'); 97 | } 98 | } 99 | /*-------------------------------------------------------------------------- 100 | Send SMS HTTP POST query. We require 3 arguments; to, from, body. 101 | args[0] is set to script name so drop this. 102 | 103 | Outline 104 | Parse arguments. 105 | Build http POST query. 106 | Setup curl for POST query. 107 | Setup curl's authentication method. 108 | Send POST query. 109 | Check response. 110 | */ 111 | public function tx_query($args) { 112 | unset($args[0]); 113 | $query_data = $this->query_data($args); 114 | if (!empty($query_data)) { 115 | $this->val_unicode($query_data); 116 | $this->val_numform($query_data); 117 | $this->val_static($query_data); 118 | $this->curl_init(); 119 | $this->curl_auth($query_data); 120 | $this->curl_data($query_data); 121 | $resp_data = $this->curl_send(); 122 | return $this->resp_check($resp_data); 123 | } else { 124 | return false; 125 | } 126 | } 127 | /*-------------------------------------------------------------------------- 128 | If remt_addr is set, match remote host and request uri, set index. 129 | If remt_addr isn't set but uri is, match request uri, set index. 130 | @return Boolean true if matched, otherwise false 131 | */ 132 | private function identify_remote() { 133 | $allow = true; 134 | $match_addr = $this->config('websmsd','remt_addr',true); 135 | $remt_addr = $this->remt_addr(); 136 | $request_uri = $_SERVER['REQUEST_URI']; 137 | if(isset($match_addr)) { 138 | $allow = false; 139 | if (is_array($match_addr)) { 140 | foreach ($match_addr as $index => $value) { 141 | if($this->match_cidr($remt_addr,$value)) { 142 | $allow = true; 143 | $this->index = $index; 144 | } 145 | } 146 | } else { 147 | if($this->match_cidr($remt_addr,$match_addr)) $allow = true; 148 | } 149 | if($allow) trigger_error( 150 | sprintf("REMOTE_ADDR (%s) is allowed, using index (%s)", 151 | $remt_addr, $this->index), E_USER_NOTICE); 152 | else trigger_error( 153 | sprintf("REMOTE_ADDR (%s) is NOT allowed", 154 | $remt_addr), E_USER_WARNING); 155 | if($this->debug) trigger_error(file_get_contents('php://input') . print_r($_SERVER, TRUE)); 156 | } 157 | /* 158 | Wait until now to call $this->config('websmsd','uri') since 159 | $this->index might have been set above. 160 | */ 161 | $match_uri = $this->config('websmsd','url_path',true); 162 | if($allow && isset($match_uri)) { 163 | $allow = false; 164 | if (is_array($match_uri)) { 165 | foreach ($match_uri as $index => $value) { 166 | if($this->match_uri($value)) { 167 | $allow = true; 168 | $this->index = $index; 169 | } 170 | } 171 | } else { 172 | if($this->match_uri($match_uri)) $allow = true; 173 | } 174 | if($allow) trigger_error( 175 | sprintf("REQUEST_URI (%s) is allowed, using index (%s)", 176 | $request_uri, $this->index), E_USER_NOTICE); 177 | else trigger_error( 178 | sprintf("REQUEST_URI (%s) is NOT allowed", 179 | $request_uri), E_USER_WARNING); 180 | } 181 | return $allow; 182 | } 183 | /*-------------------------------------------------------------------------- 184 | Respond to echo requests. 185 | The API of some ITSP, eg Zadarma, test the web server by sending an echo 186 | request. Let's respond and exit if we detect a echo request. 187 | */ 188 | private function if_echo_exit() { 189 | if (!empty($this->config('websmsd','key_echo')) 190 | && isset($_GET[$this->config('websmsd','key_echo')])) { 191 | trigger_error('Received echo request ('.$_GET[$this->config('websmsd','key_echo')].')', E_USER_NOTICE); 192 | exit($_GET[$this->config('websmsd','key_echo')]); 193 | } 194 | } 195 | /*-------------------------------------------------------------------------- 196 | Evaluates POST request data and returns $message. Parameters are 197 | json decoded and searched recursively. 198 | Use $_POST since file_get_contents("php://input") cannot handle multipart/form-data 199 | @return array $message to, from, and body. 200 | */ 201 | private function message_data() { 202 | $post_data = $_POST; 203 | $message = []; 204 | if (empty($post_data)) { 205 | trigger_error("No POST header data", E_USER_WARNING); 206 | return false; 207 | } else { 208 | if($this->debug) { 209 | trigger_error(sprintf("POST data (%s)",json_encode($post_data))); 210 | } 211 | } 212 | array_walk_recursive($post_data, function(&$val) { 213 | $val_json = json_decode($val,true); 214 | if (!empty($val_json)) $val = $val_json; 215 | }); 216 | array_walk_recursive($post_data, function($val,$key) use (&$message) { 217 | if ($key === $this->config('websmsd','key_to')) $message['to'] = $val; 218 | if ($key === $this->config('websmsd','key_from')) $message['from'] = $val; 219 | if ($key === $this->config('websmsd','key_body')) $message['body'] = $val; 220 | }); 221 | if (empty($message['body'])) $message['body'] = ''; 222 | if (empty($message['to']) || empty($message['from'])) { 223 | trigger_error(sprintf("Did not get all required POST data (%s) message (%s)", 224 | json_encode($post_data),json_encode($message)), E_USER_WARNING); 225 | } 226 | return $message; 227 | } 228 | /*-------------------------------------------------------------------------- 229 | Initially assume 'REMOTE_ADDR' is the original IP of the HTTP client. But if 230 | it is a 'prox_addr' then trust that 'prox_header' hold the real IP of the 231 | client instead. 232 | @return string $remt_addr 233 | */ 234 | private function remt_addr() { 235 | $remt_addr = $_SERVER['REMOTE_ADDR']; 236 | $prox_addr = $this->config('websmsd','prox_addr'); 237 | $prox_header = $this->config('websmsd','prox_header'); 238 | if (!empty($prox_addr) && !empty($prox_header) && 239 | $this->match_cidr($remt_addr,$prox_addr)) { 240 | $header_addr = @$_SERVER[$prox_header]; 241 | if (isset($header_addr)) { 242 | $remt_addr = $header_addr; 243 | } else { 244 | trigger_error( 245 | sprintf("REMOTE_ADDR (%s) matches proxy (%s) but there is no header (%s)", 246 | $remt_addr, $prox_addr, $prox_header), E_USER_WARNING); 247 | } 248 | } 249 | return $remt_addr; 250 | } 251 | /*-------------------------------------------------------------------------- 252 | Compare each uri (comma separated) with the REQUEST_URI 253 | @param string $csv_uris uris to test eg, /index.htm,/index.html 254 | @return Boolean true if $saddr is permitted otherwise false 255 | */ 256 | private function match_uri($csv_uris) { 257 | $request_uri = $_SERVER['REQUEST_URI']; 258 | $match_uris = explode(',',$csv_uris); 259 | foreach ($match_uris as $match_uri) { 260 | if(strcmp($match_uri,$request_uri) === 0) return true; 261 | } 262 | return false; 263 | } 264 | /*-------------------------------------------------------------------------- 265 | @param string $ip addr to test eg, 216.245.217.2 266 | @param string $csvcidrs comma separated list of CIDR ranges 267 | eg, 185.45.152.42,3.104.90.0/24,3.1.77.0/24 268 | @return Boolean true if $ip matches any range in $csvcidrs 269 | */ 270 | public function match_cidr($ip,$csvcidrs) { 271 | $cidrs = explode(',',$csvcidrs); 272 | foreach ($cidrs as $cidr) { 273 | $blknmask = explode('/',$cidr); 274 | $blk = $blknmask[0]; 275 | if (isset($blknmask[1])) { 276 | $mask = $blknmask[1]; 277 | } else { 278 | $mask = 32; 279 | } 280 | $blkbin = ip2long($blk) >> (32 - $mask); 281 | $ipbin = ip2long($ip) >> (32 - $mask); 282 | if ($ipbin === $blkbin) return true; 283 | } 284 | return false; 285 | } 286 | /*-------------------------------------------------------------------------- 287 | Receive an additional header "Signature", 288 | $signatureTest = base64_encode(hash_hmac('sha1', $_POST['result'], API_SECRET)); 289 | @param array $post HTTP POST 290 | @return Boolean true if HTTP request was verified 291 | */ 292 | private function verify($post) { 293 | $auth_method = $this->config('websmsd','auth_method'); 294 | $auth_secret = $this->config('websmsd','auth_secret'); 295 | switch ($auth_method) { 296 | case '': 297 | return true; 298 | break; 299 | case 'zadarma': 300 | $result = $post['result']; 301 | $sign = $post['Signature']; 302 | $sign_expected = base64_encode(hash_hmac('sha1', $result, $auth_secret)); 303 | return ($sign === $sign_expected); 304 | break; 305 | default: 306 | trigger_error("Unknown method (auth_method=$auth_method)", E_USER_WARNING); 307 | return true; 308 | break; 309 | } 310 | } 311 | /*-------------------------------------------------------------------------- 312 | Parse arguments. 313 | We require 3 arguments; to, from, body. 314 | OR 4 arguments; to, from, body, index. 315 | @param array $args arguments 316 | @return array $query_data or false 317 | */ 318 | private function query_data($args) { 319 | switch (count($args)) { 320 | case 4: 321 | $index = $args[4]; 322 | if(strlen($index) > 0) $this->index = $index; 323 | case 3: 324 | $query_data = [ 325 | $this->config('websms','key_to') => $args[1], 326 | $this->config('websms','key_from') => $args[2], 327 | $this->config('websms','key_body') => $args[3], 328 | ]; 329 | return $query_data; 330 | default: 331 | $strings = implode(',',$args); 332 | trigger_error("We did not get exactly 3 or 4 arguments; to, from, body [index] ($strings)", E_USER_WARNING); 333 | return false; 334 | } 335 | } 336 | /*-------------------------------------------------------------------------- 337 | Sanitize telephone numbers. 338 | @param array $query_data 339 | @void 340 | */ 341 | private function val_numform(&$query_data) { 342 | $key_to = $this->config('websms','key_to'); 343 | $key_from = $this->config('websms','key_from'); 344 | switch (strtolower($this->config('websms','val_numform'))) { 345 | case 'no+': 346 | case 'e164': 347 | case 'e.164': 348 | $query_data[$key_to] = preg_replace(['/^[+]/','/^00/'], '',$query_data[$key_to]); 349 | $query_data[$key_from] = preg_replace(['/^[+]/','/^00/'], '',$query_data[$key_from]); 350 | break; 351 | case 'e.123': 352 | default: 353 | } 354 | } 355 | /*-------------------------------------------------------------------------- 356 | Add key-value pairs in $query_data 357 | @param array $query_data 358 | @void 359 | */ 360 | private function val_static(&$query_data) { 361 | $val_static = $this->config('websms','val_static'); 362 | if (!empty($val_static)) { 363 | $static_keyvals = explode(',',$val_static); 364 | foreach ($static_keyvals as $static_keyval) { 365 | if (strpos($static_keyval, '=') !== false) { 366 | list($key, $val) = explode('=',$static_keyval); 367 | $query_data[$key] = $val; 368 | } 369 | } 370 | } 371 | } 372 | /*-------------------------------------------------------------------------- 373 | Sanitize body, since some API only accept Unicode up to xFFFF BMP (UCS-2). 374 | ucs-2: 375 | Replace characters in SMP with the replacement character U+FFFD, when needed. 376 | key=val: 377 | If There is SMP characters add the provided key:val 378 | otherwise: 379 | Do nothing. 380 | @param array $query_data 381 | @void 382 | */ 383 | private function val_unicode(&$query_data) { 384 | $val_unicode = $this->config('websms','val_unicode'); 385 | $key_body = $this->config('websms','key_body'); 386 | if (strpos($val_unicode, '=') !== false) { 387 | list($key_code, $val_code) = explode('=',$val_unicode); 388 | $val_unicode = 'key=val'; 389 | } 390 | switch (strtolower($val_unicode)) { 391 | case 'ucs-2': 392 | $query_data[$key_body] = preg_replace('/[\x{10000}-\x{10FFFF}]/u',"\u{FFFD}",$query_data[$key_body]); 393 | break; 394 | case 'key=val': 395 | if (!empty(preg_grep("/[\x{10000}-\x{10FFFF}]/u",[$query_data[$key_body]]))) 396 | $query_data[$key_code] = $val_code; 397 | break; 398 | case 'utf-8': 399 | default: 400 | } 401 | } 402 | /*-------------------------------------------------------------------------- 403 | Init and setup curl for a POST query. 404 | @return void 405 | */ 406 | private function curl_init() { 407 | $url = $this->config('websms','url_host').$this->config('websms','url_path'); 408 | $this->curl = curl_init($url); 409 | curl_setopt($this->curl, CURLOPT_RETURNTRANSFER, true); 410 | curl_setopt($this->curl, CURLOPT_POST, true); 411 | curl_setopt($this->curl, CURLOPT_SSL_VERIFYPEER, false); 412 | curl_setopt($this->curl, CURLOPT_SSL_VERIFYHOST, false); 413 | } 414 | /*-------------------------------------------------------------------------- 415 | Generates a URL-encoded query string from the $query_data array provided, 416 | and pass it on to curl. 417 | @param array $query_data 418 | @return string $query_string 419 | */ 420 | private function curl_data($query_data) { 421 | ksort($query_data); 422 | $this->debug($query_data); 423 | $query_string = http_build_query($query_data); 424 | curl_setopt($this->curl, CURLOPT_POSTFIELDS, $query_string); 425 | return $query_string; 426 | } 427 | /*-------------------------------------------------------------------------- 428 | Setup curl's authentication method. 429 | Currently we support: 430 | 431 | 'basic' 432 | basic access authentication, see, wikipedia.org/wiki/Basic_access_authentication, 433 | with headers like: Authorization: Basic 434 | 435 | 'zadarma' 436 | Zadarma's unique authentication method, see, zadarma.com/en/support/api, 437 | with headers like: Authorization: : 438 | @param array $query_data 439 | @return void 440 | */ 441 | private function curl_auth(&$query_data) { 442 | $auth_method = $this->config('websms','auth_method'); 443 | $auth_user = $this->config('websms','auth_user'); 444 | $auth_secret = $this->config('websms','auth_secret'); 445 | $key_user = $this->config('websms','key_user'); 446 | $key_secret = $this->config('websms','key_secret'); 447 | $url_path = $this->config('websms','url_path'); 448 | switch ($auth_method) { 449 | case 'none': 450 | break; 451 | case 'plain': 452 | if (!empty($key_user) && !empty($auth_user) && !empty($key_secret) && !empty($auth_secret)) { 453 | $query_data[$key_user] = $auth_user; 454 | $query_data[$key_secret] = $auth_secret; 455 | } else trigger_error( 456 | sprintf("Authorization method (%s) but not all needed parameters are defined",$auth_method), E_USER_WARNING); 457 | break; 458 | case 'basic': 459 | if (!empty($auth_user) && !empty($auth_secret)) { 460 | curl_setopt($this->curl, CURLOPT_HTTPAUTH, CURLAUTH_BASIC); 461 | curl_setopt($this->curl, CURLOPT_USERPWD, "$auth_user:$auth_secret"); 462 | } else trigger_error( 463 | sprintf("Authorization method (%s) but not all needed parameters are defined",$auth_method), E_USER_WARNING); 464 | break; 465 | case 'zadarma': 466 | if (!empty($auth_user) && !empty($auth_secret)) { 467 | $query_string = $this->curl_data($query_data); 468 | $signature = base64_encode(hash_hmac('sha1', $url_path . 469 | $query_string . md5($query_string), $auth_secret)); 470 | curl_setopt($this->curl, CURLOPT_HTTPHEADER, 471 | array('Authorization: ' . $auth_user . ':' . $signature)); 472 | } else trigger_error( 473 | sprintf("Authorization method (%s) but not all needed parameters are defined",$auth_method), E_USER_WARNING); 474 | break; 475 | default: trigger_error( 476 | sprintf("Unknown authorization method (auth_method=%s)",$auth_method), E_USER_WARNING); 477 | } 478 | } 479 | /*-------------------------------------------------------------------------- 480 | Send POST query, read response and close. 481 | @return array $resp_data 482 | */ 483 | private function curl_send() { 484 | $resp_json = curl_exec($this->curl); 485 | if (!empty($resp_json)) { 486 | $resp_data = json_decode($resp_json,true); 487 | } else { 488 | $resp_data = false; 489 | trigger_error("Curl error: ".curl_error($this->curl)); 490 | } 491 | $curl_info = curl_getinfo($this->curl); 492 | curl_close($this->curl); 493 | //$this->debug($curl_info); 494 | $this->debug($resp_json); 495 | return $resp_data; 496 | } 497 | /*-------------------------------------------------------------------------- 498 | Check response and set exit code accordingly. 499 | the response check is controlled by the variable resp_check. It can be: 500 | = null\""; No check, we will always exit with status true 501 | = "key=value" If value of key in response is equal to value exit true otherwise false 502 | = "/pattern/"; If pattern matches response exit with status true otherwise false 503 | { 504 | "status":"success", 505 | "messages":1, 506 | "cost":0.24, 507 | "currency":"USD" 508 | } 509 | @param array $resp_data 510 | @return Boolean true if resp_check matches, otherwize false 511 | */ 512 | private function resp_check($resp_data) { 513 | $resp_check = $this->config('websms','resp_check'); 514 | $url_host = $this->config('websms','url_host'); 515 | $auth_user = $this->config('websms','auth_user'); 516 | if (empty($resp_data)) return false; 517 | if (!empty($resp_check)) { 518 | if (strpos($resp_check, '=') !== false) { // "key=value" 519 | list($test_key, $test_value) = explode('=',$resp_check); 520 | // test hierarchically 521 | array_walk_recursive($resp_data, function($val,$key) use (&$resp_value,$test_key) { 522 | if ($key === $test_key) $resp_value = $val; 523 | }); 524 | if ($resp_value !== $test_value) { 525 | trigger_error(sprintf("Called (%s) but return did not match (%s = %s != %s)", 526 | $url_host,$test_key,$resp_value,$test_value), E_USER_WARNING); 527 | trigger_error(sprintf("Response was: %s",json_encode($resp_data))); 528 | return false; 529 | } 530 | } else { // "/pattern/" 531 | if (!preg_match($resp_check,$resp_data)) { 532 | trigger_error(sprintf("Called (%s) but return did not match (%s != %s)", 533 | $url_host,$resp_data,$resp_check), E_USER_WARNING); 534 | trigger_error(sprintf("Response was: %s",json_encode($resp_data))); 535 | return false; 536 | } 537 | } 538 | trigger_error("Outbound SMS sent (host=$url_host,user=$auth_user) successfully", E_USER_NOTICE); 539 | } else { 540 | trigger_error("Outbound SMS sent (host=$url_host,user=$auth_user)", E_USER_NOTICE); 541 | } 542 | return true; 543 | } 544 | /*-------------------------------------------------------------------------- 545 | Get configuration value, if $this->index is set pick that element in array 546 | @param mixed $section 547 | @param mixed $key 548 | @param Boolean $allow_array 549 | @return mixed $value or $array_of_values if $allow_array = true 550 | */ 551 | public function config($section, $key, $allow_array = false) { 552 | $value = $this->config[$section][$key]; 553 | $indices = $this->config_indices($section); 554 | if (is_array($value)) { 555 | if (isset($this->index)) { 556 | if (array_key_exists($this->index,$value)) { 557 | $value = $value[$this->index]; 558 | } else { 559 | $value = self::DEFAULT_CONF_VALS[$section][$key]; 560 | if (!in_array($this->index,$indices)) trigger_error( 561 | sprintf("Config ([%s] %s [%s]) missing; using default", 562 | $section,$key,$this->index), E_USER_NOTICE); 563 | } 564 | } else { 565 | if ($allow_array !== true) { 566 | trigger_error( 567 | sprintf("Config ([%s] %s) is array but index is not provided", 568 | $section,$key), E_USER_ERROR); 569 | $value = null; 570 | } 571 | } 572 | } 573 | return $value; 574 | } 575 | /*-------------------------------------------------------------------------- 576 | */ 577 | public function config_indices($section) { 578 | $indices = []; 579 | array_walk($this->config[$section], function($val,$key) use (&$indices) { 580 | if(is_array($val)) 581 | foreach($val as $key => $value) 582 | if(!in_array($key, $indices)) 583 | $indices[] = $key; 584 | }); 585 | return $indices; 586 | } 587 | /*-------------------------------------------------------------------------- 588 | Print variable if $debug or $this->debug is true 589 | @param mixed $var 590 | @param boolean $debug 591 | @return void 592 | */ 593 | public function debug($var, $debug = false) { 594 | if($debug || $this->debug) { 595 | var_dump($var); 596 | } 597 | } 598 | } 599 | ?> 600 | -------------------------------------------------------------------------------- /src/websms/php/websms.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | tx_query(@$argv)) { 21 | $exit_code = 0; 22 | } else { 23 | $exit_code = 1; 24 | } 25 | 26 | closelog(); 27 | exit($exit_code); 28 | ?> 29 | -------------------------------------------------------------------------------- /src/websms/php/websmsd.php: -------------------------------------------------------------------------------- 1 | rx_query(); 32 | $status = $queue->text($message); 33 | $sms->ack_query($status, $message); 34 | ?> 35 | -------------------------------------------------------------------------------- /test/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile 2 | # 3 | # test 4 | # 5 | 6 | -include *.mk 7 | 8 | TST_REPO ?= mlan/asterisk 9 | TST_VER ?= latest 10 | TST_NAME ?= test 11 | 12 | NET_NAME ?= $(TST_NAME)-net 13 | NET_ENV ?= --network $(NET_NAME) 14 | 15 | CNT_LIST ?= pbx 16 | 17 | PBX_NAME ?= $(TST_NAME)-pbx 18 | PBX_DOM ?= example.com 19 | PBX_FQDN ?= pbx.$(PBX_DOM) 20 | PBX_CMD ?= asterisk -r 21 | PBX_SIPP ?= 5060 22 | PBX_SIPS ?= 5061 23 | PBX_RTPP ?= 10000-10099 24 | PBX_SMSP ?= 8080 25 | EXP_ENV ?= \ 26 | -p $(PBX_SIPP):$(PBX_SIPP)/udp \ 27 | -p $(PBX_SIPP):$(PBX_SIPP) \ 28 | -p $(PBX_SIPS):$(PBX_SIPS) \ 29 | -p $(PBX_RTPP):$(PBX_RTPP)/udp \ 30 | -p $(PBX_SMSP):80 31 | CAP_ENV ?= \ 32 | --cap-add SYS_PTRACE \ 33 | --cap-add=NET_ADMIN \ 34 | --cap-add=NET_RAW 35 | PBX_ENV ?= $(NET_ENV) $(EXP_ENV) $(CAP_ENV) $(LOC_ENV) \ 36 | --name $(PBX_NAME) \ 37 | --hostname $(PBX_FQDN) \ 38 | -e SYSLOG_LEVEL=8 39 | PBXV_ENV ?= -v $(PBX_NAME):/srv 40 | 41 | TST_W8S ?= 5 42 | TST_W8L ?= 10 43 | 44 | NET_SYSP := $(shell command -v nft) 45 | 46 | ifdef NET_SYSP 47 | NET_RULE := nft list ruleset 48 | else 49 | NET_RULE := iptables -t nat -nvL 50 | endif 51 | 52 | variables: 53 | make -pn | grep -A1 "^# makefile"| grep -v "^#\|^--" | sort | uniq 54 | 55 | ps: 56 | docker ps -a 57 | 58 | test-all: $(addprefix test_,1 2 3 4) 59 | 60 | 61 | test_%: test-up_% test-wait_% test-logs_% test-autoban_% test-ping_% test-down_% 62 | 63 | 64 | test-up_1: test-up-net 65 | # 66 | # 67 | # 68 | # test (1) run mini with defaults (is there smoke?) 69 | # 70 | # 71 | docker run -d $(PBX_ENV) $(TST_REPO):$(call bld_tag,mini,$(TST_VER)) 72 | 73 | test-up_2: test-up-net 74 | # 75 | # 76 | # 77 | # test (2) run base 78 | # 79 | # 80 | docker run -d $(PBX_ENV) $(TST_REPO):$(call bld_tag,base,$(TST_VER)) 81 | 82 | test-up_3: test-up-net 83 | # 84 | # 85 | # 86 | # test (3) run full 87 | # 88 | # 89 | docker run -d $(PBX_ENV) $(TST_REPO):$(call bld_tag,full,$(TST_VER)) 90 | 91 | test-up_4: test-up-net 92 | # 93 | # 94 | # 95 | # test (4) run xtra 96 | # 97 | # 98 | docker run -d $(PBX_ENV) $(TST_REPO):$(call bld_tag,xtra,$(TST_VER)) 99 | 100 | test-ping_%: test-version_% 101 | # 102 | # 103 | # test ($*) success ☺ 104 | # 105 | # 106 | # 107 | 108 | test-version_%: 109 | docker exec -it $(PBX_NAME) asterisk -x "pjsip show version" | grep PJPROJECT 110 | 111 | test-autoban_%: 112 | case $* in [2-3]) docker exec -it $(PBX_NAME) autoban;; esac 113 | 114 | test-logs_%: 115 | docker container logs $(PBX_NAME) | grep 'docker-entrypoint.sh' || true 116 | 117 | test-wait_%: 118 | case $* in [1-2]) sleep $(TST_W8S);; [3-9]) sleep $(TST_W8L);; esac 119 | 120 | test-up-net: 121 | docker network create $(NET_NAME) 2>/dev/null || true 122 | 123 | test-down: test-down_0 test-down-net test-down-vol acme-destroy 124 | 125 | test-down_%: 126 | docker rm -fv $(PBX_NAME) 2>/dev/null || true 127 | 128 | test-down-net: 129 | docker network rm $(NET_NAME) 2>/dev/null || true 130 | 131 | test-down-vol: 132 | docker volume rm $(PBX_NAME) 2>/dev/null || true 133 | 134 | 135 | $(addprefix test-,diff env htop imap logs pop3 sh sv): 136 | ${MAKE} $(patsubst test-%,pbx-%,$@) 137 | 138 | $(addsuffix -sh,$(CNT_LIST)): 139 | docker exec -it $(patsubst %-sh,$(TST_NAME)-%,$@) sh -c 'exec $$(getent passwd root | sed "s/.*://g")' 140 | 141 | $(addsuffix -cli,$(CNT_LIST)): 142 | docker exec -it $(patsubst %-cli,$(TST_NAME)-%,$@) $(PBX_CMD) 143 | 144 | $(addsuffix -env,$(CNT_LIST)): 145 | docker exec -it $(patsubst %-env,$(TST_NAME)-%,$@) env 146 | 147 | $(addsuffix -logs,$(CNT_LIST)): 148 | docker container logs $(patsubst %-logs,$(TST_NAME)-%,$@) 149 | 150 | $(addsuffix -diff,$(CNT_LIST)): 151 | docker container diff $(patsubst %-diff,$(TST_NAME)-%,$@) 152 | 153 | $(addsuffix -tools,$(CNT_LIST)): 154 | docker exec -it $(patsubst %-tools,$(TST_NAME)-%,$@) \ 155 | apk --no-cache --update add \ 156 | nano less lsof htop openldap-clients bind-tools iputils strace util-linux \ 157 | pulseaudio-utils alsa-utils mariadb-client 158 | 159 | $(addsuffix -htop,$(CNT_LIST)): 160 | docker exec -it $(patsubst %-htop,$(TST_NAME)-%,$@) htop 161 | 162 | $(addsuffix -sv,$(CNT_LIST)): 163 | docker exec -it $(patsubst %-sv,$(TST_NAME)-%,$@) sh -c 'sv status $$SVDIR/*' 164 | 165 | $(addsuffix -nsrule,$(CNT_LIST)): 166 | sudo nsenter -n -t $(call dkr_cnt_pid,$(patsubst %-nsrule,$(TST_NAME)-%,$@)) $(NET_RULE) 167 | 168 | $(addsuffix -nsss,$(CNT_LIST)): 169 | sudo nsenter -n -t $(call dkr_cnt_pid,$(patsubst %-nsss,$(TST_NAME)-%,$@)) ss -utnlp 170 | 171 | acme-destroy: ssl-destroy 172 | rm -f acme/* 173 | 174 | acme/acme.json: $(SRV_CERT) 175 | bin/gen-acme-json.sh $(AD_USR_CN)@$(AD_DOM) $(SRV_FQDN) $(SRV_KEY) $(SRV_CERT) > $@ 176 | -------------------------------------------------------------------------------- /test/acme/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore everything in this directory 2 | * 3 | # Except this file 4 | !.gitignore 5 | -------------------------------------------------------------------------------- /test/bld.mk: -------------------------------------------------------------------------------- 1 | ../bld.mk -------------------------------------------------------------------------------- /test/dkr.mk: -------------------------------------------------------------------------------- 1 | ../dkr.mk -------------------------------------------------------------------------------- /test/ssl.mk: -------------------------------------------------------------------------------- 1 | # ssl.mk 2 | # 3 | # SSL and TLS make-functions 4 | # 5 | 6 | SSL_O ?= example.com 7 | SSL_KEY ?= rsa:2048 # rsa:2048 rsa:4096 8 | SSL_MAIL ?= 9 | SSL_PASS ?= secret 10 | SSL_SAN ?= 11 | SSL_TRST ?= 12 | 13 | # 14 | # Usage: OpenLDAP 15 | # 16 | #SSL_O = $(AD_DOM) 17 | #target: ssl/auth.crt ssl/demo.crt 18 | 19 | # 20 | # Usage: SMIME 21 | # 22 | #SSL_O = $(MAIL_DOMAIN) 23 | #SSL_MAIL = auto 24 | #SSL_PASS = $(AD_USR_PW) 25 | ##SSL_TRST = $(SSL_SMIME) 26 | #target: ssl/$(AD_USR_CN)@$(MAIL_DOMAIN).p12 27 | SSL_SMIME = -setalias "Self Signed SMIME" -addtrust emailProtection \ 28 | -addreject clientAuth -addreject serverAuth 29 | 30 | # 31 | # Usage: SUbject Alternate Name SAN 32 | # 33 | #SSL_O = example.com 34 | #SSL_SAN = "subjectAltName=DNS:auth,DNS:*.docker" 35 | #target: ssl/auth.crt 36 | 37 | 38 | # 39 | # $(call ssl_subj,root,example.com,) -> -subj "/CN=root/O=example.com" 40 | # $(call ssl_subj,root,example.com,auto) -> -subj "/CN=root/O=example.com/emailAddress=root@example.com" 41 | # $(call ssl_subj,root,example.com,admin@my.org) -> -subj "/CN=root/O=example.com/emailAddress=admin@my.org" 42 | # 43 | ssl_subj = -subj "/CN=$(1)/O=$(2)$(if $(3),/emailAddress=$(if $(findstring @,$(3)),$(3),$(1)@$(2)),)" 44 | 45 | # 46 | # $(call ssl_extfile,"subjectAltName=DNS:auth") -> -extfile <(printf "subjectAltName=DNS:auth") 47 | # 48 | ssl_extfile = $(if $(1),-extfile <(printf $(1)),) 49 | 50 | 51 | .PRECIOUS: %.crt %.csr %.key 52 | SHELL = /bin/bash 53 | 54 | # 55 | # Personal information exchange file PKCS#12 56 | # 57 | %.p12: %.crt 58 | openssl pkcs12 -export -in $< -inkey $*.key -out $@ \ 59 | -passout pass:$(SSL_PASS) 60 | 61 | # 62 | # Certificate PEM 63 | # 64 | %.crt: %.csr ssl/ca.crt 65 | openssl x509 -req -in $< -CA $(@D)/ca.crt -CAkey $(@D)/ca.key -out $@ \ 66 | $(call ssl_extfile,$(SSL_SAN)) $(SSL_TRST) -CAcreateserial 67 | 68 | # 69 | # Certificate signing request PEM 70 | # 71 | %.csr: ssl 72 | openssl req -new -newkey $(SSL_KEY) -nodes -keyout $*.key -out $@ \ 73 | $(call ssl_subj,$(*F),$(SSL_O),$(SSL_MAIL)) 74 | 75 | # 76 | # Certificate authority certificate PEM 77 | # 78 | ssl/ca.crt: ssl 79 | openssl req -x509 -new -newkey $(SSL_KEY) -nodes -keyout ssl/ca.key -out $@ \ 80 | $(call ssl_subj,root,$(SSL_O),$(SSL_MAIL)) 81 | 82 | # 83 | # SSL directory 84 | # 85 | ssl: 86 | mkdir -p $@ 87 | 88 | # 89 | # Remove all files in SSL directory 90 | # 91 | ssl-destroy: 92 | rm -f ssl/* 93 | 94 | # 95 | # Inspect all files in SSL directory 96 | # 97 | ssl-list: 98 | @for file in $$(ls ssl/*); do \ 99 | case $$file in \ 100 | *.crt) \ 101 | printf "\e[33;1m%s\e[0m\n" $$file; \ 102 | openssl x509 -noout -issuer -subject -ext basicConstraints,keyUsage,extendedKeyUsage,subjectAltName -in $$file;; \ 103 | *.csr) \ 104 | printf "\e[33;1m%s\e[0m\n" $$file; \ 105 | openssl req -noout -subject -in $$file;; \ 106 | *.key) \ 107 | printf "\e[33;1m%s\e[0m\n" $$file; \ 108 | openssl rsa -text -noout -in $$file | head -n 1;; \ 109 | esac \ 110 | done 111 | 112 | ssl-inspect: 113 | @for file in $$(ls ssl/*); do \ 114 | case $$file in \ 115 | *.crt) \ 116 | printf "\e[33;1m%s\e[0m " $$file; \ 117 | openssl x509 -text -noout -certopt no_sigdump,no_pubkey -in $$file;; \ 118 | *.csr) \ 119 | printf "\e[33;1m%s\e[0m " $$file; \ 120 | openssl req -text -noout -reqopt no_sigdump,no_pubkey,ext_default -in $$file;; \ 121 | *.key) \ 122 | printf "\e[33;1m%s\e[0m " $$file; \ 123 | openssl rsa -text -noout -in $$file | head -n 1;; \ 124 | esac \ 125 | done 126 | -------------------------------------------------------------------------------- /test/ssl/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore everything in this directory 2 | * 3 | # Except this file 4 | !.gitignore 5 | --------------------------------------------------------------------------------