├── README.md ├── auth.py ├── frontend ├── admin.php ├── auth_classes.php ├── change.php ├── change_admin.php ├── change_pass.html ├── config.json ├── config_handler.php ├── css │ ├── bootstrap-theme.css │ ├── bootstrap-theme.css.map │ ├── bootstrap-theme.min.css │ ├── bootstrap.css │ ├── bootstrap.css.map │ ├── bootstrap.min.css │ └── datepicker.css ├── dashboard.php ├── database_connector.php ├── fonts │ ├── glyphicons-halflings-regular.eot │ ├── glyphicons-halflings-regular.svg │ ├── glyphicons-halflings-regular.ttf │ ├── glyphicons-halflings-regular.woff │ └── glyphicons-halflings-regular.woff2 ├── index.php ├── insert.php ├── insert_admin.php ├── install.php ├── js │ ├── admin.js │ ├── bluebase.js │ ├── bootstrap-datepicker.js │ ├── bootstrap.min.js │ ├── chart.min.js │ └── jquery.min.js ├── login.php ├── logout.php ├── request.php ├── request_admin.php └── user_change.php └── schema.sql /README.md: -------------------------------------------------------------------------------- 1 | # BlueBase 2 |  3 |  4 |  5 | BlueBase is an authentication program written in PHP and Python for the purpose of providing an easy and lightweight username/password authentication system for OpenVPN, specifically for my circumvention project known as BlueBust. [BlueBust](https://lizard.company/boards/lcrad/2-bluebust-setup) is a combination of OpenVPN, stunnel, and iptables (with an optional BIND server) to create a robust filter circumvention system that was specifically targeted at BlueCoat. BlueBase is an extension of this body of work as the original BlueBust system relied on a Public Key Infrastructure in order to authenticate users, which is slightly more secure than username/password combinations, but has a higher administrative burden. This new system allows temporary disabling and automatic expiration of users, which allows for monetization or at least more control to make sure users do not abuse the service. 6 | 7 | Passwords are salted and hashed using Bcrypt, and the expiration date is NULLABLE but requires ISO 8601 format if used. The dashboard also comes with all the necessary javascript and css files within the directory so there is no need to make any external requests and possibly require unencrypted content if you run it over SSL. The dashboard allows you to create, edit, delete, and examine all the users and gives a nice little pie chart of the percentage of users enabled, disabled, and expired. The dashboard is now protected using standard PHP login system with passwords that are also secured by bcrypt. 8 | 9 | *Published: 2015-08-19* 10 | 11 | ## What this means 12 | 13 | Much more scalability and ease of administration to run any OpenVPN server when implemented. Allows people to make money off of their OpenVPN service by adding the capability to automatically expire users, but it is a completely optional field. Gives more privacy to users, because deleting the record removes every trace of that user but using easy-rsa a revoked user must be permanently accounted for within the system. 14 | 15 | ## Setting up 16 | 17 | These instructions are designed for Debian 8 systems. Debian 7 systems could work with this system, however the PHP version does not support password hashing, which means an extra file needs to be included wherever the password_hash() method is used. 18 | 19 | Run the following command: 20 | 21 | `sudo apt-get install python python-bcrypt ntp php5 php5-mysql apache2 openvpn easy-rsa python-mysql.connector` 22 | 23 | OpenVPN and other configurations are not detailed here, look to the BlueBust setup for specific instructions on how to setup a BlueBust instance. 24 | 25 | 1. Create a database and use the *install.php* file to then import the schema, then configure the parameters located within the *auth.py* file and the *config.json* file to reflect the database and respective user. 26 | 2. Place the *auth.py* file in the */etc/openvpn/* directory and place the contents of the *frontend* directory in your webroot or in a directory within the webroot. (Webroot on Debian is */var/www/*) 27 | 3. Give all ownership to the group/user www-data. `sudo chown -R www-data:www-data /var/www/` 28 | 4. Add the following lines to your openvpn config to switch from using certificate based authentication to username/password. 29 | ``` 30 | auth-user-pass-verify auth.py via-file 31 | client-cert-not-required 32 | username-as-common-name 33 | ``` 34 | 5. Go to the folder where you installed it and login using the following credentials. Username: **admin** Password: **password** 35 | 6. Change your password and username for security. 36 | 37 | ## TODO 38 | - [ ] Stress testing 39 | - [ ] Adding support for multiple database systems 40 | 41 | ## License 42 | 43 | This program is released under [GPLv3](https://www.gnu.org/licenses/gpl.html). 44 | -------------------------------------------------------------------------------- /auth.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python2 2 | 3 | import bcrypt 4 | import sys 5 | import mysql.connector 6 | from datetime import date 7 | 8 | ## Database connection configuration 9 | datahost = 'localhost' 10 | datauser = 'bluebase' 11 | dataname = 'bluebase' 12 | datapass = 'yourpasswordhere' 13 | 14 | # connect to database and create cursor 15 | conn = mysql.connector.connect(user=datauser, password=datapass, host=datahost, database=dataname) 16 | cursor = conn.cursor() 17 | today = date.today() 18 | 19 | # read in the temporary file 20 | file = open(sys.argv[1], 'r') 21 | username = file.readline().rstrip('\n') 22 | password = file.readline().rstrip('\n') 23 | 24 | # make the query such that it only gets accounts that are not expired or disabled 25 | query = "SELECT `hashed_pass` FROM `users` WHERE `username` = %s AND (`expires` >= %s OR `expires` IS NULL) AND `disabled` = 0" 26 | params = (username, today.isoformat()) 27 | cursor.execute(query, params) 28 | 29 | # we just need one record 30 | row = cursor.fetchone() 31 | 32 | # make sure to close the connection 33 | def close_connection(): 34 | cursor.close() 35 | conn.close() 36 | 37 | #if there isn't anything then there the user doesn't exist, is disabled or expired 38 | if row != None: 39 | hash_pass = row[0] 40 | hash_pass = hash_pass.replace('$2y$', '$2a$') 41 | # hash the password and make sure they match 42 | if bcrypt.hashpw(password, hash_pass) == hash_pass: 43 | close_connection() 44 | sys.exit(0) 45 | else: 46 | close_connection() 47 | sys.exit(1) 48 | else: 49 | close_connection() 50 | sys.exit(1) 51 | -------------------------------------------------------------------------------- /frontend/admin.php: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 |
11 | 12 |Username | 67 |Edit | 68 |
---|
User ID | 98 |Username | 99 |First Name | 100 |Last Name | 101 |Expiration | 102 |Disabled | 103 |Edit | 104 |
---|
Please input the database credentials for BlueBase, and it will install the schema.
37 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /frontend/js/admin.js: -------------------------------------------------------------------------------- 1 | // will be called when the window loads 2 | window.onload = loadElements; 3 | var adminJson; 4 | // prepares all necessary elements and fetches data 5 | function loadElements(){ 6 | $.getJSON('request_admin.php', processInfo); 7 | } 8 | // called whenever a change or addition is made 9 | function refreshData(){ 10 | $.getJSON('request_admin.php', processUpdate); 11 | } 12 | function processUpdate(update){ 13 | $('#adminsTable tr').remove(); 14 | adminJson = update.admins; 15 | jsonTable(update.admins); 16 | } 17 | function deleteAdmin(){ 18 | $.ajax({ 19 | type: 'DELETE', 20 | url: 'change_admin.php', 21 | data: {usernameOld: $('#usernameOld').val()}, 22 | success: refreshData 23 | }); 24 | $('#changeAdminModal').modal('hide'); 25 | } 26 | // takes in JSON response and sends to where it's needed 27 | function processInfo(response){ 28 | jsonTable(response.admins); 29 | adminJson = response.admins; 30 | } 31 | 32 | // validates changes and POSTs data to change_admin.php 33 | function changeAdmin(){ 34 | // perform data validation (validated on backend too) 35 | if($('#usernameChg').val().trim() == ''){ 36 | alert('Username cannot be blank'); 37 | } 38 | else if($('#password').val() != $('#repeatpw').val()){ 39 | alert('Password and repeated password do not match'); 40 | } 41 | // if all data checks out, POST to change.php 42 | else{ 43 | var user = { 44 | usernameOld: $('#usernameOld').val().trim(), 45 | username: $('#usernameChg').val(), 46 | password: $('#passwordChg').val(), 47 | repeat: $('#repeatpwChg').val() 48 | } 49 | $.post('change_admin.php', user, refreshData); 50 | $('#changeAdminModal').modal('hide'); 51 | } 52 | } 53 | // validates form and POSTs data to insert.php 54 | function createAdmin(){ 55 | // perform data validation (validated on backend too) 56 | if($('#username').val().trim() == ''){ 57 | alert('You must set a username'); 58 | } 59 | else if($('#password').val().trim() == '' || $('#repeatpw').val().trim() == ''){ 60 | alert('You must set a password and repeat it'); 61 | } 62 | else if($('#password').val() != $('#repeatpw').val()){ 63 | alert('Password and repeated password do not match'); 64 | } 65 | // if all data checks out, POST to insert.php 66 | else{ 67 | var user = { 68 | username: $('#username').val().trim(), 69 | password: $('#password').val(), 70 | repeat: $('#repeatpw').val() 71 | } 72 | $.post('insert_admin.php', user, refreshData); 73 | } 74 | } 75 | function findAdmin(username){ 76 | for(var i = 0; i < adminJson.length; i++){ 77 | if(username == adminJson[i].username){ 78 | return adminJson[i]; 79 | } 80 | } 81 | } 82 | function adminChange(username){ 83 | $('#usernameOld').val(username); 84 | $('#usernameChg').val(username); 85 | $('#passwordChg').val(''); 86 | $('#repeatpwChg').val(''); 87 | $('#changeAdminModal').modal({backdrop: false}); 88 | } 89 | function jsonTable(admins){ 90 | var table = document.getElementById('adminsTable'); 91 | // iterates through the admin array 92 | for(var i = 0; i < admins.length; i++) { 93 | var admin = admins[i]; 94 | var newRow = table.insertRow(); 95 | var newCell = newRow.insertCell(); 96 | var newText = document.createTextNode(admin); 97 | newCell.appendChild(newText); 98 | // adds the "edit" button 99 | newCell = newRow.insertCell(); 100 | var adminButton = document.createElement('button'); 101 | adminButton.textContent = 'Edit'; 102 | adminButton.className = 'btn btn-default'; 103 | adminButton.username = admin; 104 | adminButton.onclick = function(){adminChange(this.username)}; 105 | newCell.appendChild(adminButton); 106 | } 107 | } -------------------------------------------------------------------------------- /frontend/js/bluebase.js: -------------------------------------------------------------------------------- 1 | // will be called when the window loads 2 | window.onload = loadElements; 3 | var userJson; 4 | var usrChart; 5 | // prepares all necessary elements and fetches data 6 | function loadElements(){ 7 | $('.datepicker').datepicker(); 8 | $('[data-toggle="popover"]').popover(); 9 | $.getJSON('request.php', processInfo); 10 | } 11 | // called whenever a change or addition is made 12 | function refreshData(){ 13 | $.getJSON('request.php', processUpdate); 14 | } 15 | function processUpdate(update){ 16 | $('#usersTable tr').remove(); 17 | userJson = update.users; 18 | jsonTable(update.users); 19 | usrChart.segments[0].value = update.statistics[0]; 20 | usrChart.segments[1].value = update.statistics[1]; 21 | usrChart.segments[2].value = update.statistics[2]; 22 | usrChart.update(); 23 | } 24 | function deleteUser(){ 25 | $.ajax({ 26 | type: 'DELETE', 27 | url: 'change.php', 28 | data: {userid: $('#useridChg').val()}, 29 | success: refreshData 30 | }); 31 | $('#changeUserModal').modal('hide'); 32 | } 33 | // takes in JSON response and sends to where it's needed 34 | function processInfo(response){ 35 | userChart(response.statistics); 36 | jsonTable(response.users); 37 | userJson = response.users; 38 | } 39 | // adds the data to chart and displays it 40 | function userChart(statistics){ 41 | var stats = [{ 42 | value: statistics[0], 43 | color: '#009933', 44 | highlight: '#66C266', 45 | label: 'Enabled' 46 | }, 47 | { 48 | value: statistics[1], 49 | color: '#C21418', 50 | highlight: '#EC4A4F', 51 | label: 'Expired' 52 | }, 53 | { 54 | value: statistics[2], 55 | color: '#444444', 56 | highlight: '#777777', 57 | label: 'Disabled' 58 | }] 59 | var ctx = $('#userChart').get(0).getContext('2d'); 60 | usrChart = new Chart(ctx).Pie(stats, {responsive: true}); 61 | } 62 | // handles the checkbox for enabling user expiration 63 | function toggleExpire(){ 64 | if (document.getElementById('chkExpire').checked){ 65 | $('#expireInput').removeAttr('disabled'); 66 | } 67 | else{ 68 | $('#expireInput').attr('disabled', 'true'); 69 | $('#expire').val(''); 70 | } 71 | } 72 | 73 | function toggleExpireChg(){ 74 | if (document.getElementById('chkExpireChg').checked){ 75 | $('#expireInputChg').removeAttr('disabled'); 76 | } 77 | else{ 78 | $('#expireInputChg').attr('disabled', 'true'); 79 | $('#expireChg').val(''); 80 | } 81 | } 82 | // validates changes and POSTs data to change.php 83 | function changeUser(){ 84 | var acctStatus = '0'; 85 | var expiryDate = null; 86 | var passwd; 87 | // accepts only valid ISO 8601 dates 88 | var valiDate = /^\d{4}-[01]\d-[0-3]\d$/; 89 | // perform data formatting 90 | 91 | if(document.getElementById('statusChg').checked){ 92 | acctStatus = '1'; 93 | } 94 | else{ 95 | acctStatus = '0'; 96 | } 97 | if(document.getElementById('chkExpireChg').checked){ 98 | var expiryDate = $('#expireChg').val(); 99 | } 100 | else{ 101 | var expiryDate = null; 102 | } 103 | // perform data validation (validated on backend too) 104 | if($('#usernameChg').val().trim() == ''){ 105 | alert('Username cannot be blank'); 106 | } 107 | else if(valiDate.test(expiryDate) == false && expiryDate != null){ 108 | alert('Expiration date not valid'); 109 | } 110 | else if($('#passwordChg').val() != $('#repeatpwChg').val()){ 111 | alert('Password and repeated password do not match'); 112 | } 113 | // if all data checks out, POST to change.php 114 | else{ 115 | var user = { 116 | userid: $('#useridChg').val().trim(), 117 | username: $('#usernameChg').val(), 118 | fname: $('#fnameChg').val(), 119 | lname: $('#lnameChg').val(), 120 | expire: expiryDate, 121 | status: acctStatus, 122 | password: $('#passwordChg').val(), 123 | repeat: $('#repeatpwChg').val(), 124 | } 125 | $.post('change.php', user, refreshData); 126 | $('#changeUserModal').modal('hide'); 127 | } 128 | } 129 | // validates form and POSTs data to insert.php 130 | function createUser(){ 131 | var acctStatus = '0'; 132 | var expiryDate = null; 133 | var passwd; 134 | // accepts only valid ISO 8601 dates 135 | var valiDate = /^\d{4}-[01]\d-[0-3]\d$/; 136 | // perform data formatting 137 | 138 | if(document.getElementById('status').checked){ 139 | acctStatus = '1'; 140 | } 141 | else{ 142 | acctStatus = '0'; 143 | } 144 | if(document.getElementById('chkExpire').checked){ 145 | var expiryDate = $('#expire').val(); 146 | } 147 | else{ 148 | var expiryDate = null; 149 | } 150 | // perform data validation (validated on backend too) 151 | if($('#username').val().trim() == ''){ 152 | alert('You must set a username'); 153 | } 154 | else if(valiDate.test(expiryDate) == false && expiryDate != null){ 155 | alert('Expiration date not valid'); 156 | } 157 | else if($('#password').val().trim() == '' || $('#repeatpw').val().trim() == ''){ 158 | alert('You must set a password and repeat it'); 159 | } 160 | else if($('#password').val() != $('#repeatpw').val()){ 161 | alert('Password and repeated password do not match'); 162 | } 163 | // if all data checks out, POST to insert.php 164 | else{ 165 | var user = { 166 | username: $('#username').val().trim(), 167 | fname: $('#fname').val(), 168 | lname: $('#lname').val(), 169 | expire: expiryDate, 170 | status: acctStatus, 171 | password: $('#password').val(), 172 | repeat: $('#repeatpw').val(), 173 | } 174 | $.post('insert.php', user, refreshData); 175 | } 176 | } 177 | function findUser(userid){ 178 | for(var i = 0; i < userJson.length; i++){ 179 | if(userid == userJson[i].userid){ 180 | return userJson[i]; 181 | } 182 | } 183 | } 184 | function userChange(userid){ 185 | var chgUser = findUser(userid); 186 | $('#useridChg').val(userid); 187 | $('#usernameChg').val(chgUser.username); 188 | $('#fnameChg').val(chgUser.fname); 189 | $('#lnameChg').val(chgUser.lname); 190 | $('#passwordChg').val(''); 191 | $('#repeatpwChg').val(''); 192 | if(chgUser.expiration == null){ 193 | document.getElementById('chkExpireChg').checked = false; 194 | $('#expireChg').val(''); 195 | $('#expireInputChg').attr('disabled', 'true'); 196 | } 197 | else{ 198 | document.getElementById('chkExpireChg').checked = true; 199 | $('#expireChg').val(chgUser.expiration); 200 | $('#expireInputChg').removeAttr('disabled'); 201 | } 202 | if(chgUser.disabled == 1){ 203 | document.getElementById('statusChg').checked = true; 204 | } 205 | else{ 206 | document.getElementById('statusChg').checked = false; 207 | } 208 | $('#changeUserModal').modal({backdrop: false}); 209 | $('#expireChg').datepicker(); 210 | } 211 | function jsonTable(users){ 212 | var table = document.getElementById('usersTable'); 213 | // iterates through the user array 214 | for(var i = 0; i < users.length; i++) { 215 | var user = users[i]; 216 | var newRow = table.insertRow(); 217 | // iterates through the users details 218 | for(var detail in user){ 219 | var newCell = newRow.insertCell(); 220 | // special case for disabled value 221 | if(detail != 'disabled'){ 222 | var newText = document.createTextNode(user[detail]); 223 | } 224 | else{ 225 | if(user.disabled == 0){ 226 | var newText = document.createTextNode('False'); 227 | } 228 | else{ 229 | var newText = document.createTextNode('True'); 230 | } 231 | } 232 | newCell.appendChild(newText); 233 | } 234 | // adds the "edit" button 235 | var newCell = newRow.insertCell(); 236 | var userButton = document.createElement('button'); 237 | userButton.textContent = 'Edit'; 238 | userButton.className = 'btn btn-default btn-sm'; 239 | userButton.userID = user.userid; 240 | userButton.onclick = function(){userChange(this.userID)}; 241 | newCell.appendChild(userButton); 242 | } 243 | } -------------------------------------------------------------------------------- /frontend/js/bootstrap-datepicker.js: -------------------------------------------------------------------------------- 1 | /* ========================================================= 2 | * bootstrap-datepicker.js 3 | * http://www.eyecon.ro/bootstrap-datepicker 4 | * ========================================================= 5 | * Copyright 2012 Stefan Petre 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * ========================================================= */ 19 | 20 | !function( $ ) { 21 | 22 | // Picker object 23 | 24 | var Datepicker = function(element, options){ 25 | this.element = $(element); 26 | this.format = DPGlobal.parseFormat(options.format||this.element.data('date-format')||'mm/dd/yyyy'); 27 | this.picker = $(DPGlobal.template) 28 | .appendTo('body') 29 | .on({ 30 | click: $.proxy(this.click, this)//, 31 | //mousedown: $.proxy(this.mousedown, this) 32 | }); 33 | this.isInput = this.element.is('input'); 34 | this.component = this.element.is('.date') ? this.element.find('.add-on') : false; 35 | 36 | if (this.isInput) { 37 | this.element.on({ 38 | focus: $.proxy(this.show, this), 39 | //blur: $.proxy(this.hide, this), 40 | keyup: $.proxy(this.update, this) 41 | }); 42 | } else { 43 | if (this.component){ 44 | this.component.on('click', $.proxy(this.show, this)); 45 | } else { 46 | this.element.on('click', $.proxy(this.show, this)); 47 | } 48 | } 49 | 50 | this.minViewMode = options.minViewMode||this.element.data('date-minviewmode')||0; 51 | if (typeof this.minViewMode === 'string') { 52 | switch (this.minViewMode) { 53 | case 'months': 54 | this.minViewMode = 1; 55 | break; 56 | case 'years': 57 | this.minViewMode = 2; 58 | break; 59 | default: 60 | this.minViewMode = 0; 61 | break; 62 | } 63 | } 64 | this.viewMode = options.viewMode||this.element.data('date-viewmode')||0; 65 | if (typeof this.viewMode === 'string') { 66 | switch (this.viewMode) { 67 | case 'months': 68 | this.viewMode = 1; 69 | break; 70 | case 'years': 71 | this.viewMode = 2; 72 | break; 73 | default: 74 | this.viewMode = 0; 75 | break; 76 | } 77 | } 78 | this.startViewMode = this.viewMode; 79 | this.weekStart = options.weekStart||this.element.data('date-weekstart')||0; 80 | this.weekEnd = this.weekStart === 0 ? 6 : this.weekStart - 1; 81 | this.onRender = options.onRender; 82 | this.fillDow(); 83 | this.fillMonths(); 84 | this.update(); 85 | this.showMode(); 86 | }; 87 | 88 | Datepicker.prototype = { 89 | constructor: Datepicker, 90 | 91 | show: function(e) { 92 | this.picker.show(); 93 | this.height = this.component ? this.component.outerHeight() : this.element.outerHeight(); 94 | this.place(); 95 | $(window).on('resize', $.proxy(this.place, this)); 96 | if (e ) { 97 | e.stopPropagation(); 98 | e.preventDefault(); 99 | } 100 | if (!this.isInput) { 101 | } 102 | var that = this; 103 | $(document).on('mousedown', function(ev){ 104 | if ($(ev.target).closest('.datepicker').length == 0) { 105 | that.hide(); 106 | } 107 | }); 108 | this.element.trigger({ 109 | type: 'show', 110 | date: this.date 111 | }); 112 | }, 113 | 114 | hide: function(){ 115 | this.picker.hide(); 116 | $(window).off('resize', this.place); 117 | this.viewMode = this.startViewMode; 118 | this.showMode(); 119 | if (!this.isInput) { 120 | $(document).off('mousedown', this.hide); 121 | } 122 | //this.set(); 123 | this.element.trigger({ 124 | type: 'hide', 125 | date: this.date 126 | }); 127 | }, 128 | 129 | set: function() { 130 | var formated = DPGlobal.formatDate(this.date, this.format); 131 | if (!this.isInput) { 132 | if (this.component){ 133 | this.element.find('input').prop('value', formated); 134 | } 135 | this.element.data('date', formated); 136 | } else { 137 | this.element.prop('value', formated); 138 | } 139 | }, 140 | 141 | setValue: function(newDate) { 142 | if (typeof newDate === 'string') { 143 | this.date = DPGlobal.parseDate(newDate, this.format); 144 | } else { 145 | this.date = new Date(newDate); 146 | } 147 | this.set(); 148 | this.viewDate = new Date(this.date.getFullYear(), this.date.getMonth(), 1, 0, 0, 0, 0); 149 | this.fill(); 150 | }, 151 | 152 | place: function(){ 153 | var offset = this.component ? this.component.offset() : this.element.offset(); 154 | this.picker.css({ 155 | top: offset.top + this.height, 156 | left: offset.left 157 | }); 158 | }, 159 | 160 | update: function(newDate){ 161 | this.date = DPGlobal.parseDate( 162 | typeof newDate === 'string' ? newDate : (this.isInput ? this.element.prop('value') : this.element.data('date')), 163 | this.format 164 | ); 165 | this.viewDate = new Date(this.date.getFullYear(), this.date.getMonth(), 1, 0, 0, 0, 0); 166 | this.fill(); 167 | }, 168 | 169 | fillDow: function(){ 170 | var dowCnt = this.weekStart; 171 | var html = '