Compare commits

...

7 Commits

Author SHA1 Message Date
David Adam
1e68508b0c Authenticate connections to web_config service
- Require all requests to use a session path.
 - Use a redirect file to avoid exposing the URL on the command line, as
   it contains the session path.

Fix for CVE-2014-2914.
Closes #1438.
2014-08-07 18:53:31 +08:00
David Adam
2aac8e5dde Further fixes to universal variable server socket management
- Change fishd_path to std::string
- Warn, rather than exiting with an error, if the universal variable
  server path is not available, and provide more useful advice.
- Export the new __fishd_runtime_dir variable.
2014-08-07 18:53:16 +08:00
David Adam
209d8b7f2f Fix for CVE-2014-2905 - fishd restart required.
- Use a secure path for sockets (some code used under license from
   tmux).
 - Provide the secure path in the environment as $__fish_runtime_dir.
 - Link the new path to the old path to ease migration from earlier
   versions.

Closes #1359.

After installing fish built from or after this commit, you MUST
terminate all running fishd processes (`killall fishd`, `pkill fishd`
or similar). Distributors are encouraged to do this from within their
packaging scripts. fishd will restart automatically, and no data should
be lost.
2014-08-07 18:53:16 +08:00
David Adam
26663e042f Revert "Check effective credentials of socket peers"
This reverts commit aea9ad4965.

Just checking the credentials of the peer turns out to be
insufficient.
See https://github.com/fish-shell/fish-shell/issues/1436.
2014-08-07 18:52:27 +08:00
David Adam
55986120aa use mktemp(1) to generate temporary file names
Fix for CVE-2014-2906.

Closes a race condition in funced which would allow execution of
arbitrary code; closes a race condition in psub which would allow
alternation of the data stream.

