diff options
author | Simon Rettberg | 2024-10-10 15:20:34 +0200 |
---|---|---|
committer | Simon Rettberg | 2024-10-10 15:20:34 +0200 |
commit | 8a215c492913d6d329a7b64229738d028c7489de (patch) | |
tree | 582b057745bdf8efc9a322070a6c004cd79cf0d0 | |
parent | [webinterface] Refactor TM-Calls for https changes (diff) | |
download | slx-admin-8a215c492913d6d329a7b64229738d028c7489de.tar.gz slx-admin-8a215c492913d6d329a7b64229738d028c7489de.tar.xz slx-admin-8a215c492913d6d329a7b64229738d028c7489de.zip |
[webinterface] Add simple API to remotely supply a certificate
6 files changed, 179 insertions, 45 deletions
diff --git a/modules-available/webinterface/api.inc.php b/modules-available/webinterface/api.inc.php new file mode 100644 index 00000000..be374ed5 --- /dev/null +++ b/modules-available/webinterface/api.inc.php @@ -0,0 +1,38 @@ +<?php + +Header('Content-Type: application/json; charset=utf-8'); + +$apikey = WebInterface::getApiKey(); +if (empty($apikey) || $apikey !== Request::post('token', null, 'string')) { + http_response_code(403); + die('{"error":"Unauthorized"}'); +} + +$newKey = Request::post('privkey', null, 'string'); +if (empty($newKey)) { + http_response_code(400); + die('{"error":"privkey missing"}'); +} +$newCert = Request::post('cert', null, 'string'); +if (empty($newCert)) { + http_response_code(400); + die('{"error":"cert missing"}'); +} + +// Import will try to validate the certificate too +$task = WebInterface::tmImportCustomCert($newKey, $newCert); +$task = Taskmanager::waitComplete($task, 10000); +if (!Taskmanager::isTask($task)) { + http_send_status(500); + die('{"error":"error communicating with task manager"}'); +} +if (Taskmanager::isFailed($task)) { + // Send task data back to user, might contain more information + http_send_status(400); + die(json_encode([ + 'error' => 'import failed', + 'data' => $task['data'] ?? [], + ])); +} + +die('{"message":"OK"}');
\ No newline at end of file diff --git a/modules-available/webinterface/inc/webinterface.inc.php b/modules-available/webinterface/inc/webinterface.inc.php index 276110eb..20be6545 100644 --- a/modules-available/webinterface/inc/webinterface.inc.php +++ b/modules-available/webinterface/inc/webinterface.inc.php @@ -110,4 +110,17 @@ class WebInterface return $task['id'] ?? null; } + public static function getApiKey(): ?string + { + $key = Property::get(self::PROP_API_KEY, null); + if (empty($key)) + return null; + return $key; + } + + public static function setApiKey(?string $key): void + { + Property::set(self::PROP_API_KEY, empty($key) ? null : $key); + } + }
\ No newline at end of file diff --git a/modules-available/webinterface/lang/de/template-tags.json b/modules-available/webinterface/lang/de/template-tags.json index e5a149b4..05cd88b0 100644 --- a/modules-available/webinterface/lang/de/template-tags.json +++ b/modules-available/webinterface/lang/de/template-tags.json @@ -22,6 +22,14 @@ "lang_generalHttpsOptions": "Allgemeine Optionen", "lang_generatedSelected": "Der Server verwendet zur Zeit ein automatisch generiertes, selbst signiertes Zertifikat.", "lang_hidePasswords": "Passw\u00f6rter maskieren", + "lang_httpsApiKey": "API Token", + "lang_httpsApiKeyDeleteConfirm": "Aktuelles Token l\u00f6schen?", + "lang_httpsApiKeyDescription": "Hier k\u00f6nnen Sie ein API-Token generieren, mit dem Sie das Zertifikat von einem anderen Rechner aus aktualisieren k\u00f6nnen. Um die Funktion zu deaktivieren, l\u00f6schen Sie das Token wieder.", + "lang_httpsApiKeyRegenerateConfirm": "Aktuelles Token verwerfen und neu generieren?", + "lang_httpsApiPostExample": "Ein Beispiel mittels curl", + "lang_httpsApiPostMaybeInsecure": "Es ist ggf. sinnvoll, zum \u00fcbermitteln die Zertifikatsverifikation abzuschalten, wenn z.B. zu erwarten ist, dass das Zertifikat des Satellitenservers bereits abgelaufen ist, oder selbstsigniert. Bei curl geht das unter Verwendung des Paramters -k", + "lang_httpsApiPostText": "Um ein neues Zertifikat einzuspielen, senden Sie einen POST-Request an die folgende URL, mit den POST-Feldern \"token\" (obiges Token), sowie \"privkey\" (privater Schl\u00fcssel des Zertifikats) und \"cert\" (Zertifikat, ggf. mit angeh\u00e4ngten Intermediates, aka fullchain), beides im PEM-Format.", + "lang_httpsCurrentApiKey": "Aktuelles Token", "lang_httpsDescription": "Hier k\u00f6nnen Sie festlegen, ob das Web-Interface auch per HTTPS erreichbar sein soll, und welches Zertifikat daf\u00fcr verwendet werden soll.", "lang_httpsOptionNoChange": "Nichts \u00e4ndern", "lang_httpsRedirect": "Anfragen per HTTP immer auf HTTPS umleiten (sofern aktiviert)", @@ -39,6 +47,7 @@ "lang_privateKey": "Bitte f\u00fcgen Sie hier den privaten Schl\u00fcssel ein, der zum obigen Zertifikat geh\u00f6rt. Er muss ebenfalls im \"pem\"-Format vorliegen, und sieht wie folgt aus:", "lang_randomCert": "Neues selbstsigniertes Zertifikat generieren", "lang_redirectDomain": "Bei Browserzugriff \u00fcber eine andere Domain oder per IP auf die Prim\u00e4rdomain des Zertifikats umleiten", + "lang_regenerate": "(Re)generieren", "lang_showPasswords": "Passw\u00f6rter anzeigen", "lang_suppliedSelected": "Der Server verwendet zur Zeit ein \u00fcber die Option \"Eigenes Zertifikat\" hochgeladenes Zertifikat.", "lang_unknownSelected": "Unbekanntes oder ung\u00fcltiges Zertifikat vorhanden. Wahrscheinlich wurde der Server von einer alten Version aktualisiert. Um diese Meldung zu entfernen, die HTTPS-Konfiguration erneut vornehmen.", diff --git a/modules-available/webinterface/lang/en/template-tags.json b/modules-available/webinterface/lang/en/template-tags.json index a1cbff7f..9c290083 100644 --- a/modules-available/webinterface/lang/en/template-tags.json +++ b/modules-available/webinterface/lang/en/template-tags.json @@ -22,6 +22,14 @@ "lang_generalHttpsOptions": "General options", "lang_generatedSelected": "The server is currently using an automatically generated, self-signed certificate.", "lang_hidePasswords": "Mask passwords", + "lang_httpsApiKey": "API token", + "lang_httpsApiKeyDeleteConfirm": "Delete current token?", + "lang_httpsApiKeyDescription": "Here you can generate a token to remotely supply a certificate. To disable this feature, delete the token.", + "lang_httpsApiKeyRegenerateConfirm": "Delete current token and generate new one?", + "lang_httpsApiPostExample": "An example using curl", + "lang_httpsApiPostMaybeInsecure": "It might make sense to disable certificate validation while submitting a new one, in case the one on the satellite server is already expired or expected to be self-signed. For curl this can be achieved by passing the parameter \"-k\"", + "lang_httpsApiPostText": "To apply a new certificate, send a POST-Request to the following URL, while setting the POST fields \"token\" (as shown above), as well as \"privkey\" (private key of cert) and \"cert\" (certificate, if applicable with intermediates attaches, aka fullchain), both expected in PEM format.", + "lang_httpsCurrentApiKey": "Current token", "lang_httpsDescription": "Here you can set whether the web interface should be accessible via https. You can choose if you want to use a random self signed certificate, or supply your own.", "lang_httpsOptionNoChange": "Don't change", "lang_httpsRedirect": "Redirect incoming HTTP requests to HTTPS (if enabled).", @@ -39,6 +47,7 @@ "lang_privateKey": "Please paste the private key belonging to the certificate here. It has to be in \"pem\" format too, which should look like this:", "lang_randomCert": "Generate new self-signed certificate", "lang_redirectDomain": "Redirect browser to certificate's primary domain if accessed via unknown domain name or IP address", + "lang_regenerate": "(Re)generate", "lang_showPasswords": "Show passwords", "lang_suppliedSelected": "The server is currently using a certificate supplied using the \"Supply own certificate\" option.", "lang_unknownSelected": "Unknown or invalid certificate in use. The server was probably updated from an old version while HTTPS was already enabled. Redo the HTTPS configuration steps to get rid of this message.", diff --git a/modules-available/webinterface/page.inc.php b/modules-available/webinterface/page.inc.php index 318dd82c..d21c627e 100644 --- a/modules-available/webinterface/page.inc.php +++ b/modules-available/webinterface/page.inc.php @@ -10,7 +10,8 @@ class Page_WebInterface extends Page Message::addError('main.no-permission'); Util::redirect('?do=Main'); } - switch (Request::post('action')) { + $action = Request::post('action', null, 'string'); + switch ($action) { case 'https': User::assertPermission("edit.https"); $this->actionConfigureHttps(); @@ -23,7 +24,15 @@ class Page_WebInterface extends Page User::assertPermission("edit.design"); $this->actionCustomization(); break; + case 'https-api-key-generate': + case 'https-api-key-delete': + User::assertPermission("edit.https"); + $this->handleApiKey(substr($action, 14)); + break; default: + if ($action !== null) { + Message::addWarning('main.invalid-action', $action); + } } if (Request::isPost()) { Util::redirect('?do=webinterface'); @@ -154,8 +163,11 @@ class Page_WebInterface extends Page } $data['acmeMail'] = Acme::getMail(); $data['acmeDomains'] = $domains; - $data['acmeKeyId'] = Acme::getKeyId(); - $data['acmeHmacKey'] = Acme::getHmacKey(); + if (User::hasPermission("edit.https")) { + $data['acmeKeyId'] = Acme::getKeyId(); + $data['acmeHmacKey'] = Acme::getHmacKey(); + $data['httpsApiKey'] = WebInterface::getApiKey(); + } // $type might have changed in above block $data[$type . 'Selected'] = true; // Show cert info if possible @@ -177,6 +189,7 @@ class Page_WebInterface extends Page $data['certExpireClass'] = implode(' ', $class); } } + $data['httpsApiKeyPostUrl'] = ($https ? 'https' : 'http') . '://' . $_SERVER['HTTP_HOST'] . '/slx-admin/api.php?do=webinterface'; Permission::addGlobalTags($data['perms'], null, ['edit.https']); Render::addTemplate('https', $data); // @@ -283,5 +296,14 @@ class Page_WebInterface extends Page return Acme::issueNewCertificate($wipeAll); } + private function handleApiKey(string $substr) + { + if ($substr === 'generate') { + WebInterface::setApiKey(Util::randomUuid()); + } elseif ($substr === 'delete') { + WebInterface::setApiKey(null); + } + } + } diff --git a/modules-available/webinterface/templates/https.html b/modules-available/webinterface/templates/https.html index 5198c299..dbffa9b7 100644 --- a/modules-available/webinterface/templates/https.html +++ b/modules-available/webinterface/templates/https.html @@ -1,57 +1,64 @@ -<form action="?do=WebInterface" method="post"> - <input type="hidden" name="token" value="{{token}}"> - <input type="hidden" name="action" value="https"> - <div class="panel panel-default"> - <div class="panel-heading">{{lang_httpsSettings}}</div> - <div class="panel-body"> - <p>{{lang_httpsDescription}}</p> - {{^httpsUsed}} +<div class="panel panel-default"> + <div class="panel-heading">{{lang_httpsSettings}}</div> + <div class="panel-body"> + <p>{{lang_httpsDescription}}</p> + {{^httpsUsed}} <p>{{lang_youreNotUsingHttps}}</p> - {{/httpsUsed}} - {{#httpsUsed}} + {{/httpsUsed}} + {{#httpsUsed}} <p>{{lang_youreUsingHttps}}</p> - {{/httpsUsed}} - <div class="text-info slx-bold"> - {{#offSelected}} + {{/httpsUsed}} + <div class="text-info slx-bold"> + {{#offSelected}} <p>{{lang_offSelected}}</p> - {{/offSelected}} - {{#unknownSelected}} + {{/offSelected}} + {{#unknownSelected}} <p>{{lang_unknownSelected}}</p> - {{/unknownSelected}} - {{#generatedSelected}} + {{/unknownSelected}} + {{#generatedSelected}} <p>{{lang_generatedSelected}}</p> - {{/generatedSelected}} - {{#suppliedSelected}} + {{/generatedSelected}} + {{#suppliedSelected}} <p>{{lang_suppliedSelected}}</p> - {{/suppliedSelected}} - {{#acmeSelected}} - <p>{{lang_acmeSelected}}</p> - {{/acmeSelected}} - </div> - <table class="slx-table"> + {{/suppliedSelected}} + {{#acmeSelected}} + <p>{{lang_acmeSelected}}</p> + {{/acmeSelected}} + </div> + <table class="slx-table"> {{#certIssuer}} - <tr><td>{{lang_certIssuer}}:</td><td>{{.}}</td></tr> + <tr> + <td>{{lang_certIssuer}}:</td> + <td>{{.}}</td> + </tr> {{/certIssuer}} {{#certExpire}} - <tr><td>{{lang_certExpireTime}}:</td><td class="{{certExpireClass}}">{{.}}</td></tr> + <tr> + <td>{{lang_certExpireTime}}:</td> + <td class="{{certExpireClass}}">{{.}}</td> + </tr> {{/certExpire}} {{#certDomains.0}} <tr> <td style="vertical-align:top">{{lang_currentCertDomains}}:</td> <td> - {{#certDomains}} - <div>{{.}}</div> - {{/certDomains}} + {{#certDomains}} + <div>{{.}}</div> + {{/certDomains}} </td> </tr> {{/certDomains.0}} - </table> + </table> + <form action="?do=WebInterface" method="post"> + <input type="hidden" name="token" value="{{token}}"> + <input type="hidden" name="action" value="https"> <label>{{lang_generalHttpsOptions}}</label> <div class="input-group row-select"> <span class="input-group-addon"> <span class="checkbox"> - <input id="httpsredirect" type="checkbox" name="httpsredirect" value="on" {{redirect_checked}} {{perms.edit.https.disabled}}> + <input id="httpsredirect" type="checkbox" name="httpsredirect" + value="on" {{redirect_checked}} {{perms.edit.https.disabled}}> <label></label> </span> </span> @@ -62,7 +69,8 @@ <div class="input-group row-select"> <span class="input-group-addon"> <span class="checkbox"> - <input id="usehsts" type="checkbox" name="usehsts" value="on" {{hsts_checked}} {{perms.edit.https.disabled}}> + <input id="usehsts" type="checkbox" name="usehsts" + value="on" {{hsts_checked}} {{perms.edit.https.disabled}}> <label></label> </span> </span> @@ -73,7 +81,8 @@ <div class="input-group row-select"> <span class="input-group-addon"> <span class="checkbox"> - <input id="redirdomain" type="checkbox" name="redirdomain" value="on" {{redirdomain_checked}} {{perms.edit.https.disabled}}> + <input id="redirdomain" type="checkbox" name="redirdomain" + value="on" {{redirdomain_checked}} {{perms.edit.https.disabled}}> <label></label> </span> </span> @@ -89,7 +98,8 @@ <div class="input-group row-select"> <span class="input-group-addon"> <span class="radio"> - <input id="https-do-nothing" type="radio" name="mode" value="noop" {{perms.edit.https.disabled}} checked> + <input id="https-do-nothing" type="radio" name="mode" value="noop" {{perms.edit.https.disabled}} + checked> <label></label> </span> </span> @@ -99,17 +109,17 @@ </div> {{#httpsEnabled}} - <div class="input-group row-select"> + <div class="input-group row-select"> <span class="input-group-addon"> <span class="radio"> <input id="moff" type="radio" name="mode" value="off" {{perms.edit.https.disabled}}> <label></label> </span> </span> - <span class="form-control"> - {{lang_noHttps}} - </span> - </div> + <span class="form-control"> + {{lang_noHttps}} + </span> + </div> {{/httpsEnabled}} <div class="input-group row-select"> @@ -218,6 +228,39 @@ MIIFfTCCA... {{lang_save}} </button> </div> - </div> + </form> + <div class="clearfix"></div> + <hr> + <h5 class="slx-bold">{{lang_httpsApiKey}}</h5> + <p>{{lang_httpsApiKeyDescription}}</p> + <form action="?do=WebInterface" method="post"> + <input type="hidden" name="token" value="{{token}}"> + <div class="input-group"> + <span class="input-group-addon"><label for="https-api-key">{{lang_httpsCurrentApiKey}}</label></span> + <input class="form-control" type="text" id="https-api-key" readonly value="{{httpsApiKey}}"> + <div class="input-group-btn"> + <button type="submit" name="action" value="https-api-key-generate" class="btn btn-success {{perms.edit.https.disabled}}" + {{#httpsApiKey}}data-confirm="{{lang_httpsApiKeyRegenerateConfirm}}"{{/httpsApiKey}}> + <span class="glyphicon glyphicon-refresh"></span> + {{lang_regenerate}} + </button> + </div> + <div class="input-group-btn"> + <button type="submit" name="action" value="https-api-key-delete" class="btn btn-danger {{perms.edit.https.disabled}}" + {{#httpsApiKey}}data-confirm="{{lang_httpsApiKeyDeleteConfirm}}"{{/httpsApiKey}}> + <span class="glyphicon glyphicon-trash"></span> + {{lang_delete}} + </button> + </div> + </div> + <br> + <br> + {{lang_httpsApiPostText}} + <div class="slx-bold">{{httpsApiKeyPostUrl}}</div> + <br> + {{lang_httpsApiPostExample}}: + <div class="monospace">curl -L --data-urlencode "token=123456" --data-urlencode "privkey@/path/to/privkey.pem" --data-urlencode "cert@/path/to/cert.pem" "{{httpsApiKeyPostUrl}}"</div> + {{lang_httpsApiPostMaybeInsecure}} + </form> </div> -</form> +</div> |