├── .gitignore ├── .editorconfig ├── .travis.yml ├── license.md ├── package.json ├── readme.md └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | indent_style = space 3 | indent_size = 2 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 8 4 | - 7 5 | - 6 6 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Kodie Grantham 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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "apache-module-manager", 3 | "version": "1.0.1", 4 | "description": "A CLI tool for enabling/disabling Apache modules", 5 | "keywords": [ 6 | "apache", 7 | "cli", 8 | "command", 9 | "conf", 10 | "disable", 11 | "enable", 12 | "http", 13 | "module", 14 | "manage", 15 | "server", 16 | "switch" 17 | ], 18 | "author": { 19 | "name": "Kodie Grantham", 20 | "email": "kodie.grantham@gmail.com", 21 | "url": "http://kodieg.com" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "https://github.com/kodie/apache-module-manager.git" 26 | }, 27 | "bugs": { 28 | "url": "https://github.com/kodie/apache-module-manager/issues" 29 | }, 30 | "homepage": "https://github.com/kodie/apache-module-manager", 31 | "license": "MIT", 32 | "main": "index.js", 33 | "engines": { 34 | "node": ">=6.0.0" 35 | }, 36 | "bin": { 37 | "amm": "index.js" 38 | }, 39 | "scripts": { 40 | "test": "standard" 41 | }, 42 | "dependencies": { 43 | "chalk": "^2.3.2", 44 | "columnify": "^1.5.4", 45 | "commander": "^2.15.1", 46 | "fast-sort": "^1.2.6", 47 | "fuse.js": "^3.2.0", 48 | "inquirer": "^5.2.0", 49 | "line-number": "^0.1.0" 50 | }, 51 | "devDependencies": { 52 | "standard": "^11.0.1" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Apache Module Manager 2 | 3 | [![npm package version](https://img.shields.io/npm/v/apache-module-manager.svg?style=flat-square)](https://www.npmjs.com/package/apache-module-manager) 4 | [![Travis build status](https://img.shields.io/travis/kodie/apache-module-manager.svg?style=flat-square)](https://travis-ci.org/kodie/apache-module-manager) 5 | [![npm package downloads](https://img.shields.io/npm/dt/apache-module-manager.svg?style=flat-square)](https://www.npmjs.com/package/apache-module-manager) 6 | [![code style](https://img.shields.io/badge/code_style-standard-yellow.svg?style=flat-square)](https://github.com/standard/standard) 7 | [![license](https://img.shields.io/github/license/kodie/apache-module-manager.svg?style=flat-square)](license.md) 8 | 9 | [![asciicast](https://asciinema.org/a/176960.png)](https://asciinema.org/a/176960) 10 | 11 | A CLI tool for enabling/disabling [Apache](https://httpd.apache.org) modules. 12 | 13 | ## Installation 14 | 15 | ```shell 16 | $ npm install --global apache-module-manager 17 | ``` 18 | 19 | ## Note 20 | 21 | This will modify your Apache config file. It is your responsibility to make a backup. This has only been tested on macOS with the default Apache installation. Use at your own risk. 22 | 23 | ## Usage 24 | 25 | ### `list|l [-d -e -s ] [search]` 26 | 27 | List/search for modules. 28 | 29 | #### Options 30 | 31 | | Long | Short | Description | 32 | |------------|-------|--------------------------------------------------------------------------------| 33 | | --disabled | -d | Only display disabled modules | 34 | | --enabled | -e | Only display enabled modules | 35 | | --sort | -s | A comma separated list of columns to sort by (defaults to `enabled,name,path`) | 36 | 37 | 38 | #### Examples 39 | 40 | ```shell 41 | # Display all modules 42 | $ amm list 43 | $ amm l 44 | 45 | # Display modules that contain 'php' 46 | $ amm list php 47 | 48 | # Display all modules and sort them by their line number 49 | $ amm list --sort line 50 | 51 | # Display all currently enabled modules 52 | $ amm list -e 53 | ``` 54 | 55 | ##### Example Output 56 | 57 | ```shell 58 | $ amm list -s line 59 | 60 | ID NAME PATH ENABLED LINE 61 | 0 authn_file_module libexec/apache2/mod_authn_file.so true 71 62 | 1 authn_dbm_module libexec/apache2/mod_authn_dbm.so false 72 63 | 2 authn_anon_module libexec/apache2/mod_authn_anon.so false 73 64 | 3 authn_dbd_module libexec/apache2/mod_authn_dbd.so false 74 65 | 4 authn_socache_module libexec/apache2/mod_authn_socache.so false 75 66 | 5 authn_core_module libexec/apache2/mod_authn_core.so true 76 67 | 6 authz_host_module libexec/apache2/mod_authz_host.so true 77 68 | 7 authz_groupfile_module libexec/apache2/mod_authz_groupfile.so true 78 69 | 8 authz_user_module libexec/apache2/mod_authz_user.so true 79 70 | 9 authz_dbm_module libexec/apache2/mod_authz_dbm.so false 80 71 | ... 72 | ``` 73 | 74 | ### `enable|e ` 75 | 76 | Enable a module. If multiple modules match the search term, a select prompt will be displayed allowing you to choose from the matches. The `sudo` prefix is required since the script modifies a system file. 77 | 78 | #### Examples 79 | 80 | ```shell 81 | $ sudo amm enable php 82 | $ sudo amm e php 83 | ``` 84 | 85 | ##### Example Output 86 | 87 | ```shell 88 | $ sudo amm enable php 89 | 90 | ? Which module are you looking for? (Use arrow keys) 91 | php7_module libexec/apache2/libphp7.so 92 | ❯ php5_module /usr/local/opt/php@5.6/lib/httpd/modules/libphp5.so 93 | ? Enable php5_module (/usr/local/opt/php@5.6/lib/httpd/modules/libphp5.so)? (Y/n) 94 | ✔ Changed line 180 to LoadModule php5_module /usr/local/opt/php@5.6/lib/httpd/modules/libphp5.so 95 | ? Restart Apache (/usr/sbin/apachectl restart)? (Y/n) 96 | ``` 97 | 98 | ### `disable|d ` 99 | 100 | Disable a module. If multiple modules match the search term, a select prompt will be displayed allowing you to choose from the matches. The `sudo` prefix is required since the script modifies a system file. 101 | 102 | #### Examples 103 | 104 | ```shell 105 | $ sudo amm disable php 106 | $ sudo amm d php 107 | ``` 108 | 109 | ##### Example Output 110 | 111 | ```shell 112 | $ sudo amm disable php 113 | 114 | ? Disable php5_module (/usr/local/opt/php@5.6/lib/httpd/modules/libphp5.so)? (Y/n) 115 | ✔ Changed line 180 to #LoadModule php5_module /usr/local/opt/php@5.6/lib/httpd/modules/libphp5.so 116 | ? Restart Apache (/usr/sbin/apachectl restart)? (Y/n) 117 | ``` 118 | 119 | ### `switch [new search]` 120 | 121 | Disable a module and enable another one. If only one argument is given, the first argument will be used to search for both the module to disable and the module to enable. If multiple modules match a search term, a select prompt will be displayed allowing you to choose from the matches. The `sudo` prefix is required since the script modifies a system file. 122 | 123 | #### Examples 124 | 125 | ```shell 126 | $ sudo amm switch php5 php7 127 | $ sudo amm s php 128 | ``` 129 | 130 | ##### Example Output 131 | 132 | ```shell 133 | $ sudo amm switch php 134 | 135 | ? Disable php5_module (/usr/local/opt/php@5.6/lib/httpd/modules/libphp5.so)? (Y/n) 136 | ✔ Changed line 180 to #LoadModule php5_module /usr/local/opt/php@5.6/lib/httpd/modules/libphp5.so 137 | ? Enable php7_module (libexec/apache2/libphp7.so)? (Y/n) 138 | ✔ Changed line 176 to LoadModule php7_module libexec/apache2/libphp7.so 139 | ? Restart Apache (/usr/sbin/apachectl restart)? (Y/n) 140 | ``` 141 | 142 | ### Config File 143 | 144 | By default, AMM will check if the file `~/.amm.json` exists and load config options from it. 145 | 146 | #### Default Config 147 | 148 | ```json 149 | { 150 | "apache_config": "/etc/apache2/httpd.conf", 151 | "apache_restart": "/usr/sbin/apachectl restart" 152 | } 153 | ``` 154 | 155 | ### Global Options 156 | 157 | These options can be used with any of the above commands: 158 | 159 | | Long | Short | Description | 160 | |-----------------|-------|-----------------------------------------------| 161 | | --version | -V | Display current Apache Module Manager version | 162 | | --apache-config | -a | Path to the Apache config file | 163 | | --config | -c | Path to AMM config file | 164 | | --help | -h | Display help information | 165 | 166 | ### Environment Variables 167 | 168 | These environment variables can be used to change different config options: 169 | 170 | | Variable | Description | 171 | |-------------------|--------------------------------| 172 | | AMM_CONFIG | Path to an AMM config file | 173 | | AMM_APACHE_CONFIG | Path to the Apache config file | 174 | 175 | ## TODO/Ideas 176 | 177 | - [ ] Automated testing 178 | - [ ] Better Linux support/testing 179 | - [ ] Support for multiple Apache config files (file traversing?) 180 | - [ ] Allow for multiple modules to be enabled/disabled at the same time 181 | - [ ] Automated Apache config file backups 182 | - [ ] Implement module so that AMM can be used by other packages 183 | - [ ] Implement `add` and `remove` commands to add/remove LoadModule lines from config 184 | 185 | ## Frequently Asked Questions 186 | 187 | ### Doesn't a2enmod/a2dismod do the same thing? 188 | 189 | Currently, yes. I built this before I knew that a2enmod/a2dismod could be installed on macOS. However, AMM has a nicer interface and I would like to extend the functionality of it in the near future. 190 | 191 | ### Don't you mean httpd instead of Apache? 192 | 193 | Yes, however [plenty of people refer to it as Apache](https://askubuntu.com/a/600902) and you know what I'm talking about. 194 | 195 | ## License 196 | MIT. See the [license.md file](license.md) for more info. 197 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /* eslint prefer-promise-reject-errors: ["error", {"allowEmptyReject": true}] */ 3 | 'use strict' 4 | 5 | const chalk = require('chalk') 6 | const columnify = require('columnify') 7 | const { execSync } = require('child_process') 8 | const fs = require('fs') 9 | const Fuse = require('fuse.js') 10 | const inquirer = require('inquirer') 11 | const lineNumber = require('line-number') 12 | const os = require('os') 13 | const program = require('commander') 14 | const sort = require('fast-sort') 15 | const version = require('./package.json').version 16 | 17 | const reg = /^(#)?LoadModule[ \t](\S*)[ \t](\S*)$/ 18 | 19 | var config = { 20 | apache_config: '/etc/apache2/httpd.conf', 21 | apache_restart: '/usr/sbin/apachectl restart' 22 | } 23 | 24 | var apacheConfig 25 | var lines 26 | var mods 27 | var userConfig 28 | 29 | function changeModuleStatus (mod, status) { 30 | return new Promise((resolve, reject) => { 31 | if (status) { 32 | var dupCheck = mods.filter(m => m.enabled && m.name === mod.name).length 33 | 34 | if (dupCheck) { 35 | console.log(chalk`{red ✘} There is already an {green enabled} module named {yellow ${mod.name}}`) 36 | return reject() 37 | } 38 | } 39 | 40 | return inquirer.prompt({ 41 | type: 'confirm', 42 | name: 'confirm', 43 | message: chalk`${status ? 'Enable' : 'Disable'} {cyan ${mod.name}} ({yellow ${mod.path}})?` 44 | }).then(a => { 45 | if (!a['confirm']) return reject() 46 | 47 | var line = lines[mod.line - 1] 48 | 49 | if (line.startsWith(status ? '#' : 'L')) { 50 | mods[mod.id].enabled = status 51 | 52 | if (status) { 53 | editLine(mod.line, line.substr(1)) 54 | } else { 55 | editLine(mod.line, '#' + line) 56 | } 57 | 58 | return resolve() 59 | } 60 | }) 61 | }) 62 | } 63 | 64 | function chooseModule (find, list) { 65 | if (!list) list = mods 66 | 67 | var fuse = new Fuse(list, { 68 | keys: ['name'], 69 | threshold: 0.1 70 | }) 71 | var search = fuse.search(find) 72 | 73 | return new Promise((resolve, reject) => { 74 | if (search.length === 0) { 75 | console.log(chalk`{red ✘} No applicable modules found matching {yellow ${find}}`) 76 | return reject() 77 | } else if (search.length === 1) { 78 | return resolve(search[0]) 79 | } else { 80 | return inquirer.prompt({ 81 | name: 'mod', 82 | type: 'list', 83 | message: 'Which module are you looking for?', 84 | choices: search.map((m, i) => Object({ 85 | key: i, 86 | name: chalk`${m.name} {yellow ${m.path}}`, 87 | value: m 88 | })) 89 | }).then(a => resolve(a.mod)) 90 | } 91 | }) 92 | } 93 | 94 | function editLine (lineNumber, line) { 95 | lines[lineNumber - 1] = line 96 | apacheConfig = lines.join('\n') 97 | 98 | try { 99 | fs.writeFileSync(config.apache_config, apacheConfig) 100 | console.log(chalk`{green ✔} Changed line {cyan ${lineNumber}} to {yellow ${line}}`) 101 | } catch (e) { 102 | console.log(chalk`{red ✘} An error occured while trying to edit {yellow ${config.apache_config}}: ${e.code}`) 103 | console.log(chalk`{yellow !} You may want to retry that command with the {cyan sudo} prefix`) 104 | process.exit(1) 105 | } 106 | } 107 | 108 | function getModules () { 109 | var modList = apacheConfig.match(RegExp(reg, 'gm')) 110 | var lineList = lineNumber(apacheConfig, reg) 111 | 112 | modList = modList.map((m, id) => { 113 | var mod = reg.exec(m) 114 | 115 | return { 116 | id, 117 | name: mod[2], 118 | path: mod[3], 119 | enabled: !mod[1], 120 | line: lineList[id].number 121 | } 122 | }) 123 | 124 | mods = modList 125 | return modList 126 | } 127 | 128 | function promptRestart () { 129 | return new Promise((resolve, reject) => { 130 | if (!config.apache_restart) return resolve() 131 | 132 | return inquirer.prompt({ 133 | name: 'confirm', 134 | type: 'confirm', 135 | message: chalk`Restart Apache ({yellow ${config.apache_restart}})?` 136 | }).then(a => { 137 | if (a['confirm']) execSync(config.apache_restart) 138 | return resolve(a['confirm']) 139 | }) 140 | }) 141 | } 142 | 143 | function setup () { 144 | var newUserConfig = os.homedir() + '/.amm.json' 145 | 146 | if (process.env.AMM_CONFIG) newUserConfig = process.env.AMM_CONFIG 147 | if (program.config) newUserConfig = program.config 148 | 149 | if (fs.existsSync(newUserConfig)) { 150 | try { 151 | userConfig = newUserConfig 152 | newUserConfig = require(newUserConfig) 153 | Object.assign(config, newUserConfig) 154 | } catch (e) { 155 | console.log(e) 156 | console.log(chalk`{red ✘} An error occured while loading specified user config file {yellow ${newUserConfig}}`) 157 | } 158 | } else if (process.env.AMM_CONFIG || program.config) { 159 | console.log(chalk`{yellow !} Specified user config file {yellow ${newUserConfig}} was not found`) 160 | } 161 | 162 | var newApacheConfig = config.apache_config 163 | 164 | if (process.env.AMM_APACHE_CONFIG) newApacheConfig = process.env.AMM_APACHE_CONFIG 165 | if (program.apacheConfig) newApacheConfig = program.apacheConfig 166 | 167 | if (fs.existsSync(newApacheConfig)) { 168 | config.apache_config = newApacheConfig 169 | } else { 170 | console.log(chalk`{red ✘} Apache config file {yellow ${newApacheConfig}} was not found`) 171 | process.exit(1) 172 | } 173 | 174 | apacheConfig = fs.readFileSync(config.apache_config, 'utf8') 175 | lines = apacheConfig.split('\n') 176 | 177 | getModules() 178 | 179 | if (userConfig) console.log(chalk`{yellow AMM Config:} ${userConfig}`) 180 | console.log(chalk`{yellow Apache Config:} ${config.apache_config}\n`) 181 | } 182 | 183 | program 184 | .command('disable ') 185 | .alias('d') 186 | .description('Disable module(s)') 187 | .action((search, cmd) => { 188 | setup() 189 | 190 | chooseModule(search, mods.filter(m => m.enabled)) 191 | .then(mod => changeModuleStatus(mod, false)) 192 | .then(() => promptRestart()) 193 | .catch(() => {}) 194 | }) 195 | 196 | program 197 | .command('enable [module]') 198 | .alias('e') 199 | .description('Enable module(s)') 200 | .action((search, cmd) => { 201 | setup() 202 | 203 | chooseModule(search, mods.filter(m => !m.enabled)) 204 | .then(mod => changeModuleStatus(mod, true)) 205 | .then(() => promptRestart()) 206 | .catch(() => {}) 207 | }) 208 | 209 | program 210 | .command('list [search]') 211 | .alias('l') 212 | .description('List modules') 213 | .option('-d, --disabled', 'Only display disabled modules') 214 | .option('-e, --enabled', 'Only display enabled modules') 215 | .option('-s, --sort ', 'Sort results by column values') 216 | .action((search, cmd) => { 217 | setup() 218 | 219 | var columns = ['id', 'name', 'path', 'enabled', 'line'] 220 | 221 | if (cmd.disabled) mods = mods.filter(m => !m.enabled) 222 | if (cmd.enabled) mods = mods.filter(m => m.enabled) 223 | 224 | if (search) { 225 | var fuse = new Fuse(mods, { 226 | keys: ['name'], 227 | threshold: 0.1 228 | }) 229 | mods = fuse.search(search) 230 | } 231 | 232 | if (!mods.length) { 233 | console.log(chalk`{red ✘} No applicable modules found`) 234 | return 235 | } 236 | 237 | var sortOrder = 'enabled,name,path' 238 | var sortFuncs = [] 239 | 240 | if (cmd.sort) sortOrder = cmd.sort 241 | 242 | sortOrder 243 | .split(',') 244 | .map(s => { 245 | s = s.toLowerCase() 246 | 247 | if (columns.includes(s)) { 248 | var func = m => m[s] 249 | if (s === 'enabled') func = m => m.enabled ? 0 : 1 250 | sortFuncs.push(func) 251 | } 252 | }) 253 | 254 | if (sortFuncs.length) sort(mods).asc(sortFuncs) 255 | 256 | var data = columnify(mods, { 257 | columns, 258 | config: { 259 | enabled: { 260 | dataTransform: enabled => { 261 | if (enabled === 'true') { 262 | return chalk.green(enabled) 263 | } else { 264 | return chalk.red(enabled) 265 | } 266 | } 267 | } 268 | } 269 | }) 270 | 271 | console.log(data) 272 | }) 273 | 274 | program 275 | .command('switch [new-module]') 276 | .alias('s') 277 | .description('Switch out module(s)') 278 | .action((disable, enable, cmd) => { 279 | setup() 280 | 281 | if (!enable) enable = disable 282 | 283 | var enabledMods = mods.filter(m => m.enabled) 284 | var disabledMods = mods.filter(m => !m.enabled) 285 | 286 | chooseModule(disable, enabledMods) 287 | .then(mod => changeModuleStatus(mod, false)) 288 | .then(() => chooseModule(enable, disabledMods)) 289 | .then(mod => changeModuleStatus(mod, true)) 290 | .then(() => promptRestart()) 291 | .catch(() => {}) 292 | }) 293 | 294 | console.log(chalk`{greenBright Apache Module Manager v${version}} (https://github.com/kodie/apache-module-manager)`) 295 | console.log(chalk`by {cyanBright Kodie Grantham} (http://kodieg.com)\n`) 296 | 297 | program 298 | .version(version) 299 | .option('-a, --apache-config ', 'Apache config path') 300 | .option('-c, --config ', 'AMM config path') 301 | .parse(process.argv) 302 | 303 | if (process.argv.length < 3) program.help() 304 | --------------------------------------------------------------------------------