Note that `psub -f` does not work (#1040); a fix should be committed
separately for ease of maintenance.
2014-04-27 12:23:24 +08:00
David Adam
aea9ad4965 Check effective credentials of socket peers
Fix for CVE-2014-2905.

Code for getpeereid() on non-BSD systems imported from the PostgreSQL
project under a BSD-style license.
2014-04-27 12:23:13 +08:00
Anders Bergh
216d32055d fish_config: Listen on both IPv6 and IPv4.
A subclass of TCPServer was created to deny any non-local connections and to
listen using an IPv6 socket.
2014-04-27 11:36:41 +08:00
14 changed files with 283 additions and 86 deletions

View File

@@ -24,6 +24,7 @@ parts of fish.
#include <stdio.h>
#include <dirent.h>
#include <sys/types.h>
#include <pwd.h>
#ifdef HAVE_SYS_IOCTL_H
#include <sys/ioctl.h>
@@ -2229,3 +2230,73 @@ char **make_null_terminated_array(const std::vector<std::string> &lst)
{
return make_null_terminated_array_helper(lst);
}
/**
Check, and create if necessary, a secure runtime path
Derived from tmux.c in tmux (http://tmux.sourceforge.net/)
*/
static int check_runtime_path(const char * path)
{
/*
* Copyright (c) 2007 Nicholas Marriott <nicm@users.sourceforge.net>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER
* IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
* OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
struct stat statpath;
u_int uid = geteuid();
if (mkdir(path, S_IRWXU) != 0 && errno != EEXIST)
return errno;
if (lstat(path, &statpath) != 0)
return errno;
if (!S_ISDIR(statpath.st_mode)
|| statpath.st_uid != uid
|| (statpath.st_mode & (S_IRWXG|S_IRWXO)) != 0)
return EACCES;
return 0;
}
/** Return the path of an appropriate runtime data directory */
std::string common_get_runtime_path()
{
const char *dir = getenv("XDG_RUNTIME_DIR");
const char *uname = getenv("USER");
std::string path;
if (uname == NULL)
{
const struct passwd *pw = getpwuid(getuid());
uname = pw->pw_name;
}
if (dir == NULL)
{
// /tmp/fish.user
dir = "/tmp/fish.";
path.reserve(strlen(dir) + strlen(uname));
path.append(dir);
path.append(uname);
if (check_runtime_path(path.c_str()) != 0)
{
debug(0, L"Runtime path not available. Try deleting the directory %s and restarting fish.", path.c_str());
path.clear();
}
}
else
{
path.reserve(strlen(dir));
path.append(dir);
}
return path;
}

View File

@@ -742,5 +742,7 @@ extern "C" {
__attribute__((noinline)) void debug_thread_error(void);
}
/** Return the path of an appropriate runtime data directory */
std::string common_get_runtime_path();
#endif

View File

@@ -1398,4 +1398,24 @@ POSSIBILITY OF SUCH DAMAGES.
<P>
<h2>License for code derived from tmux</h2>
Fish contains code derived from
<a href="http://tmux.sourceforge.net">tmux</a>, made available under an ISC
license.
<p>
Copyright (c) 2007 Nicholas Marriott <nicm@users.sourceforge.net>
<p>
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
<p>
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER
IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/

View File

@@ -57,7 +57,7 @@
#include "complete.h"
/** Command used to start fishd */
#define FISHD_CMD L"fishd ^ /tmp/fishd.log.%s"
#define FISHD_CMD L"fishd ^ $__fish_runtime_dir/fishd.log.%s"
// Version for easier debugging
//#define FISHD_CMD L"fishd"
@@ -672,10 +672,11 @@ void env_init(const struct config_paths_t *paths /* or NULL */)
env_set(L"version", version.c_str(), ENV_GLOBAL);
env_set(L"FISH_VERSION", version.c_str(), ENV_GLOBAL);
const env_var_t fishd_dir_wstr = env_get_string(L"FISHD_SOCKET_DIR");
const env_var_t user_dir_wstr = env_get_string(L"USER");
wchar_t * fishd_dir = fishd_dir_wstr.missing()?NULL:const_cast<wchar_t*>(fishd_dir_wstr.c_str());
std::string fishd_dir = common_get_runtime_path();
env_set(L"__fish_runtime_dir", str2wcstring(fishd_dir).c_str(), ENV_GLOBAL | ENV_EXPORT);
wchar_t * user_dir = user_dir_wstr.missing()?NULL:const_cast<wchar_t*>(user_dir_wstr.c_str());
env_universal_init(fishd_dir , user_dir ,

View File

@@ -61,7 +61,7 @@ static int get_socket_count = 0;
#define DEFAULT_RETRY_COUNT 15
#define DEFAULT_RETRY_DELAY 0.2
static wchar_t * path;
static const char * path;
static wchar_t *user;
static void (*start_fishd)();
static void (*external_callback)(fish_message_type_t type, const wchar_t *name, const wchar_t *val);
@@ -82,48 +82,19 @@ static int try_get_socket_once(void)
{
int s;
wchar_t *wdir;
wchar_t *wuname;
char *dir = 0;
wdir = path;
wuname = user;
if ((s = socket(AF_UNIX, SOCK_STREAM, 0)) == -1)
{
wperror(L"socket");
return -1;
}
if (wdir)
dir = wcs2str(wdir);
else
dir = strdup("/tmp");
std::string uname;
if (wuname)
{
uname = wcs2string(wuname);
}
else
{
struct passwd *pw = getpwuid(getuid());
if (pw && pw->pw_name)
{
uname = pw->pw_name;
}
}
std::string name;
name.reserve(strlen(dir) + uname.size() + strlen(SOCK_FILENAME) + 2);
name.append(dir);
name.append("/");
name.reserve(strlen(path) + strlen(SOCK_FILENAME) + 1);
name.append(path);
name.push_back('/');
name.append(SOCK_FILENAME);
name.append(uname);
free(dir);
debug(3, L"Connect to socket %s at fd %2", name.c_str(), s);
debug(3, L"Connect to socket %s at fd %d", name.c_str(), s);
struct sockaddr_un local = {};
local.sun_family = AF_UNIX;
@@ -271,23 +242,29 @@ static void reconnect()
}
void env_universal_init(wchar_t * p,
void env_universal_init(std::string p,
wchar_t *u,
void (*sf)(),
void (*cb)(fish_message_type_t type, const wchar_t *name, const wchar_t *val))
{
path=p;
path=p.c_str();
user=u;
start_fishd=sf;
external_callback = cb;
env_universal_server.fd = get_socket();
env_universal_common_init(&callback);
env_universal_read_all();
s_env_univeral_inited = true;
if (env_universal_server.fd >= 0)
if (p == "") {
debug(1, L"Could not connect to universal variable server. You will not be able to share variable values between fish sessions.");
}
else
{
env_universal_barrier();
env_universal_server.fd = get_socket();
env_universal_common_init(&callback);
env_universal_read_all();
s_env_univeral_inited = true;
if (env_universal_server.fd >= 0)
{
env_universal_barrier();
}
}
}

View File

@@ -17,7 +17,7 @@ extern connection_t env_universal_server;
/**
Initialize the envuni library
*/
void env_universal_init(wchar_t * p,
void env_universal_init(std::string p,
wchar_t *u,
void (*sf)(),
void (*cb)(fish_message_type_t type, const wchar_t *name, const wchar_t *val));

View File

@@ -27,7 +27,6 @@
#include <locale.h>
#include <dirent.h>
#include <signal.h>
#include <sys/stat.h>
#include <map>
#ifdef HAVE_SYS_SELECT_H
@@ -86,6 +85,13 @@
*/
#define ENV_UNIVERSAL_EOF 0x102
/**
Maximum length of socket filename
*/
#ifndef UNIX_PATH_MAX
#define UNIX_PATH_MAX 100
#endif
/**
A variable entry. Stores the value of a variable and whether it
should be exported. Obviously, it needs to be allocated large
@@ -417,7 +423,7 @@ void env_universal_common_init(void (*cb)(fish_message_type_t type, const wchar_
}
/**
Read one byte of date form the specified connection
Read one byte of date from the specified connection
*/
static int read_byte(connection_t *src)
{

View File

@@ -33,9 +33,9 @@
/**
The filename to use for univeral variables. The username is appended
The filename to use for univeral variables.
*/
#define SOCK_FILENAME "fishd.socket."
#define SOCK_FILENAME "fishd.socket"
/**
The different types of commands that can be sent between client/server
@@ -133,6 +133,11 @@ void try_send_all(connection_t *c);
*/
message_t *create_message(fish_message_type_t type, const wchar_t *key, const wchar_t *val);
/**
Constructs the fish socket filename
*/
std::string env_universal_common_get_socket_filename(void);
/**
Init the library
*/

View File

@@ -1032,8 +1032,8 @@ static void init(int mangle_descriptors, int out)
exit(1);
}
env_universal_init(0, 0, 0, 0);
std::string dir = common_get_runtime_path();
env_universal_init(dir, 0, 0, 0);
input_common_init(&interrupt_handler);
output_set_writer(&pager_buffered_writer);

View File

@@ -158,6 +158,32 @@ static int quit=0;
Constructs the fish socket filename
*/
static std::string get_socket_filename(void)
{
std::string dir = common_get_runtime_path();
if (dir == "") {
debug(0, L"Cannot access desired socket path.");
exit(EXIT_FAILURE);
}
std::string name;
name.reserve(dir.length() + strlen(SOCK_FILENAME) + 1);
name.append(dir);
name.push_back('/');
name.append(SOCK_FILENAME);
if (name.size() >= UNIX_PATH_MAX)
{
debug(1, L"Filename too long: '%s'", name.c_str());
exit(EXIT_FAILURE);
}
return name;
}
/**
Constructs the legacy socket filename
*/
static std::string get_old_socket_filename(void)
{
const char *dir = getenv("FISHD_SOCKET_DIR");
char *uname = getenv("USER");
@@ -174,10 +200,9 @@ static std::string get_socket_filename(void)
}
std::string name;
name.reserve(strlen(dir)+ strlen(uname)+ strlen(SOCK_FILENAME) + 1);
name.reserve(strlen(dir)+ strlen(uname)+ strlen("fishd.socket.") + 1);
name.append(dir);
name.push_back('/');
name.append(SOCK_FILENAME);
name.append("/fishd.socket.");
name.append(uname);
if (name.size() >= UNIX_PATH_MAX)
@@ -533,6 +558,7 @@ static int get_socket(void)
int exitcode = EXIT_FAILURE;
struct sockaddr_un local;
const std::string sock_name = get_socket_filename();
const std::string old_sock_name = get_old_socket_filename();
/*
Start critical section protected by lock
@@ -590,6 +616,19 @@ static int get_socket(void)
doexit = 1;
}
// Attempt to hardlink the old socket name so that old versions of fish keep working on upgrade
// Not critical if it fails
if (unlink(old_sock_name.c_str()) != 0 && errno != ENOENT)
{
debug(0, L"Could not create legacy socket path");
wperror(L"unlink");
}
else if (link(sock_name.c_str(), old_sock_name.c_str()) != 0)
{
debug(0, L"Could not create legacy socket path");
wperror(L"link");
}
unlock:
(void)unlink(lockfile.c_str());
debug(4, L"Released lockfile: %s", lockfile.c_str());
@@ -860,6 +899,18 @@ static void init()
load();
}
/**
Clean up behind ourselves
*/
static void cleanup()
{
if (unlink(get_old_socket_filename().c_str()) != 0)
{
debug(0, L"Could not remove legacy socket path");
wperror(L"unlink");
}
}
/**
Main function for fishd
*/
@@ -961,6 +1012,7 @@ int main(int argc, char ** argv)
if (quit)
{
save();
cleanup();
exit(0);
}
@@ -970,6 +1022,7 @@ int main(int argc, char ** argv)
if (errno != EINTR)
{
wperror(L"select");
cleanup();
exit(1);
}
}
@@ -982,6 +1035,7 @@ int main(int argc, char ** argv)
&t)) == -1)
{
wperror(L"accept");
cleanup();
exit(1);
}
else
@@ -1058,6 +1112,7 @@ int main(int argc, char ** argv)
{
debug(0, L"No more clients. Quitting");
save();
cleanup();
break;
}

View File

@@ -81,11 +81,7 @@ function funced --description 'Edit function definition'
return 0
end
set -q TMPDIR; or set -l TMPDIR /tmp
set -l tmpname (printf "$TMPDIR/fish_funced_%d_%d.fish" %self (random))
while test -f $tmpname
set tmpname (printf "$TMPDIR/fish_funced_%d_%d.fish" %self (random))
end
set tmpname (mktemp -t fish_funced.XXXXXXXXXX)
if functions -q -- $funcname
functions -- $funcname > $tmpname

View File

@@ -45,21 +45,16 @@ function psub --description "Read from stdin into a file and output the filename
return
end
# Find unique file name for writing output to
while true
set filename /tmp/.psub.(echo %self).(random);
if not test -e $filename
break;
end
end
if test use_fifo = 1
# Write output to pipe. This needs to be done in the background so
# that the command substitution exits without needing to wait for
# all the commands to exit
set dir (mktemp -d /tmp/.psub.XXXXXXXXXX); or return
set filename $dir/psub.fifo
mkfifo $filename
cat >$filename &
else
set filename (mktemp /tmp/.psub.XXXXXXXXXX)
cat >$filename
end

View File

@@ -556,7 +556,7 @@ function switch_tab(new_tab) {
if (new_tab == 'tab_colors') {
/* Keep track of whether this is the first element */
var first = true
run_get_request('/colors/', function(key_and_values){
run_get_request('colors/', function(key_and_values){
/* Result is name, description, value */
var key = key_and_values[0]
var description = key_and_values[1]
@@ -577,7 +577,7 @@ function switch_tab(new_tab) {
sample_prompts.length = 0
/* Color the first one blue */
var first = true;
run_get_request('/sample_prompts/', function(sample_prompt){
run_get_request('sample_prompts/', function(sample_prompt){
var name = sample_prompt['name']
sample_prompts[name] = sample_prompt
var color = first ? '66F' : 'AAA'
@@ -594,7 +594,7 @@ function switch_tab(new_tab) {
} else if (new_tab == 'tab_functions') {
/* Keep track of whether this is the first element */
var first = true
run_get_request('/functions/', function(contents){
run_get_request('functions/', function(contents){
var elem = create_master_element(contents, false/* description */, 'AAAAAA', '11pt', select_function_master_element)
if (first) {
/* It's the first element, so select it, so something gets selected */
@@ -606,7 +606,7 @@ function switch_tab(new_tab) {
$('#master_detail_table').show()
wants_data_table = false
} else if (new_tab == 'tab_variables') {
run_get_request_with_bulk_handler('/variables/', function(json_contents){
run_get_request_with_bulk_handler('variables/', function(json_contents){
var rows = new Array()
for (var i = 0; i < json_contents.length; i++) {
var contents = json_contents[i]
@@ -622,7 +622,7 @@ function switch_tab(new_tab) {
} else if (new_tab == 'tab_history') {
// Clear the history map
history_element_map.length = 0
run_get_request_with_bulk_handler('/history/', function(json_contents){
run_get_request_with_bulk_handler('history/', function(json_contents){
start = new Date().getTime()
var rows = new Array()
for (var i = 0; i < json_contents.length; i++) {
@@ -757,7 +757,7 @@ function select_color_master_element(elem) {
function select_function_master_element(elem) {
select_master_element(elem)
run_post_request('/get_function/', {
run_post_request('get_function/', {
what: current_master_element_name()
}, function(contents){
/* Replace leading tabs and groups of four spaces at the beginning of a line with two spaces. */
@@ -773,7 +773,7 @@ function select_sample_prompt_master_element(elem) {
select_master_element(elem)
var name = current_master_element_name()
sample_prompt = sample_prompts[name]
run_post_request('/get_sample_prompt/', {
run_post_request('get_sample_prompt/', {
what: sample_prompt['function']
}, function(keys_and_values){
var prompt_func = keys_and_values['function']
@@ -788,7 +788,7 @@ function select_sample_prompt_master_element(elem) {
function select_current_prompt_master_element(elem) {
$('.prompt_save_button').hide()
select_master_element(elem)
run_get_request_with_bulk_handler('/current_prompt/', function(keys_and_values){
run_get_request_with_bulk_handler('current_prompt/', function(keys_and_values){
var prompt_func = keys_and_values['function']
var prompt_demo = keys_and_values['demo']
var prompt_font_size = keys_and_values['font_size']
@@ -801,7 +801,7 @@ function select_current_prompt_master_element(elem) {
function save_current_prompt() {
var name = current_master_element_name()
var sample_prompt = sample_prompts[name]
run_post_request('/set_prompt/', {
run_post_request('set_prompt/', {
what: sample_prompt['function']
}, function(contents){
if (contents == "OK") {
@@ -817,7 +817,7 @@ function post_style_to_server() {
if (! style)
return
run_post_request('/set_color/', {
run_post_request('set_color/', {
what: current_master_element_name(),
color: style.color,
background_color: style.background_color,
@@ -1221,7 +1221,7 @@ function escape_HTML(foo) {
function tell_fish_to_delete_element(idx) {
var row_elem = $('#data_table_row_' + idx)
var txt = history_element_map[idx]
run_post_request('/delete_history_item/', {
run_post_request('delete_history_item/', {
what: txt
}, function(contents){
if (contents == "OK") {

View File

@@ -17,7 +17,7 @@ else:
from urllib.parse import parse_qs
import webbrowser
import subprocess
import re, socket, os, sys, cgi, select, time, glob
import re, socket, os, sys, cgi, select, time, glob, random, string
try:
import json
except ImportError:
@@ -252,6 +252,16 @@ class FishVar:
if self.exported: flags.append('exported')
return [self.name, self.value, ', '.join(flags)]
class FishConfigTCPServer(SocketServer.TCPServer):
"""TCPServer that only accepts connections from localhost (IPv4/IPv6)."""
WHITELIST = set(['::1', '::ffff:127.0.0.1', '127.0.0.1'])
address_family = socket.AF_INET6
def verify_request(self, request, client_address):
return client_address[0] in FishConfigTCPServer.WHITELIST
class FishConfigHTTPRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
def write_to_wfile(self, txt):
@@ -477,9 +487,16 @@ class FishConfigHTTPRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
else: font_size = '18pt'
return font_size
def do_GET(self):
p = self.path
authpath = '/' + authkey
if p.startswith(authpath):
p = p[len(authpath):]
else:
return self.send_error(403)
self.path = p
if p == '/colors/':
output = self.do_get_colors()
elif p == '/functions/':
@@ -511,6 +528,14 @@ class FishConfigHTTPRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
def do_POST(self):
p = self.path
authpath = '/' + authkey
if p.startswith(authpath):
p = p[len(authpath):]
else:
return self.send_error(403)
self.path = p
if IS_PY2:
ctype, pdict = cgi.parse_header(self.headers.getheader('content-type'))
else: # Python 3
@@ -574,7 +599,19 @@ class FishConfigHTTPRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
def log_request(self, code='-', size='-'):
""" Disable request logging """
pass
redirect_template_html = """
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="refresh" content="0;URL='%s'" />
</head>
<body>
<p><a href="%s">Start the Fish Web config</a></p>
</body>
</html>
"""
# find fish
fish_bin_dir = os.environ.get('__fish_bin_dir')
fish_bin_path = None
@@ -610,12 +647,15 @@ initial_wd = os.getcwd()
where = os.path.dirname(sys.argv[0])
os.chdir(where)
# Generate a 16-byte random key as a hexadecimal string
authkey = hex(random.getrandbits(16*4))[2:]
# Try to find a suitable port
PORT = 8000
while PORT <= 9000:
try:
Handler = FishConfigHTTPRequestHandler
httpd = SocketServer.TCPServer(("", PORT), Handler)
httpd = FishConfigTCPServer(("::", PORT), Handler)
# Success
break;
except socket.error:
@@ -639,9 +679,36 @@ if len(sys.argv) > 1:
initial_tab = '#' + tab
break
url = 'http://localhost:%d/%s' % (PORT, initial_tab)
print("Web config started at '%s'. Hit enter to stop." % url)
webbrowser.open(url)
url = 'http://localhost:%d/%s/%s' % (PORT, authkey, initial_tab)
# Create temporary file to hold redirect to real server
# This prevents exposing the URL containing the authentication key on the command line
# (see CVE-2014-2914 or https://github.com/fish-shell/fish-shell/issues/1438)
if 'XDG_CACHE_HOME' in os.environ:
dirname = os.path.expanduser(os.path.expandvars('$XDG_CACHE_HOME/fish/'))
else:
dirname = os.path.expanduser('~/.cache/fish/')
os.umask(0o0077)
try:
os.makedirs(dirname, 0o0700)
except OSError as e:
if e.errno == 17:
pass
else:
raise e
randtoken = ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(6))
filename = dirname + 'web_config-%s.html' % randtoken
f = open(filename, 'w')
f.write(redirect_template_html % (url, url))
f.close()
# Open temporary file as URL
fileurl = 'file://' + filename
print("Web config started at '%s'. Hit enter to stop." % fileurl)
webbrowser.open(fileurl)
# Select on stdin and httpd
stdin_no = sys.stdin.fileno()
@@ -658,3 +725,5 @@ try:
except KeyboardInterrupt:
print("\nShutting down.")
# Clean up temporary file
os.remove(filename)