hotspotlogin 0.1.2 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +34 -13
- data/examples/etc/lighttpd/lighttpd.conf +31 -0
- data/examples/hotspotlogin.conf.yaml +6 -0
- data/lib/hotspotlogin/app.rb +74 -50
- data/lib/hotspotlogin/app/helpers.rb +17 -0
- data/lib/hotspotlogin/config.rb +25 -1
- data/lib/hotspotlogin/constants.rb +32 -2
- data/public/hotspotlogin/css/default.css +77 -0
- data/public/hotspotlogin/js/ChilliLibrary.js +844 -0
- data/public/hotspotlogin/js/UserStatus.js +229 -0
- data/views/_login_form.erb +5 -3
- data/views/hotspotlogin.erb +78 -18
- data/views/layout.erb +31 -43
- metadata +16 -11
@@ -0,0 +1,229 @@
|
|
1
|
+
// Requires ChilliLibrary.js
|
2
|
+
// See: http://www.coova.org/CoovaChilli/JSON
|
3
|
+
//
|
4
|
+
// Copyright(c) 2010, Guido De Rosa (guido.derosa at vemarsas.it) .
|
5
|
+
// License: MIT
|
6
|
+
|
7
|
+
chilliController.sessionTimeLeft = function() {
|
8
|
+
return Math.max(
|
9
|
+
(
|
10
|
+
chilliController.session.sessionTimeout -
|
11
|
+
chilliController.accounting.sessionTime
|
12
|
+
),
|
13
|
+
0
|
14
|
+
);
|
15
|
+
}
|
16
|
+
chilliController.idleTimeLeft = function() {
|
17
|
+
return Math.max(
|
18
|
+
(
|
19
|
+
chilliController.session.idleTimeout -
|
20
|
+
chilliController.accounting.idleTime
|
21
|
+
),
|
22
|
+
0
|
23
|
+
);
|
24
|
+
}
|
25
|
+
|
26
|
+
chilliController.scheduleSessionTimeoutAutorefresh = function() {
|
27
|
+
if (chilliController.sessionTimeLeft() && !chilliController.sessionTimeoutTimer) {
|
28
|
+
chilliController.sessionTimeoutTimer = {
|
29
|
+
preLogoff: setTimeout(
|
30
|
+
'chilliController.refresh()', 1000 * chilliController.sessionTimeLeft()
|
31
|
+
),
|
32
|
+
atLogoff: setTimeout( // 3 seconds delay looks fair
|
33
|
+
'chilliController.refresh()', 1000 * (3 + chilliController.sessionTimeLeft())
|
34
|
+
)
|
35
|
+
}
|
36
|
+
}
|
37
|
+
}
|
38
|
+
|
39
|
+
chilliController.scheduleIdleTimeoutAutorefresh = function() {
|
40
|
+
if (chilliController.idleTimeLeft()) {
|
41
|
+
if (chilliController.idleTimeoutTimer) {
|
42
|
+
clearTimeout(chilliController.idleTimeoutTimer.preLogoff);
|
43
|
+
clearTimeout(chilliController.idleTimeoutTimer.atLogoff);
|
44
|
+
}
|
45
|
+
chilliController.idleTimeoutTimer = {
|
46
|
+
preLogoff: setTimeout(
|
47
|
+
'chilliController.refresh()', 1000 * chilliController.idleTimeLeft()
|
48
|
+
),
|
49
|
+
atLogoff: setTimeout( // 3 seconds delay looks fair
|
50
|
+
'chilliController.refresh()', 1000 * (3 + chilliController.idleTimeLeft())
|
51
|
+
)
|
52
|
+
}
|
53
|
+
}
|
54
|
+
}
|
55
|
+
|
56
|
+
function showUserStatus(h) {
|
57
|
+
|
58
|
+
// Utility functions and objects
|
59
|
+
//
|
60
|
+
function formatStateCode(code) {
|
61
|
+
switch(code) {
|
62
|
+
case chilliController.stateCodes.UNKNOWN:
|
63
|
+
return 'Unknown';
|
64
|
+
case chilliController.stateCodes.NOT_AUTH:
|
65
|
+
return 'Not Authorized';
|
66
|
+
case chilliController.stateCodes.AUTH:
|
67
|
+
return 'Authorized';
|
68
|
+
case chilliController.stateCodes.AUTH_PENDING:
|
69
|
+
return 'Authorization Pending';
|
70
|
+
case chilliController.stateCodes.AUTH_SPLASH:
|
71
|
+
return 'AUTH_SPLASH'; // What does it mean?
|
72
|
+
default:
|
73
|
+
return code;
|
74
|
+
}
|
75
|
+
}
|
76
|
+
|
77
|
+
// CoovaChilli JSON interface only supports CHAP... don't use JSON to login.
|
78
|
+
function loginURL() {
|
79
|
+
var schema = chilliController.ssl ? 'https' : 'http';
|
80
|
+
return schema + '://' + h.uamip + ':' + h.uamport;
|
81
|
+
}
|
82
|
+
|
83
|
+
// chilliController.debug = true;
|
84
|
+
|
85
|
+
// If you use non standard configuration, define your configuration
|
86
|
+
if (h.uamip)
|
87
|
+
chilliController.host = h.uamip; // default: 192.168.182.1
|
88
|
+
if (h.uamport)
|
89
|
+
chilliController.port = h.uamport; // default: 3990
|
90
|
+
|
91
|
+
// We choose 5 minutes because is the default interval of Chilli->Radius
|
92
|
+
// accounting updates, and looks reasonable for busy sites (avoiding too
|
93
|
+
// much load on the network infrastructure and servers) .
|
94
|
+
chilliController.interval = (h.interval || 300); // default = 30
|
95
|
+
|
96
|
+
// then define event handler functions
|
97
|
+
chilliController.onError = handleErrors;
|
98
|
+
chilliController.onUpdate = updateUI ;
|
99
|
+
|
100
|
+
// get current state
|
101
|
+
chilliController.refresh() ;
|
102
|
+
|
103
|
+
initTable();
|
104
|
+
|
105
|
+
function initTable() {
|
106
|
+
// show them when/if we need them; but do not hide them when
|
107
|
+
// session terminates
|
108
|
+
document.getElementById('sessionTimeLeft:row').style.display = 'none';
|
109
|
+
document.getElementById('idleTimeout:row').style.display = 'none';
|
110
|
+
}
|
111
|
+
|
112
|
+
function updateHeadings(clientState) {
|
113
|
+
txt = null;
|
114
|
+
switch(clientState) {
|
115
|
+
case chilliController.stateCodes.NOT_AUTH:
|
116
|
+
txt = 'Logged out from HotSpot';
|
117
|
+
break;
|
118
|
+
case chilliController.stateCodes.AUTH:
|
119
|
+
txt = 'Logged in to HotSpot';
|
120
|
+
break;
|
121
|
+
}
|
122
|
+
if (txt) {
|
123
|
+
document.title = txt;
|
124
|
+
if (document.getElementById('headline'))
|
125
|
+
document.getElementById('headline').innerHTML = txt;
|
126
|
+
}
|
127
|
+
}
|
128
|
+
|
129
|
+
function updateLinks(clientState) {
|
130
|
+
var e = document.getElementById('logInLogOut');
|
131
|
+
if (e) {
|
132
|
+
switch(clientState) {
|
133
|
+
case chilliController.stateCodes.NOT_AUTH:
|
134
|
+
e.setAttribute('href', loginURL());
|
135
|
+
e.innerHTML = 'Login';
|
136
|
+
break;
|
137
|
+
case chilliController.stateCodes.AUTH:
|
138
|
+
e.setAttribute('href', '#');
|
139
|
+
e.onclick = chilliController.logoff;
|
140
|
+
e.innerHTML = 'Logout';
|
141
|
+
break;
|
142
|
+
}
|
143
|
+
}
|
144
|
+
}
|
145
|
+
|
146
|
+
// when the reply is ready, this handler function is called
|
147
|
+
function updateUI( cmd ) {
|
148
|
+
updateHeadings( chilliController.clientState);
|
149
|
+
updateLinks( chilliController.clientState);
|
150
|
+
var userName_e = document.getElementById('userName');
|
151
|
+
var clientState_e = document.getElementById('clientState');
|
152
|
+
var sessionTime_e = document.getElementById('sessionTime');
|
153
|
+
var sessionTimeLeft_e = document.getElementById('sessionTimeLeft');
|
154
|
+
var download_e = document.getElementById('download');
|
155
|
+
var upload_e = document.getElementById('upload');
|
156
|
+
var interval_e = document.getElementById('interval');
|
157
|
+
if (userName_e) {
|
158
|
+
userName_e.innerHTML = (
|
159
|
+
chilliController.session.userName
|
160
|
+
);
|
161
|
+
}
|
162
|
+
if (clientState_e) {
|
163
|
+
clientState_e.innerHTML = (
|
164
|
+
formatStateCode(chilliController.clientState)
|
165
|
+
);
|
166
|
+
}
|
167
|
+
//if (chilliController.terminateCause) {
|
168
|
+
// document.getElementById('terminateCause').innerHTML = (
|
169
|
+
// chilliController.terminateCause
|
170
|
+
// )
|
171
|
+
//}
|
172
|
+
if (sessionTime_e) {
|
173
|
+
document.getElementById('sessionTime').innerHTML = (
|
174
|
+
chilliController.formatTime(
|
175
|
+
chilliController.accounting.sessionTime, '0')
|
176
|
+
);
|
177
|
+
}
|
178
|
+
if (sessionTimeLeft_e) {
|
179
|
+
if (chilliController.session.sessionTimeout) {
|
180
|
+
document.getElementById('sessionTimeLeft').innerHTML = (
|
181
|
+
chilliController.formatTime(chilliController.sessionTimeLeft(), 0)
|
182
|
+
);
|
183
|
+
document.getElementById('sessionTimeLeft:row').style.display = '';
|
184
|
+
} else {
|
185
|
+
document.getElementById('sessionTimeLeft').innerHTML = ''
|
186
|
+
}
|
187
|
+
}
|
188
|
+
if (chilliController.session.idleTimeout) {
|
189
|
+
document.getElementById('idleTimeout').innerHTML = (
|
190
|
+
chilliController.formatTime(chilliController.accounting.idleTime) +
|
191
|
+
' / ' +
|
192
|
+
chilliController.formatTime(chilliController.session.idleTimeout)
|
193
|
+
);
|
194
|
+
document.getElementById('idleTimeout:row').style.display = '';
|
195
|
+
}
|
196
|
+
var download_bytes =
|
197
|
+
chilliController.accounting.inputOctets +
|
198
|
+
Math.pow(2, 32) * chilliController.accounting.inputGigawords;
|
199
|
+
var upload_bytes =
|
200
|
+
chilliController.accounting.outputOctets +
|
201
|
+
Math.pow(2, 32) * chilliController.accounting.outputGigawords;
|
202
|
+
if (download_e) {
|
203
|
+
download_e.innerHTML = (
|
204
|
+
chilliController.formatBytes(download_bytes, 0)
|
205
|
+
);
|
206
|
+
}
|
207
|
+
if (upload_e) {
|
208
|
+
upload_e.innerHTML = (
|
209
|
+
chilliController.formatBytes(upload_bytes, 0)
|
210
|
+
);
|
211
|
+
}
|
212
|
+
if (interval_e) {
|
213
|
+
interval_e.innerHTML = (
|
214
|
+
chilliController.formatTime(chilliController.interval, 0)
|
215
|
+
);
|
216
|
+
}
|
217
|
+
|
218
|
+
chilliController.scheduleSessionTimeoutAutorefresh();
|
219
|
+
chilliController.scheduleIdleTimeoutAutorefresh();
|
220
|
+
}
|
221
|
+
|
222
|
+
// If an error occurs, this handler will be called instead
|
223
|
+
function handleErrors ( code ) {
|
224
|
+
alert('The last contact with the Controller failed. Error code =' + code);
|
225
|
+
}
|
226
|
+
|
227
|
+
|
228
|
+
}
|
229
|
+
|
data/views/_login_form.erb
CHANGED
@@ -6,14 +6,16 @@
|
|
6
6
|
<table>
|
7
7
|
<tbody>
|
8
8
|
<tr>
|
9
|
-
<
|
9
|
+
<th scope="row">Login:</td>
|
10
10
|
<td><input type="text" name="UserName" size="20" maxlength="255"></td>
|
11
11
|
</tr>
|
12
12
|
<tr>
|
13
|
-
<
|
13
|
+
<th scope="row">Password:</td>
|
14
14
|
<td><input type="password" name="Password" size="20" maxlength="255"></td>
|
15
15
|
</tr>
|
16
16
|
</tbody>
|
17
17
|
</table>
|
18
|
-
<
|
18
|
+
<div id="submit-container">
|
19
|
+
<input type="submit" name="login" value="login">
|
20
|
+
</div>
|
19
21
|
</form>
|
data/views/hotspotlogin.erb
CHANGED
@@ -1,22 +1,36 @@
|
|
1
|
-
|
1
|
+
<%
|
2
|
+
require 'hotspotlogin/app/helpers'
|
3
|
+
%>
|
4
|
+
|
5
|
+
<% if custom_headline %>
|
6
|
+
<h1><%= custom_headline %></h1>
|
7
|
+
<% end %>
|
8
|
+
<% if logoext %>
|
9
|
+
<div id="logo-container"><img src="/hotspotlogin/logo<%= logoext %>"/></div>
|
10
|
+
<% end %>
|
11
|
+
|
12
|
+
<h2 id="headline"><%= titel %></h2>
|
13
|
+
|
14
|
+
<% if custom_text and File.file? custom_text %>
|
15
|
+
<div id="custom-text"><%= File.read custom_text %></div>
|
16
|
+
<% end %>
|
17
|
+
|
18
|
+
<%# TODO: use constants.rb %>
|
2
19
|
<% if [1, 4, 12].include? result %>
|
3
|
-
<div
|
4
|
-
<a
|
5
|
-
|
6
|
-
|
20
|
+
<div id="logInLogOut-container">
|
21
|
+
<a
|
22
|
+
id="logInLogOut"
|
23
|
+
href="http://<%= uamip %>:<%= uamport %>/logoff">Logout</a>
|
24
|
+
<a href="#" onClick="javascript:chilliController.refresh();">Refresh</a>
|
7
25
|
</div>
|
8
|
-
<% elsif [2, 5].include? result %>
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
<div style="text-align: center;">
|
17
|
-
<a href="http://<%= uamip %>:<%= uamport %>/prelogin">
|
18
|
-
Login
|
19
|
-
</a>
|
26
|
+
<% elsif [2, 5, 3, 13].include? result %>
|
27
|
+
<div id="form-container">
|
28
|
+
<%=
|
29
|
+
erb(
|
30
|
+
:_login_form,
|
31
|
+
:layout => false
|
32
|
+
)
|
33
|
+
%>
|
20
34
|
</div>
|
21
35
|
<% elsif result == 11 %>
|
22
36
|
<p>Please wait.. </p> <!-- popup1 -->
|
@@ -24,7 +38,53 @@
|
|
24
38
|
<p>Login must be performed through ChilliSpot daemon!</p>
|
25
39
|
<% end %>
|
26
40
|
|
27
|
-
|
28
41
|
<% if params['reply'] %>
|
29
42
|
<div style="text-align: center;"><%= params['reply'] %></div>
|
30
43
|
<% end %>
|
44
|
+
|
45
|
+
<% if status_window?(result) %>
|
46
|
+
<div id="status-container">
|
47
|
+
<table id="status-table">
|
48
|
+
<tbody>
|
49
|
+
<tr id="userName:row">
|
50
|
+
<th scope="row">Username</th>
|
51
|
+
<td id="userName"></td>
|
52
|
+
<tr id="clientState:row">
|
53
|
+
<th scope="row">Client State</th>
|
54
|
+
<td id="clientState"></td>
|
55
|
+
</tr>
|
56
|
+
<tr id="sessionTime:row">
|
57
|
+
<th scope="row">Session Time</th>
|
58
|
+
<td id="sessionTime"></td>
|
59
|
+
</tr>
|
60
|
+
<tr id="sessionTimeLeft:row">
|
61
|
+
<th scope="row">Session Time Left</th>
|
62
|
+
<td id="sessionTimeLeft"></th>
|
63
|
+
</tr>
|
64
|
+
<tr id="idleTimeout:row">
|
65
|
+
<th scope="row">Idle Time/Timeout</th>
|
66
|
+
<td id="idleTimeout"></th>
|
67
|
+
</tr>
|
68
|
+
<tr id="download:row">
|
69
|
+
<th scope="row">Download Traffic</th>
|
70
|
+
<td id="download"></td>
|
71
|
+
</tr>
|
72
|
+
<tr id="upload:row">
|
73
|
+
<th scope="row">Upload Traffic</th>
|
74
|
+
<td id="upload"></td>
|
75
|
+
</tr>
|
76
|
+
<tr id="interval:row">
|
77
|
+
<th scope="row" class="optinfo">Automatically updated every</th>
|
78
|
+
<td id="interval"></td>
|
79
|
+
</tr>
|
80
|
+
</tbody>
|
81
|
+
</table>
|
82
|
+
</div>
|
83
|
+
|
84
|
+
<% end %>
|
85
|
+
|
86
|
+
<% if custom_footer and File.file? custom_footer %>
|
87
|
+
<div id="custom-footer"><%= File.read custom_footer %></div>
|
88
|
+
<% end %>
|
89
|
+
|
90
|
+
|
data/views/layout.erb
CHANGED
@@ -1,4 +1,6 @@
|
|
1
1
|
<%
|
2
|
+
require 'hotspotlogin/constants'
|
3
|
+
|
2
4
|
loginpath = request.path_info
|
3
5
|
%>
|
4
6
|
|
@@ -8,47 +10,17 @@
|
|
8
10
|
<title><%= titel %></title>
|
9
11
|
<meta http-equiv="Cache-control" content="no-cache">
|
10
12
|
<meta http-equiv="Pragma" content="no-cache">
|
11
|
-
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
|
12
13
|
|
13
|
-
<script language="JavaScript">
|
14
|
+
<script language="JavaScript"> // legacy stuff from hotspotlogin.cgi/php
|
15
|
+
var width = 500; // popup
|
16
|
+
var height = 500; // popup
|
14
17
|
var blur = 0;
|
15
18
|
var starttime = new Date();
|
16
19
|
var startclock = starttime.getTime();
|
17
20
|
var mytimeleft = 0;
|
18
|
-
|
19
|
-
function doTime() {
|
20
|
-
window.setTimeout( "doTime()", 1000 );
|
21
|
-
t = new Date();
|
22
|
-
time = Math.round((t.getTime() - starttime.getTime())/1000);
|
23
|
-
if (mytimeleft) {
|
24
|
-
time = mytimeleft - time;
|
25
|
-
if (time <= 0) {
|
26
|
-
window.location = "<%= loginpath %>?res=popup3&uamip=<%= uamip %>&uamport=<%= uamport %>";
|
27
|
-
}
|
28
|
-
}
|
29
|
-
if (time < 0) time = 0;
|
30
|
-
hours = (time - (time % 3600)) / 3600;
|
31
|
-
time = time - (hours * 3600);
|
32
|
-
mins = (time - (time % 60)) / 60;
|
33
|
-
secs = time - (mins * 60);
|
34
|
-
if (hours < 10) hours = "0" + hours;
|
35
|
-
if (mins < 10) mins = "0" + mins;
|
36
|
-
if (secs < 10) secs = "0" + secs;
|
37
|
-
title = "Online time: " + hours + ":" + mins + ":" + secs;
|
38
|
-
if (mytimeleft) {
|
39
|
-
title = "Remaining time: " + hours + ":" + mins + ":" + secs;
|
40
|
-
}
|
41
|
-
if(document.all || document.getElementById){
|
42
|
-
document.title = title;
|
43
|
-
}
|
44
|
-
else {
|
45
|
-
self.status = title;
|
46
|
-
}
|
47
|
-
}
|
48
|
-
|
49
21
|
function popUp(URL) {
|
50
22
|
if (self.name != "chillispot_popup") {
|
51
|
-
chillispot_popup = window.open(URL, 'chillispot_popup', 'toolbar=0,scrollbars=0,location=0,statusbar=0,menubar=0,resizable=1,width=
|
23
|
+
chillispot_popup = window.open(URL, 'chillispot_popup', 'toolbar=0,scrollbars=0,location=0,statusbar=0,menubar=0,resizable=1,width=' + width + ',height=' + height);
|
52
24
|
}
|
53
25
|
}
|
54
26
|
|
@@ -56,21 +28,17 @@
|
|
56
28
|
if (timeleft) {
|
57
29
|
mytimeleft = timeleft;
|
58
30
|
}
|
59
|
-
if ((result == 1) && (self.name == "chillispot_popup")) {
|
60
|
-
doTime();
|
61
|
-
}
|
62
31
|
if ((result == 1) && (self.name != "chillispot_popup")) {
|
63
|
-
chillispot_popup = window.open(URL, 'chillispot_popup', 'toolbar=0,scrollbars=0,location=0,statusbar=0,menubar=0,resizable=1,width=
|
32
|
+
chillispot_popup = window.open(URL, 'chillispot_popup', 'toolbar=0,scrollbars=0,location=0,statusbar=0,menubar=0,resizable=1,width=' + width + ',height=' + height);
|
64
33
|
}
|
65
34
|
if ((result == 2) || result == 5) {
|
66
35
|
document.form1.UserName.focus()
|
67
36
|
}
|
68
37
|
if ((result == 2) && (self.name != "chillispot_popup")) {
|
69
|
-
chillispot_popup = window.open('', 'chillispot_popup', 'toolbar=0,scrollbars=0,location=0,statusbar=0,menubar=0,resizable=1,width=
|
38
|
+
chillispot_popup = window.open('', 'chillispot_popup', 'toolbar=0,scrollbars=0,location=0,statusbar=0,menubar=0,resizable=1,width='+ width + ',height=' + height);
|
70
39
|
chillispot_popup.close();
|
71
40
|
}
|
72
41
|
if ((result == 12) && (self.name == "chillispot_popup")) {
|
73
|
-
doTime();
|
74
42
|
if (redirurl) {
|
75
43
|
opener.location = redirurl;
|
76
44
|
}
|
@@ -98,15 +66,35 @@
|
|
98
66
|
}
|
99
67
|
}
|
100
68
|
</script>
|
69
|
+
|
70
|
+
<link rel="stylesheet" href="/hotspotlogin/css/default.css"/>
|
71
|
+
<link rel="shortcut icon" href="/hotspotlogin/favicon.ico"/>
|
72
|
+
|
101
73
|
</head>
|
102
74
|
|
103
|
-
<body onLoad="javascript:doOnLoad(<%= result %>, '<%= request.path_info %>?res=popup2&uamip=<%= uamip %>&uamport=<%= uamport %>&userurl=<%= userurl %>&redirurl=<%= redirurl %>&timeleft=<%= timeleft %>','<%= userurl %>', '<%= redirurl %>', '<%= timeleft %>')" onBlur="javascript:doOnBlur(
|
104
|
-
<div
|
105
|
-
hotspotlogin.rb
|
75
|
+
<body onLoad="javascript:doOnLoad(<%= result %>, '<%= request.path_info %>?res=popup2&uamip=<%= uamip %>&uamport=<%= uamport %>&userurl=<%= userurl %>&redirurl=<%= redirurl %>&timeleft=<%= timeleft %>','<%= userurl %>', '<%= redirurl %>', '<%= timeleft %>')" onBlur="javascript:doOnBlur(<%= result %>)">
|
76
|
+
<div id="powered-by">
|
77
|
+
Powered by <a href="http://rubygems.org/gems/hotspotlogin">hotspotlogin.rb</a>
|
106
78
|
</div>
|
107
79
|
<div id="main">
|
108
80
|
<%= yield %>
|
109
81
|
</div>
|
82
|
+
<% if status_window?(result) %>
|
83
|
+
<!-- CovaChilli JSON interface -->
|
84
|
+
<script
|
85
|
+
type="text/javascript" src="hotspotlogin/js/ChilliLibrary.js">
|
86
|
+
</script>
|
87
|
+
<script
|
88
|
+
type="text/javascript" src="hotspotlogin/js/UserStatus.js">
|
89
|
+
</script>
|
90
|
+
<script language="JavaScript">
|
91
|
+
showUserStatus( {
|
92
|
+
uamip: "<%= uamip %>",
|
93
|
+
uamport: <%= uamport %>,
|
94
|
+
interval: <%= interval %>
|
95
|
+
} );
|
96
|
+
</script>
|
97
|
+
<% end %>
|
110
98
|
</body>
|
111
99
|
</html>
|
112
100
|
|