reSIProcate
SIP stack, Dialog Usage Manager and related libraries
Loading...
Searching...
No Matches
WebAdmin.cxx
Go to the documentation of this file.
1#include "rutil/ResipAssert.h"
2#include <fstream>
3#include <iostream>
4#include <string>
5#include <time.h>
6
7#if defined(HAVE_CONFIG_H)
8 #include "config.h"
9#endif
10
15#include "resip/stack/Tuple.hxx"
18#include "rutil/Data.hxx"
19#include "rutil/DnsUtil.hxx"
20#include "rutil/Lock.hxx"
21#include "rutil/Logger.hxx"
23#include "rutil/ParseBuffer.hxx"
24#include "rutil/Socket.hxx"
25#include "rutil/Timer.hxx"
27
29#include "repro/Proxy.hxx"
30#include "repro/HttpBase.hxx"
32#include "repro/WebAdmin.hxx"
33#include "repro/RestAdmin.hxx"
34#include "repro/RouteStore.hxx"
35#include "repro/UserStore.hxx"
36#include "repro/FilterStore.hxx"
37#include "repro/Store.hxx"
38
39#ifdef USE_SSL
41#endif
42
43using namespace resip;
44using namespace repro;
45using namespace std;
46
47#define RESIPROCATE_SUBSYSTEM Subsystem::REPRO
48
49// Presentation is driven entirely by the stylesheet in webadmin/pageOutlinePre.html.
50// "form-table" is the borderless label/field layout, "data-table" is the bordered
51// record listing.
52#define REPRO_BORDERLESS_TABLE_PROPS " class=\"form-table\""
53#define REPRO_BORDERED_TABLE_PROPS " class=\"data-table\""
54
55// Must match the default ReproRunner uses when deciding whether to start the
56// CommandServer (see ReproRunner.cxx). These drifted apart: WebAdmin defaulted
57// to 0, so on any install that left CommandPort unset -- which is the shipped
58// repro.config, where the line is commented out -- the CommandServer was running
59// on 5081 but the web UI hid the Restart Proxy button and refused the restart.
60#define REPRO_DEFAULT_COMMAND_PORT 5081
61
62// The repro product mark: a request arriving at the proxy and being forked onward.
63// Kept in sync with the copy in webadmin/pageOutlinePre.html.
64static const char*
66{
67 return
68 "<svg class=\"logo\" viewBox=\"0 0 32 32\" role=\"img\" aria-label=\"Repro\">"
69 "<rect x=\"0.9\" y=\"0.9\" width=\"30.2\" height=\"30.2\" rx=\"6.5\""
70 " fill=\"var(--mark-navy)\" stroke=\"var(--mark-edge)\" stroke-width=\"1.6\"/>"
71 "<g stroke=\"var(--mark-amber)\" stroke-width=\"2.2\" fill=\"none\" stroke-linecap=\"round\">"
72 "<path d=\"M8 16h7\"/><path d=\"M15 16l8-6.5\"/><path d=\"M15 16l8 6.5\"/></g>"
73 "<circle cx=\"8\" cy=\"16\" r=\"3\" fill=\"var(--mark-amber)\"/>"
74 "<circle cx=\"23\" cy=\"9.5\" r=\"3\" fill=\"#ffb454\"/>"
75 "<circle cx=\"23\" cy=\"22.5\" r=\"3\" fill=\"#ffb454\"/>"
76 "</svg>";
77}
78
79// The same mark as an inline data: URI, for the browser tab icon. '#' has to be
80// percent encoded (%23) or it would terminate the URI at the first colour.
81static const char*
83{
84 return
85 "<link rel=\"icon\" href=\"data:image/svg+xml,"
86 "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'>"
87 "<rect width='32' height='32' rx='7' fill='%2312294d'/>"
88 "<g stroke='%23ff8d09' stroke-width='2.4' fill='none' stroke-linecap='round'>"
89 "<path d='M8 16h7'/><path d='M15 16l8-6.5'/><path d='M15 16l8 6.5'/></g>"
90 "<circle cx='8' cy='16' r='3' fill='%23ff8d09'/>"
91 "<circle cx='23' cy='9.5' r='3' fill='%23ffb454'/>"
92 "<circle cx='23' cy='22.5' r='3' fill='%23ffb454'/>"
93 "</svg>\" />\n";
94}
95
96// Stylesheet for the pages that are served outside of the admin page outline
97// (the login page and the per-user page). It uses the same palette as
98// webadmin/pageOutlinePre.html so that every repro page looks like one product.
99static const char*
101{
102 return
103 "<style>\n"
104 " :root { --bg:#0a0c0f; --bg2:#10141a; --border:#1e2730; --green:#00e676;\n"
105 " --blue:#40c4ff; --text:#c8d0da; --text-dim:#4a5568; --text-mid:#7a8899;\n"
106 " --mark-navy:#12294d; --mark-edge:#2f5da8; --mark-amber:#ff8d09;\n"
107 " --mono:'IBM Plex Mono',ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;\n"
108 " color-scheme:dark; }\n"
109 " *,*::before,*::after { box-sizing:border-box; margin:0; padding:0; }\n"
110 " body { background:var(--bg); color:var(--text); font-family:var(--mono); font-size:13px;\n"
111 " line-height:1.55; min-height:100vh; display:flex; align-items:center;\n"
112 " justify-content:center; padding:24px; }\n"
113 " .card { background:var(--bg2); border:1px solid var(--border); border-radius:3px;\n"
114 " padding:26px 28px; max-width:520px; width:100%; }\n"
115 " .brand { display:flex; align-items:center; gap:12px; }\n"
116 " .logo { width:38px; height:38px; flex-shrink:0; display:block; }\n"
117 " .name { font-size:16px; font-weight:600; letter-spacing:.12em; text-transform:uppercase; }\n"
118 " .sub { font-size:10px; letter-spacing:.14em; text-transform:uppercase;\n"
119 " color:var(--text-dim); margin:6px 0 20px; }\n"
120 " .cta { display:inline-block; padding:8px 20px; border:1px solid var(--green);\n"
121 " border-radius:2px; color:var(--green); text-decoration:none; font-size:11px;\n"
122 " font-weight:600; letter-spacing:.12em; text-transform:uppercase;\n"
123 " transition:all .15s; }\n"
124 " .cta:hover { background:var(--green); color:var(--bg); }\n"
125 " p { color:var(--text-mid); margin-top:18px; font-size:12px; }\n"
126 " a { color:var(--blue); text-decoration:none; }\n"
127 " a:hover { text-decoration:underline; }\n"
128 "</style>\n";
129}
130
131WebAdmin::RemoveKey::RemoveKey(const Data &key1, const Data &key2) : mKey1(key1), mKey2(key2)
132{
133}
134
135bool
136WebAdmin::RemoveKey::operator<(const RemoveKey& rhs) const
137{
138 if(mKey1 < rhs.mKey1)
139 {
140 return true;
141 }
142 else if(mKey1 == rhs.mKey1 && mKey2 < rhs.mKey2)
143 {
144 return true;
145 }
146 else
147 {
148 return false;
149 }
150}
151
155 const Data& realm, // this realm is used for http challenges
156 int port,
157 IpVersion version,
158 const Data& ipAddr):
159 HttpBase(port, version, realm, ipAddr),
160 mProxy(proxy),
161 mStore(*mProxy.getConfig().getDataStore()),
162 mRegDb(regDb),
163 mPubDb(pubDb),
164 mNoWebChallenges(proxy.getConfig().getConfigBool("DisableHttpAuth", false)),
165 mPageOutlinePre(
166#include "repro/webadmin/pageOutlinePre.ixx"
167 ),
168 mPageOutlinePost(
169#include "repro/webadmin/pageOutlinePost.ixx"
170 ),
171 mUserFile(proxy.getConfig().getConfigData("HttpAdminUserFile", "users.txt")),
172 mStatsReady(false)
173{
174 // Place repro version into PageOutlinePre
175 mPageOutlinePre.replace("VERSION", VersionUtils::instance().releaseVersion().c_str());
176
178
179 // Construct the REST API handler now that all members are initialized.
180 mRestAdmin.reset(new RestAdmin(*this));
181}
182
184{
185 // mRestAdmin is destroyed here. Defined out-of-line so that RestAdmin is
186 // a complete type at the point where unique_ptr invokes its deleter,
187 // which avoids the "invalid application of 'sizeof' to incomplete type"
188 // error from GCC's unique_ptr implementation.
189}
190
202void
204{
205 InfoLog(<< "Trying to load web admin users from: " << mUserFile);
206 std::ifstream users(mUserFile.c_str());
207 std::string sline;
208 int lineNbr = 0;
209 if(!users)
210 {
211 throw ConfigException("Error opening/reading user database file", __FILE__, __LINE__);
212 }
213 std::map<Data,Data> userMap;
214
215 while(std::getline(users, sline))
216 {
217 Data username;
218 Data realm;
219 Data ha1;
220
221 Data line(sline);
222 ParseBuffer pb(line);
223 lineNbr++;
224
225 // Jump over empty lines.
226 if(line.size() == 0)
227 {
228 continue;
229 }
230
231 pb.skipWhitespace();
232 if(!pb.eof() && *pb.position() == '#')
233 {
234 // Line is commented out, skip it
235 continue;
236 }
237
238 const char * anchor = pb.position();
239
240 pb.skipToOneOf(" :");
241
242 if (pb.eof())
243 {
244 ErrLog(<< "Missing or invalid credentials at line " << lineNbr);
245 continue;
246 }
247
248 pb.data(username, anchor);
249
250 pb.skipToChar(':');
251 if (!pb.eof())
252 {
253 pb.skipChar(':');
254 pb.skipWhitespace();
255 }
256
257 anchor = pb.position();
258 pb.skipToOneOf(" :");
259
260 if (pb.eof())
261 {
262 ErrLog(<< "Missing or invalid credentials at line " << lineNbr);
263 continue;
264 }
265
266 pb.data(realm, anchor);
267
268 pb.skipToChar(':');
269 if (!pb.eof())
270 {
271 pb.skipChar(':');
272 pb.skipWhitespace();
273 }
274
275 anchor = pb.position();
276 // Tolerate tab padding, and include the EOL chars: getline only strips
277 // the LF, so a file with CRLF line endings would otherwise leave a
278 // trailing CR on this last field.
279 pb.skipToOneOf(" \t:\r\n");
280
281 pb.data(ha1, anchor);
282
283 // We can only authenticate users in the realm we challenge for
284 if(realm != mRealm)
285 {
286 DebugLog(<< "Ignoring user " << username << " for realm " << realm);
287 continue;
288 }
289
290 userMap[username] = ha1;
291 }
292 users.close();
293 InfoLog(<< "Processed " << userMap.size() << " user(s) from " << lineNbr << " line(s) in " << mUserFile);
294 mUsers = userMap;
295}
296
297void
299 const Data& uri,
300 int pageNumber,
301 const resip::Data& pUser,
302 const resip::Data& pPassword )
303{
304 ParseBuffer pb(uri);
305
306 DebugLog (<< "Parsing URL" << uri );
307
308 const char* anchor = pb.skipChar('/');
309 pb.skipToChar('?');
310 Data pageName;
311 pb.data(pageName,anchor);
312
313 DebugLog (<< " got page name: " << pageName );
314
315 // Dispatch any URI starting with /api/v1 to the REST handler.
316 // Authentication still happens below; the REST handler is invoked only
317 // after the user has been authenticated (or challenges disabled).
318 bool isRestRequest = uri.prefix("/api/v1");
319
320 // if this is not a valid page, redirect it
321 if ( !isRestRequest &&
322 ( pageName != Data("index.html") ) &&
323 ( pageName != Data("input") ) &&
324 ( pageName != Data("cert.cer") ) &&
325 ( ! pageName.prefix("cert") ) &&
326 ( pageName != Data("userTest.html") ) &&
327 ( pageName != Data("domains.html") ) &&
328 ( pageName != Data("acls.html") ) &&
329 ( pageName != Data("addUser.html") ) &&
330 ( pageName != Data("editUser.html") ) &&
331 ( pageName != Data("showUsers.html") ) &&
332 ( pageName != Data("addFilter.html") ) &&
333 ( pageName != Data("editFilter.html") ) &&
334 ( pageName != Data("showFilters.html") )&&
335 ( pageName != Data("addRoute.html") ) &&
336 ( pageName != Data("editRoute.html") ) &&
337 ( pageName != Data("showRoutes.html") )&&
338 ( pageName != Data("registrations.html") ) &&
339 ( pageName != Data("publications.html")) &&
340 ( pageName != Data("settings.html")) &&
341 ( pageName != Data("restart.html") ) &&
342 ( pageName != Data("logLevel.html") ) &&
343 ( pageName != Data("reloadcerts.html") ) &&
344 ( pageName != Data("user.html") ) )
345 {
346 setPage( resip::Data::Empty, pageNumber, 301 );
347 return;
348 }
349
350 // pages anyone can use
351 if ( pageName == Data("index.html") )
352 {
353 setPage( buildDefaultPage(), pageNumber, 200);
354 return;
355 }
356
357 // certificate pages
358 if ( pageName.prefix("cert") || pageName == Data("cert.cer") )
359 {
360#ifdef USE_SSL
361 Data domain = mRealm;
362 try
363 {
364 const char* anchor = pb.skipChar('?');
365 pb.skipToChar('=');
366 Data query;
367 pb.data(query, anchor);
368 InfoLog( << "query is " << query );
369 if ( query == "domain" )
370 {
371 anchor = pb.skipChar('=');
372 pb.skipToEnd();
373 pb.data(domain, anchor);
374 }
375 }
376 catch (ParseException& )
377 {
378 }
379
380 if ( !domain.empty() )
381 {
382 InfoLog( << "domain is " << domain );
383 try
384 {
385 setPage( buildCertPage(domain), pageNumber, 200, Mime("application","pkix-cert") );
386 }
388 {
389 setPage( resip::Data::Empty, pageNumber, 404 );
390 }
391 return;
392 }
393 else
394 {
395 setPage( resip::Data::Empty, pageNumber, 404 );
396 return;
397 }
398#else
399 // ?bwc? Probably could use a better indication?
400 setPage(resip::Data::Empty, pageNumber, 404);
401#endif
402 }
403
404 Data authenticatedUser;
405 if (mNoWebChallenges)
406 {
407 // do't do authentication - give everyone admin privilages
408 authenticatedUser = Data("admin");
409 }
410 else
411 {
412 // TODO !cj! - this code is broken - the user name in the web digest should be
413 // moved to alice@example.com instead of alice and assuming the realm is
414 // empty
415
416 // all pages after this, user must authenticate
417 if ( pUser.empty() )
418 {
419 setPage( resip::Data::Empty, pageNumber,401 );
420 return;
421 }
422
423 // check that authentication is correct
424 Data dbA1;
425 std::map<Data,Data>::iterator it = mUsers.find(pUser);
426 if(it != mUsers.end())
427 {
428 dbA1 = it->second;
429 }
430
431 if ( !dbA1.empty() )
432 {
433 DigestStream a1;
434 a1 << pUser // username
436 << mRealm // realm
438 << pPassword;
439 Data compA1 = a1.getHex();
440
441 if ( dbA1 == compA1 )
442 {
443 authenticatedUser = pUser;
444 }
445 else
446 {
447 InfoLog( << "user " << pUser << " failed to authenticate to web server" );
448 DebugLog( << " compA1="<<compA1<< " dbA1="<<dbA1 );
449 setPage( resip::Data::Empty, pageNumber,401 );
450 return;
451 }
452 }
453 else //No A1, so we must assume this user does not exist.
454 {
455 setPage( "User does not exist.", pageNumber,401 );
456 return;
457 }
458 }
459
460 // If this is a REST API request, dispatch to the REST handler.
461 // The REST handler is responsible for parsing query parameters and
462 // generating a JSON response.
463 if ( isRestRequest )
464 {
465 resip_assert( mRestAdmin );
466 mRestAdmin->dispatch(method, uri, pageNumber, authenticatedUser);
467 return;
468 }
469
470 // parse any URI tags from form entry
471 mRemoveSet.clear();
472 mHttpParams.clear();
473
474 if (!pb.eof())
475 {
476 pb.skipChar('?');
477
478 while ( !pb.eof() )
479 {
480 const char* anchor1 = pb.position();
481 pb.skipToChar('=');
482 Data key;
483 pb.data(key,anchor1);
484
485 const char* anchor2 = pb.skipChar('=');
486 pb.skipToChar('&');
487 Data value;
488 pb.data(value,anchor2);
489
490 if ( !pb.eof() )
491 {
492 pb.skipChar('&');
493 }
494
495 if ( key.prefix("remove.") ) // special case of parameters to delete one or more records
496 {
497 Data tmp = key.substr(7); // the ID is everything after the dot
498 if (!tmp.empty())
499 {
500 DebugLog (<< " remove key=" << tmp.urlDecoded());
501 mRemoveSet.insert(RemoveKey(tmp.urlDecoded(),value.urlDecoded())); // add to the set of records to remove
502 }
503 }
504 else if ( !key.empty() && !value.empty() ) // make sure both exist
505 {
506 DebugLog (<< " key=" << key << " value=" << value << " & unencoded form: " << value.urlDecoded() );
507 mHttpParams[key] = value.urlDecoded(); // add other parameters to the Map
508 }
509 }
510 }
511
512 DebugLog( << "building page for user=" << authenticatedUser );
513
514 Data page;
515 if ( authenticatedUser == Data("admin") )
516 {
517 DataStream s(page);
518 s << mPageOutlinePre;
519
520 // admin only pages
521 if ( pageName == Data("user.html") ) {}; /* do nothing */
522 //if ( pageName == Data("input") ) ; /* do nothing */
523 if ( pageName == Data("domains.html") ) buildDomainsSubPage(s);
524 if ( pageName == Data("acls.html") ) buildAclsSubPage(s);
525
526 if ( pageName == Data("addUser.html") ) buildAddUserSubPage(s);
527 if ( pageName == Data("editUser.html") ) buildEditUserSubPage(s);
528 if ( pageName == Data("showUsers.html") ) buildShowUsersSubPage(s);
529
530 if ( pageName == Data("addFilter.html") ) buildAddFilterSubPage(s);
531 if ( pageName == Data("editFilter.html") ) buildEditFilterSubPage(s);
532 if ( pageName == Data("showFilters.html") ) buildShowFiltersSubPage(s);
533
534 if ( pageName == Data("addRoute.html") ) buildAddRouteSubPage(s);
535 if ( pageName == Data("editRoute.html") ) buildEditRouteSubPage(s);
536 if ( pageName == Data("showRoutes.html") ) buildShowRoutesSubPage(s);
537
538 if ( pageName == Data("registrations.html")) buildRegistrationsSubPage(s);
539 if ( pageName == Data("publications.html")) buildPublicationsSubPage(s);
540 if ( pageName == Data("settings.html")) buildSettingsSubPage(s);
541 if ( pageName == Data("restart.html")) buildRestartSubPage(s);
542 if ( pageName == Data("logLevel.html")) buildLogLevelSubPage(s);
543 if ( pageName == Data("reloadcerts.html")) buildReloadCertsSubPage(s);
544
545 s << mPageOutlinePost;
546 s.flush();
547
548 if ( pageName == Data("userTest.html") ) page=buildUserPage();
549 }
550 else if ( !authenticatedUser.empty() )
551 {
552 // user only pages
553 if ( pageName == Data("user.html") ) page=buildUserPage();
554 //if ( pageName == Data("input") ) page=buildUserPage();
555
556 if(page.empty())
557 {
558 DataStream s(page);
559 s << "Invalid page request";
560 s.flush();
561 setPage(page, pageNumber, 500);
562 return;
563 }
564 }
565
566 resip_assert( !authenticatedUser.empty() );
567 resip_assert( !page.empty() );
568
569 setPage( page, pageNumber,200 );
570}
571
572void
573WebAdmin::buildDomainsSubPage(DataStream& s)
574{
575 Data domainUri;
576
577 if (!mRemoveSet.empty() && (mHttpParams["action"] == "Remove"))
578 {
579 int j = 0;
580 for (set<RemoveKey>::iterator i = mRemoveSet.begin(); i != mRemoveSet.end(); ++i)
581 {
582 mStore.mConfigStore.eraseDomain(i->mKey1);
583 ++j;
584 }
585 s << "<p class=\"notice ok\"><em>Removed:</em> " << j << " records</p>" << endl;
586 }
587
588 Dictionary::iterator pos = mHttpParams.find("domainUri");
589 if (pos != mHttpParams.end() && (mHttpParams["action"] == "Add")) // found domainUri key
590 {
591 domainUri = pos->second;
592 // The per-domain TLS port is dead weight: nothing in repro reads it back
593 // (ConfigStore::getTlsPort has no callers), so the field is no longer
594 // offered in the UI and every record is written with 0. The column stays
595 // in the database record for schema compatibility.
596 if(mStore.mConfigStore.addDomain(domainUri, 0))
597 {
598 s << "<p class=\"notice ok\"><em>Added</em> domain: " << domainUri << "</p>" << endl;
599 }
600 else
601 {
602 s << "<p class=\"notice err\"><em>Error</em> adding domain: likely database error (check logs).</p>\n";
603 }
604 }
605
606 s <<
607 " <h2>Domains</h2>" << endl <<
608 " <p>Requests addressed to these domains are treated as locally served, rather than" << endl <<
609 " being proxied onwards to another host.</p>" << endl <<
610 " <form id=\"domainForm\" method=\"get\" action=\"domains.html\" name=\"domainForm\">" << endl <<
611 " <table" REPRO_BORDERLESS_TABLE_PROPS ">" << endl <<
612 " <tr>" << endl <<
613 " <td align=\"right\">New Domain:</td>" << endl <<
614 " <td><input type=\"text\" name=\"domainUri\" size=\"24\" placeholder=\"example.com\"/></td>" << endl <<
615 " <td><input type=\"submit\" name=\"action\" value=\"Add\"/></td>" << endl <<
616 " </tr>" << endl <<
617 " </table>" << endl <<
618 " <div class=space>" << endl <<
619 " <br>" << endl <<
620 " </div>" << endl <<
621 " <table" REPRO_BORDERED_TABLE_PROPS ">" << endl <<
622 " <thead>" << endl <<
623 " <tr>" << endl <<
624 " <td>Domain</td>" << endl <<
625 " <td><input type=\"submit\" name=\"action\" value=\"Remove\"/></td>" << endl <<
626 " </tr>" << endl <<
627 " </thead>" << endl <<
628 " <tbody>" << endl;
629
630 const ConfigStore::ConfigData& configs = mStore.mConfigStore.getConfigs();
631 for ( ConfigStore::ConfigData::const_iterator i = configs.begin();
632 i != configs.end(); i++ )
633 {
634 s <<
635 " <tr>" << endl <<
636 " <td>" << i->second.mDomain << "</td>" << endl <<
637 " <td><input type=\"checkbox\" name=\"remove." << i->second.mDomain << "\"/></td>" << endl <<
638 " </tr>" << endl;
639 }
640
641 s <<
642 " </tbody>" << endl <<
643 " </table>" << endl <<
644 " </form>" << endl <<
645 "<p class=\"notice warn\"><em>WARNING:</em> You must restart repro after adding domains.</p>" << endl;
646}
647
648void
649WebAdmin::buildAclsSubPage(DataStream& s)
650{
651 if (!mRemoveSet.empty() && (mHttpParams["action"] == "Remove"))
652 {
653 int j = 0;
654 for (set<RemoveKey>::iterator i = mRemoveSet.begin(); i != mRemoveSet.end(); ++i)
655 {
656 mStore.mAclStore.eraseAcl(i->mKey1);
657 ++j;
658 }
659 s << "<p class=\"notice ok\"><em>Removed:</em> " << j << " records</p>" << endl;
660 }
661
662 Dictionary::iterator pos = mHttpParams.find("aclUri");
663 if (pos != mHttpParams.end() && (mHttpParams["action"] == "Add")) // found
664 {
665 Data hostOrIp = mHttpParams["aclUri"];
666 int port = mHttpParams["aclPort"].convertInt();
667 TransportType transport = Tuple::toTransport(mHttpParams["aclTransport"]);
668
669 if (mStore.mAclStore.addAcl(hostOrIp, port, transport))
670 {
671 s << "<p class=\"notice ok\"><em>Added</em> trusted access for: " << hostOrIp << "</p>\n";
672 }
673 else
674 {
675 s << "<p class=\"notice err\"><em>Error</em> parsing: " << hostOrIp << "</p>\n";
676 }
677 }
678
679 s <<
680 " <h2>ACLs</h2>" << endl <<
681 " <div class=\"help\">" << endl <<
682 " <p>Access lists are used as a whitelist to allow gateways and other trusted" << endl <<
683 " nodes to skip authentication.</p>" << endl <<
684 " <p>If a hostname or FQDN is used then a TLS transport type is assumed. All" << endl <<
685 " other transport types must specify ACLs by address. Leave Port empty to match" << endl <<
686 " any port.</p>" << endl <<
687 " <pre>Host or IP can be in any of these formats" << endl <<
688 " localhost localhost (becomes 127.0.0.1/8, ::1/128 and fe80::1/64)" << endl <<
689 " bare hostname server1" << endl <<
690 " FQDN server1.example.com" << endl <<
691 " IPv4 address 192.168.1.100" << endl <<
692 " IPv4 + mask 192.168.1.0/24" << endl <<
693 " IPv6 address ::341:0:23:4bb:0011:2435:abcd" << endl <<
694 " IPv6 + mask ::341:0:23:4bb:0011:2435:abcd/80" << endl <<
695 " IPv6 reference [::341:0:23:4bb:0011:2435:abcd]" << endl <<
696 " IPv6 ref + mask [::341:0:23:4bb:0011:2435:abcd]/64</pre>" << endl <<
697 " </div>" << endl <<
698 " <form id=\"aclsForm\" method=\"get\" action=\"acls.html\" name=\"aclsForm\">" << endl <<
699 " <table" REPRO_BORDERLESS_TABLE_PROPS ">" << endl <<
700 " <tr>" << endl <<
701 " <td align=\"right\">Host or IP:</td>" << endl <<
702 " <td><input type=\"text\" name=\"aclUri\" size=\"24\" placeholder=\"host, FQDN or IP/mask\"/></td>" << endl <<
703 // An empty port box submits nothing, and convertInt() of a missing param is
704 // 0, which is what "any port" means here -- so the box can be left blank.
705 " <td><input type=\"text\" name=\"aclPort\" size=\"10\" placeholder=\"Port\"/></td>" << endl <<
706 " <td><select name=\"aclTransport\">" << endl <<
707 " <option selected=\"selected\">UDP</option>" << endl <<
708 " <option>TCP</option>" << endl <<
709#ifdef USE_SSL
710 " <option>TLS</option>" << endl <<
711#endif
712#ifdef USE_DTLS
713 " <option>DTLS</option>" << endl <<
714#endif
715 " </select></td>" << endl <<
716 " <td><input type=\"submit\" name=\"action\" value=\"Add\"/></td>" << endl <<
717 " </tr>" << endl <<
718 " </table>" << endl <<
719 " <div class=space>" << endl <<
720 " <br>" << endl <<
721 " </div>" << endl <<
722 " <table" REPRO_BORDERED_TABLE_PROPS ">" << endl <<
723 " <thead>" << endl <<
724 " <tr>" << endl <<
725 " <td>Host Address or Peer Name</td>" << endl <<
726 " <td>Port</td>" << endl <<
727 " <td>Transport</td>" << endl <<
728 " <td><input type=\"submit\" name=\"action\" value=\"Remove\"/></td>" << endl <<
729 " </tr>" << endl <<
730 " </thead>" << endl <<
731 " <tbody>" << endl;
732
734 while (key != Data::Empty)
735 {
736 s <<
737 " <tr>" << endl <<
738 " <td colspan=\"2\">" << mStore.mAclStore.getTlsPeerName(key) << "</td>" << endl <<
739 " <td>TLS auth</td>" << endl <<
740 " <td><input type=\"checkbox\" name=\"remove." << key << "\"/></td>" << endl <<
741 "</tr>" << endl;
742
743 key = mStore.mAclStore.getNextTlsPeerNameKey(key);
744 }
745 key = mStore.mAclStore.getFirstAddressKey();
746 while (key != Data::Empty)
747 {
748 s <<
749 " <tr>" << endl <<
750 " <td>" << mStore.mAclStore.getAddressTuple(key).presentationFormat() << "/"
751 << mStore.mAclStore.getAddressMask(key) << "</td>" << endl <<
752 " <td>" << mStore.mAclStore.getAddressTuple(key).getPort() << "</td>" << endl <<
753 " <td>" << Tuple::toData(mStore.mAclStore.getAddressTuple(key).getType()) << "</td>" << endl <<
754 " <td><input type=\"checkbox\" name=\"remove." << key << "\"/></td>" << endl <<
755 " </tr>" << endl;
756 key = mStore.mAclStore.getNextAddressKey(key);
757 }
758
759 s <<
760 " </tbody>" << endl <<
761 " </table>" << endl <<
762 " </form>" << endl;
763}
764
765void
766WebAdmin::buildAddUserSubPage(DataStream& s)
767{
768 Dictionary::iterator pos;
769 Data user;
770
771 pos = mHttpParams.find("user");
772 if (pos != mHttpParams.end()) // found user key
773 {
774 user = pos->second;
775 Data domain = mHttpParams["domain"];
776
777// pos = mHttpParams.find("realm");
778// if (pos == mHttpParams.end())
779// {
780// realm = mHttpParams["domain"];
781// }
782
783 if(mStore.mUserStore.addUser(user,domain,domain,mHttpParams["password"],true,mHttpParams["name"],mHttpParams["email"]))
784 {
785 s << "<p class=\"notice ok\"><em>Added:</em> " << user << "@" << domain << "</p>\n";
786 }
787 else
788 {
789 s << "<p class=\"notice err\"><em>Error</em> adding user: likely database error (check logs).</p>\n";
790 }
791 }
792
793 s <<
794 "<h2>Add User</h2>" << endl <<
795 "<div class=\"help\">" << endl <<
796 " <p>Full Name and Email are recorded for display purposes only. They are not used" << endl <<
797 " for authentication, routing, or as a SIP identity.</p>" << endl <<
798 "</div>" << endl <<
799 "<form id=\"addUserForm\" action=\"addUser.html\" method=\"get\" name=\"addUserForm\" enctype=\"application/x-www-form-urlencoded\">" << endl <<
800 "<table" REPRO_BORDERLESS_TABLE_PROPS ">" << endl <<
801 "<tr>" << endl <<
802 " <td align=\"right\" valign=\"middle\">User Name:</td>" << endl <<
803 " <td align=\"left\" valign=\"middle\"><input type=\"text\" name=\"user\" size=\"40\" placeholder=\"alice\"/></td>" << endl <<
804 "</tr>" << endl <<
805
806 //"<tr>" << endl <<
807 //"<td align=\"right\" valign=\"middle\" >Realm:</td>" << endl <<
808 //"<td align=\"left\" valign=\"middle\"><input type=\"text\" name=\"realm\" size=\"40\"/></td>" << endl <<
809 //"</tr>" << endl <<
810
811 "<tr>" << endl <<
812 " <td align=\"right\" valign=\"middle\" >Domain:</td>" << endl <<
813 " <td align=\"left\" valign=\"middle\"><select name=\"domain\">" << endl
814 ;
815
816 // for each domain, add an option in the pulldown
818 for ( ConfigStore::ConfigData::const_iterator i = list.begin();
819 i != list.end(); i++ )
820 {
821 s << " <option";
822
823 // if i->Domain is the default domain
824 // {
825 // s << " selected=\"true\"";
826 // }
827
828 s << ">" << i->second.mDomain << "</option>" << endl;
829 }
830
831 s <<
832 "</select></td></tr>" << endl <<
833 "<tr>" << endl <<
834 " <td align=\"right\" valign=\"middle\" >Password:</td>" << endl <<
835 " <td align=\"left\" valign=\"middle\"><input type=\"password\" name=\"password\" size=\"40\" placeholder=\"account password\"/></td>" << endl <<
836 "</tr>" << endl <<
837
838 "<tr>" << endl <<
839 " <td align=\"right\" valign=\"middle\" >Full Name:</td>" << endl <<
840 " <td align=\"left\" valign=\"middle\"><input type=\"text\" name=\"name\" size=\"40\" placeholder=\"Alice Smith\"/></td>" << endl <<
841 "</tr>" << endl <<
842
843 "<tr>" << endl <<
844 " <td align=\"right\" valign=\"middle\" >Email:</td>" << endl <<
845 " <td align=\"left\" valign=\"middle\"><input type=\"text\" name=\"email\" size=\"40\" placeholder=\"alice@example.com\"/></td>" << endl <<
846 "</tr>" << endl <<
847
848 "<tr>" << endl <<
849 " <td colspan=\"2\" align=\"right\" valign=\"middle\">" << endl <<
850 " <input type=\"reset\" value=\"Cancel\"/>" << endl <<
851 " <input type=\"submit\" name=\"submit\" value=\"Add\"/>" << endl <<
852 " </td>" << endl <<
853 "</tr>" << endl <<
854
855 "</table>" << endl <<
856 "</form>" << endl
857 ;
858}
859
860void
861WebAdmin::buildEditUserSubPage(DataStream& s)
862{
863 Dictionary::iterator pos;
864 pos = mHttpParams.find("key");
865 if (pos != mHttpParams.end())
866 {
867 Data key = pos->second;
869 // !rwm! TODO check to see if we actually found a record corresponding to the key. how do we do that?
870
871 s << "<h2>Edit User</h2>" << endl <<
872 "<div class=\"help\">" << endl <<
873 " <p>Editing record with key: " << key << "</p>" << endl <<
874 " <p>If you leave the password field empty, the user's current "
875 "password will not be reset. However, if you change the username or "
876 "domain, a new password is required (the stored password hash is "
877 "bound to user+realm, so renaming without a new password would "
878 "invalidate the account).</p>" << endl <<
879 " <p>Full Name and Email are recorded for display purposes only. They are not used "
880 "for authentication, routing, or as a SIP identity.</p>" << endl <<
881 "</div>" << endl;
882
883 s <<
884 "<form id=\"editUserForm\" action=\"showUsers.html\" method=\"get\" name=\"editUserForm\" enctype=\"application/x-www-form-urlencoded\">" << endl <<
885 "<table" REPRO_BORDERLESS_TABLE_PROPS ">" << endl <<
886 "<input type=\"hidden\" name=\"key\" value=\"" << key << "\"/>" << endl <<
887 "<tr>" << endl <<
888 " <td align=\"right\" valign=\"middle\">User Name:</td>" << endl <<
889 " <td align=\"left\" valign=\"middle\"><input type=\"text\" name=\"user\" placeholder=\"alice\" value=\"" << rec.user << "\" size=\"40\"/></td>" << endl <<
890 "</tr>" << endl <<
891
892 //"<tr>" << endl <<
893 //"<td align=\"right\" valign=\"middle\" >Realm:</td>" << endl <<
894 //"<td align=\"left\" valign=\"middle\"><input type=\"text\" name=\"realm\" size=\"40\"/></td>" << endl <<
895 //"</tr>" << endl <<
896
897 "<tr>" << endl <<
898 " <td align=\"right\" valign=\"middle\" >Domain:</td>" << endl <<
899 " <td align=\"left\" valign=\"middle\"><select name=\"domain\">" << endl
900 ;
901
902 // for each domain, add an option in the pulldown
904 for ( ConfigStore::ConfigData::const_iterator i = list.begin();
905 i != list.end(); i++ )
906 {
907 s << " <option";
908
909 if ( i->second.mDomain == rec.domain)
910 {
911 s << " selected=\"true\"";
912 }
913
914 s << ">" << i->second.mDomain << "</option>" << endl;
915 }
916
917 s <<
918 "</select></td></tr>" << endl <<
919 "<tr>" << endl <<
920 " <td align=\"right\" valign=\"middle\" >Password:</td>" << endl <<
921 " <td align=\"left\" valign=\"middle\"><input type=\"password\" name=\"password\" size=\"40\" placeholder=\"leave empty to keep current password\"/></td>" << endl <<
922 "</tr>" << endl <<
923 // Note that the UserStore only stores a passwordHash, so we will collect a password. If one is provided in the
924 // edit page, we will use it to generate a new passwordHash, otherwise we will leave the hash alone.
925
926 "<tr>" << endl <<
927 " <td align=\"right\" valign=\"middle\" >Full Name:</td>" << endl <<
928 " <td align=\"left\" valign=\"middle\"><input type=\"text\" name=\"name\" value=\"" << rec.name <<
929 "\" size=\"40\" placeholder=\"Alice Smith\"/></td>" << endl <<
930 "</tr>" << endl <<
931
932 "<tr>" << endl <<
933 " <td align=\"right\" valign=\"middle\" >Email:</td>" << endl <<
934 " <td align=\"left\" valign=\"middle\"><input type=\"text\" name=\"email\" value=\"" << rec.email <<
935 "\" size=\"40\" placeholder=\"alice@example.com\"/></td>" << endl <<
936 "</tr>" << endl <<
937
938 "<tr>" << endl <<
939 " <td colspan=\"2\" align=\"right\" valign=\"middle\">" << endl <<
940 " <input type=\"submit\" name=\"submit\" value=\"Update\"/>" << endl <<
941 " </td>" << endl <<
942 "</tr>" << endl <<
943
944 "</table>" << endl <<
945 "</form>" << endl
946 ;
947 }
948 else
949 {
950 // go back to show users page
951 }
952}
953
954void
955WebAdmin::buildShowUsersSubPage(DataStream& s)
956{
957 Dictionary::iterator pos;
958 Data key;
960
961 if (!mRemoveSet.empty())
962 {
963 int j = 0;
964 for (set<RemoveKey>::iterator i = mRemoveSet.begin(); i != mRemoveSet.end(); ++i)
965 {
966 mStore.mUserStore.eraseUser(i->mKey1);
967 ++j;
968 }
969 s << "<p class=\"notice ok\"><em>Removed:</em> " << j << " records</p>" << endl;
970 }
971
972 pos = mHttpParams.find("key");
973 if (pos != mHttpParams.end()) // check if the user parameter exists, if so, use as a key to update the record
974 {
975 key = pos->second;
976 rec = mStore.mUserStore.getUserInfo(key);
977 // check to see if we actually found a record corresponding to the key
978 if (!rec.user.empty())
979 {
980 Data user = mHttpParams["user"];
981 Data domain = mHttpParams["domain"];
982 Data realm = mHttpParams["domain"]; // eventually sort out realms
983 Data password = mHttpParams["password"];
984 Data passwordHashAlt = Data::Empty;
985 Data name = mHttpParams["name"];
986 Data email = mHttpParams["email"];
987 bool applyA1HashToPassword = true;
988
989 // The stored password hash is MD5(user:realm:password), so if the
990 // username or domain changes, the old hash is no longer valid for
991 // the new identity and a new password is required.
992 bool identityChanged = (user != rec.user) || (realm != rec.realm);
993 if (identityChanged && password.empty())
994 {
995 s << "<p class=\"notice err\"><em>Error</em> updating user: changing user or domain "
996 "requires a new password (the stored password hash is bound "
997 "to user+realm).</p>\n";
998 }
999 else
1000 {
1001 // if no password was specified (and identity is unchanged), leave
1002 // the current password hash untouched.
1003 if (password.empty() && !identityChanged)
1004 {
1005 password = rec.passwordHash;
1006 passwordHashAlt = rec.passwordHashAlt;
1007 applyA1HashToPassword = false;
1008 }
1009 // write out the updated record to the database now
1010 if(mStore.mUserStore.updateUser(key, user, domain, realm, password, applyA1HashToPassword, name, email, passwordHashAlt))
1011 {
1012 s << "<p class=\"notice ok\"><em>Updated:</em> " << key << "</p>" << endl;
1013 }
1014 else
1015 {
1016 s << "<p class=\"notice err\"><em>Error</em> updating user: likely database error (check logs).</p>\n";
1017 }
1018 }
1019 }
1020 }
1021
1022 s <<
1023 "<h2>Users</h2>" << endl <<
1024 "<form id=\"showUsers\" method=\"get\" action=\"showUsers.html\" name=\"showUsers\" enctype=\"application/x-www-form-urlencoded\">" << endl <<
1025 "<table" REPRO_BORDERED_TABLE_PROPS ">" << endl <<
1026 "<thead><tr>" << endl <<
1027 " <td>User@Domain</td>" << endl <<
1028 // " <td>Realm</td>" << endl <<
1029 " <td>Name</td>" << endl <<
1030 " <td>Email</td>" << endl <<
1031 " <td><input type=\"submit\" value=\"Remove\"/></td>" << endl <<
1032 "</tr></thead>" << endl <<
1033 "<tbody>" << endl;
1034
1035 s << endl;
1036
1037 int count =0;
1038
1039 key = mStore.mUserStore.getFirstKey();
1040 while ( !key.empty() )
1041 {
1042 rec = mStore.mUserStore.getUserInfo(key);
1043
1044 s << "<tr>" << endl
1045 << " <td><a href=\"editUser.html?key=";
1046 key.urlEncode(s);
1047 s << "\">" << rec.user << "@" << rec.domain << "</a></td>" << endl
1048 << " <td>" << rec.name << "</td>" << endl
1049 << " <td>" << rec.email << "</td>" << endl
1050 << " <td><input type=\"checkbox\" name=\"remove." << key << "\"/></td>" << endl
1051 << "</tr>" << endl;
1052
1053 key = mStore.mUserStore.getNextKey();
1054
1055 // make a limit to how many users are displayed
1056 if ( ++count > 1000 )
1057 {
1058 break;
1059 }
1060 }
1061
1062 if ( !key.empty() )
1063 {
1064 s << "<tr><td colspan=\"4\">Only first 1000 users were displayed</td></tr>" << endl;
1065 }
1066
1067 s <<
1068 "</tbody>" << endl <<
1069 "</table>" << endl <<
1070 "</form>" << endl;
1071}
1072
1073void
1074WebAdmin::buildAddFilterSubPage(DataStream& s)
1075{
1076 Dictionary::iterator pos;
1077
1078 pos = mHttpParams.find("cond1header");
1079 if (pos != mHttpParams.end())
1080 {
1081 Data action = mHttpParams["action"];
1082 Data actionData = mHttpParams["actiondata"];
1083
1084 if (action != "Accept" && actionData.empty())
1085 {
1086 s << "<p class=\"notice err\"><em>Error</em> adding request filter. You must provide appropriate Action Data for non-Accept action.</p>\n";
1087 }
1088 else
1089 {
1090 short actionShort = 0; // 0 - Accept, 1 - Reject, 2 - SQL Query
1091 if(action == "Reject") actionShort = 1;
1092 else if(action == "SQL Query") actionShort = 2;
1093
1094 if(mStore.mFilterStore.addFilter(mHttpParams["cond1header"],
1095 mHttpParams["cond1regex"],
1096 mHttpParams["cond2header"],
1097 mHttpParams["cond2regex"],
1098 mHttpParams["method"],
1099 mHttpParams["event"],
1100 actionShort,
1101 actionData,
1102 mHttpParams["order"].convertInt()))
1103 {
1104 s << "<p class=\"notice ok\"><em>Added</em> request filter: " << mHttpParams["cond1header"] << "=" << mHttpParams["cond1regex"] << ", "
1105 << mHttpParams["cond2header"] << "=" << mHttpParams["cond2regex"] << "</p>\n";
1106 }
1107 else
1108 {
1109 s << "<p class=\"notice err\"><em>Error</em> adding request filter, likely duplicate found.</p>\n";
1110 }
1111 }
1112 }
1113
1114 s <<
1115 "<h2>Add Request Filter</h2>" << endl <<
1116 "<div class=\"help\">" << endl <<
1117 " <p>A request filter matches an incoming request against one or two header values" << endl <<
1118 " and decides whether to accept or reject it. Both conditions must match for the" << endl <<
1119 " filter to fire; leave the second pair empty to match on one header alone. Regexes" << endl <<
1120 " are POSIX-standard and are matched against the header's value.</p>" << endl <<
1121 " <p>Method and Event narrow the filter to a single request type; leave them empty to" << endl <<
1122 " match any. Order decides evaluation sequence when several filters could match --" << endl <<
1123 " lowest first.</p>" << endl <<
1124 " <p>Action Data depends on the chosen Action:</p>" << endl <<
1125 " <pre>Accept Action Data is ignored." << endl <<
1126 "Reject SIPRejectionCode[, SIPReason] e.g. 403, Request Blocked" << endl <<
1127#ifdef USE_MYSQL
1128 "SQL Query The SQL query to execute. Replacement strings from the regexes" << endl <<
1129 " above can be used in the query. The query must return a string" << endl <<
1130 " formatted like the Reject data above, or a string with a status" << endl <<
1131 " code of 0 to accept the request." << endl <<
1132#endif
1133 "</pre>" << endl <<
1134 "</div>" << endl <<
1135 "<form id=\"addFilterForm\" method=\"get\" action=\"addFilter.html\" name=\"addFilterForm\">" << endl <<
1136 "<table" REPRO_BORDERLESS_TABLE_PROPS ">" << endl <<
1137
1138 "<tr>" << endl <<
1139 " <td align=\"right\" valign=\"middle\">Condition1 Header:</td>" << endl <<
1140 " <td align=\"left\" valign=\"middle\"><input type=\"text\" name=\"cond1header\" size=\"40\" value=\"From\" placeholder=\"header name, e.g. From\"/></td>" << endl <<
1141 "</tr>" << endl <<
1142
1143 "<tr>" << endl <<
1144 " <td align=\"right\" valign=\"middle\">Condition1 Regex:</td>" << endl <<
1145 " <td align=\"left\" valign=\"middle\"><input type=\"text\" name=\"cond1regex\" size=\"40\" placeholder=\"POSIX regex, e.g. sip:1234@.*\"/></td>" << endl <<
1146 "</tr>" << endl <<
1147
1148 "<tr>" << endl <<
1149 " <td align=\"right\" valign=\"middle\">Condition2 Header:</td>" << endl <<
1150 " <td align=\"left\" valign=\"middle\"><input type=\"text\" name=\"cond2header\" size=\"40\" value=\"To\" placeholder=\"header name, e.g. To\"/></td>" << endl <<
1151 "</tr>" << endl <<
1152
1153 "<tr>" << endl <<
1154 " <td align=\"right\" valign=\"middle\">Condition2 Regex:</td>" << endl <<
1155 " <td align=\"left\" valign=\"middle\"><input type=\"text\" name=\"cond2regex\" size=\"40\" placeholder=\"POSIX regex, empty = any\"/></td>" << endl <<
1156 "</tr>" << endl <<
1157
1158 "<tr>" << endl <<
1159 " <td align=\"right\" valign=\"middle\">Method:</td>" << endl <<
1160 " <td align=\"left\" valign=\"middle\"><input type=\"text\" name=\"method\" size=\"40\" placeholder=\"INVITE, REGISTER... empty = any\"/></td>" << endl <<
1161 "</tr>" << endl <<
1162
1163 "<tr>" << endl <<
1164 " <td align=\"right\" valign=\"middle\">Event:</td>" << endl <<
1165 " <td align=\"left\" valign=\"middle\"><input type=\"text\" name=\"event\" size=\"40\" placeholder=\"presence, message-summary... empty = any\"/></td>" << endl <<
1166 "</tr>" << endl <<
1167
1168 "<tr>" << endl <<
1169 " <td align=\"right\" valign=\"middle\">Action:</td>" << endl <<
1170 " <td align=\"left\" valign=\"middle\">" << endl <<
1171 " <select name=\"action\">" << endl <<
1172 " <option>Reject</option>" << endl <<
1173 " <option>Accept</option>" << endl <<
1174#ifdef USE_MYSQL
1175 " <option>SQL Query</option>" << endl <<
1176#endif
1177 " </select>" << endl <<
1178 " </td>" << endl <<
1179 "</tr>" << endl <<
1180
1181 "<tr>" << endl <<
1182 " <td align=\"right\" valign=\"middle\">Action Data:</td>" << endl <<
1183 " <td align=\"left\" valign=\"middle\"><input type=\"text\" name=\"actiondata\" size=\"40\" value=\"403, Request Blocked\" placeholder=\"e.g. 403, Request Blocked\"/></td>" << endl <<
1184 "</tr>" << endl <<
1185
1186 "<tr>" << endl <<
1187 " <td align=\"right\" valign=\"middle\">Order:</td>" << endl <<
1188 " <td align=\"left\" valign=\"middle\"><input type=\"text\" name=\"order\" size=\"8\" value=\"0\" placeholder=\"0\"/></td>" << endl <<
1189 "</tr>" << endl <<
1190
1191 "<tr>" << endl <<
1192 " <td colspan=\"2\" align=\"right\" valign=\"middle\">" << endl <<
1193 " <input type=\"reset\" value=\"Cancel\"/>" << endl <<
1194 " <input type=\"submit\" name=\"filterAdd\" value=\"Add\"/>" << endl <<
1195 " </td>" << endl <<
1196 "</tr>" << endl <<
1197
1198 "</table>" << endl <<
1199 "</form>" << endl;
1200}
1201
1202void
1203WebAdmin::buildEditFilterSubPage(DataStream& s)
1204{
1205 Dictionary::iterator pos;
1206 pos = mHttpParams.find("key");
1207 if (pos != mHttpParams.end())
1208 {
1209 Data key = pos->second;
1210
1211 // !rwm! TODO check to see if we actually found a record corresponding to the key. how do we do that?
1212 DebugLog( << "Creating page to edit filter " << key );
1213
1215
1216 s <<"<h2>Edit Request Filter</h2>" << endl <<
1217 "<p>Editing Record with conditions: " << rec.mCondition1Header << "=" << rec.mCondition1Regex << ", "
1218 << rec.mCondition2Header << "=" << rec.mCondition2Regex << "</p>" << endl;
1219
1220 s <<
1221 "<form id=\"editFilterForm\" method=\"get\" action=\"showFilters.html\" name=\"editFilterForm\">" << endl <<
1222 "<table" REPRO_BORDERLESS_TABLE_PROPS ">" << endl <<
1223 "<input type=\"hidden\" name=\"key\" value=\"" << key << "\"/>" << endl <<
1224 "<tr>" << endl <<
1225
1226 "<tr>" << endl <<
1227 " <td align=\"right\" valign=\"middle\">Condition1 Header:</td>" << endl <<
1228 " <td align=\"left\" valign=\"middle\"><input type=\"text\" name=\"cond1header\" size=\"40\" placeholder=\"header name, e.g. From\" value=\"" << rec.mCondition1Header.xmlCharDataEncode() << "\"/></td>" << endl <<
1229 "</tr>" << endl <<
1230
1231 "<tr>" << endl <<
1232 " <td align=\"right\" valign=\"middle\">Condition1 Regex:</td>" << endl <<
1233 " <td align=\"left\" valign=\"middle\"><input type=\"text\" name=\"cond1regex\" size=\"40\" placeholder=\"POSIX regex, e.g. sip:1234@.*\" value=\"" << rec.mCondition1Regex.xmlCharDataEncode() << "\"/></td>" << endl <<
1234 "</tr>" << endl <<
1235
1236 "<tr>" << endl <<
1237 " <td align=\"right\" valign=\"middle\">Condition2 Header:</td>" << endl <<
1238 " <td align=\"left\" valign=\"middle\"><input type=\"text\" name=\"cond2header\" size=\"40\" placeholder=\"header name, e.g. To\" value=\"" << rec.mCondition2Header.xmlCharDataEncode() << "\"/></td>" << endl <<
1239 "</tr>" << endl <<
1240
1241 "<tr>" << endl <<
1242 " <td align=\"right\" valign=\"middle\">Condition2 Regex:</td>" << endl <<
1243 " <td align=\"left\" valign=\"middle\"><input type=\"text\" name=\"cond2regex\" size=\"40\" placeholder=\"POSIX regex, empty = any\" value=\"" << rec.mCondition2Regex.xmlCharDataEncode() << "\"/></td>" << endl <<
1244 "</tr>" << endl <<
1245
1246 "<tr>" << endl <<
1247 " <td align=\"right\" valign=\"middle\">Method:</td>" << endl <<
1248 " <td align=\"left\" valign=\"middle\"><input type=\"text\" name=\"method\" size=\"40\" placeholder=\"INVITE, REGISTER... empty = any\" value=\"" << rec.mMethod << "\"/></td>" << endl <<
1249 "</tr>" << endl <<
1250
1251 "<tr>" << endl <<
1252 " <td align=\"right\" valign=\"middle\">Event:</td>" << endl <<
1253 " <td align=\"left\" valign=\"middle\"><input type=\"text\" name=\"event\" size=\"40\" placeholder=\"presence, message-summary... empty = any\" value=\"" << rec.mEvent << "\"/></td>" << endl <<
1254 "</tr>" << endl <<
1255
1256 "<tr>" << endl <<
1257 " <td align=\"right\" valign=\"middle\">Action:</td>" << endl <<
1258 " <td align=\"left\" valign=\"middle\">" << endl <<
1259 " <select name=\"action\">" << endl <<
1260 " <option" << (rec.mAction == FilterStore::Reject ? " selected=\"selected\"" : "") << ">Reject</option>" << endl <<
1261 " <option" << (rec.mAction == FilterStore::Accept ? " selected=\"selected\"" : "") << ">Accept</option>" << endl <<
1262#ifdef USE_MYSQL
1263 " <option" << (rec.mAction == FilterStore::SQLQuery ? " selected=\"selected\"" : "") << ">SQL Query</option>" << endl <<
1264#endif
1265 " </select>" << endl <<
1266 " </td>" << endl <<
1267 "</tr>" << endl <<
1268
1269 "<tr>" << endl <<
1270 " <td align=\"right\" valign=\"middle\">Action Data:</td>" << endl <<
1271 " <td align=\"left\" valign=\"middle\"><input type=\"text\" name=\"actiondata\" size=\"40\" placeholder=\"e.g. 403, Request Blocked\" value=\"" << rec.mActionData.xmlCharDataEncode() << "\"/></td>" << endl <<
1272 "</tr>" << endl <<
1273
1274 "<tr>" << endl <<
1275 " <td align=\"right\" valign=\"middle\">Order:</td>" << endl <<
1276 " <td align=\"left\" valign=\"middle\"><input type=\"text\" name=\"order\" size=\"8\" placeholder=\"0\" value=\"" << rec.mOrder << "\"/></td>" << endl <<
1277 "</tr>" << endl <<
1278
1279 "<tr>" << endl <<
1280 " <td colspan=\"2\" align=\"right\" valign=\"middle\">" << endl <<
1281 " <input type=\"submit\" name=\"routeEdit\" value=\"Update\"/>" << endl <<
1282 " </td>" << endl <<
1283 "</tr>" << endl <<
1284
1285 "</table>" << endl <<
1286 "</form>" << endl;
1287 }
1288 else
1289 {
1290 // go back to show filter page
1291 }
1292}
1293
1294void
1295WebAdmin::buildShowFiltersSubPage(DataStream& s)
1296{
1297 Dictionary::iterator pos;
1298 Data key;
1300
1301 if (!mRemoveSet.empty())
1302 {
1303 int j = 0;
1304 for (set<RemoveKey>::iterator i = mRemoveSet.begin(); i != mRemoveSet.end(); ++i)
1305 {
1306 mStore.mFilterStore.eraseFilter(i->mKey1);
1307 ++j;
1308 }
1309 s << "<p class=\"notice ok\"><em>Removed:</em> " << j << " records</p>" << endl;
1310 }
1311
1312 pos = mHttpParams.find("key");
1313 if (pos != mHttpParams.end()) // if a key parameter exists, use the key to update the record
1314 {
1315 key = pos->second;
1316
1317 // !rwm! TODO check to see if we actually found a record corresponding to the key. how do we do that?
1318 if (1)
1319 {
1320 Data action = mHttpParams["action"];
1321 Data actionData = mHttpParams["actiondata"];
1322
1323 if (action != "Accept" && actionData.empty())
1324 {
1325 s << "<p class=\"notice err\"><em>Error</em> updating request filter. You must provide appropriate Action Data for non-Accept action.</p>\n";
1326 }
1327 else
1328 {
1329 short actionShort = 0; // 0 - Accept, 1 - Reject, 2 - SQL Query
1330 if(action == "Reject") actionShort = 1;
1331 else if(action == "SQL Query") actionShort = 2;
1332
1333 if(mStore.mFilterStore.updateFilter(key,
1334 mHttpParams["cond1header"],
1335 mHttpParams["cond1regex"],
1336 mHttpParams["cond2header"],
1337 mHttpParams["cond2regex"],
1338 mHttpParams["method"],
1339 mHttpParams["event"],
1340 actionShort,
1341 actionData,
1342 mHttpParams["order"].convertInt()))
1343 {
1344 s << "<p class=\"notice ok\"><em>Updated</em> request filter: " << mHttpParams["cond1header"] << "=" << mHttpParams["cond1regex"] << ", "
1345 << mHttpParams["cond2header"] << "=" << mHttpParams["cond2regex"] << "</p>\n";
1346 }
1347 else
1348 {
1349 s << "<p class=\"notice err\"><em>Error</em> updating request filter: likely database error (check logs).</p>\n";
1350 }
1351 }
1352 }
1353 }
1354
1355 s <<
1356 "<h2>Request Filters</h2>" << endl <<
1357 "<form id=\"showFilters\" action=\"showFilters.html\" method=\"get\" name=\"showFilters\" enctype=\"application/x-www-form-urlencoded\">" << endl <<
1358 // " <button name=\"removeAllRoute\" value=\"\" type=\"submit\">Remove All</button>" << endl <<
1359 "<table" REPRO_BORDERED_TABLE_PROPS ">" << endl <<
1360 "<thead><tr>" << endl <<
1361 " <td>Condition 1</td>" << endl <<
1362 " <td>Condition 2</td>" << endl <<
1363 " <td>Method</td>" << endl <<
1364 " <td>Event</td>" << endl <<
1365 " <td>Action</td>" << endl <<
1366 " <td>Action Data</td>" << endl <<
1367 " <td>Order</td>" << endl <<
1368 " <td><input type=\"submit\" value=\"Remove\"/></td>" << endl <<
1369 "</tr></thead>" << endl <<
1370 "<tbody>" << endl;
1371
1372 for(FilterStore::Key key = mStore.mFilterStore.getFirstKey();
1373 !key.empty();
1374 key = mStore.mFilterStore.getNextKey(key))
1375 {
1377 Data action("Accept");
1378 if(rec.mAction == FilterStore::Reject)
1379 {
1380 action = "Reject";
1381 }
1382 else if(rec.mAction == FilterStore::SQLQuery)
1383 {
1384 action = "SQL Query";
1385 }
1386 s << "<tr>" << endl <<
1387 "<td><a href=\"editFilter.html?key=";
1388 key.urlEncode(s);
1389 s <<
1390 "\">" << rec.mCondition1Header << "=" << rec.mCondition1Regex << "</a></td>" << endl <<
1391 "<td>" << rec.mCondition2Header << "=" << rec.mCondition2Regex << "</td>" << endl <<
1392 "<td>" << rec.mMethod << "</td>" << endl <<
1393 "<td>" << rec.mEvent << "</td>" << endl <<
1394 "<td>" << action << "</td>" << endl <<
1395 "<td>" << rec.mActionData << "</td>" << endl <<
1396 "<td>" << rec.mOrder << "</td>" << endl <<
1397 "<td><input type=\"checkbox\" name=\"remove." << key << "\"/></td>" << endl <<
1398 "</tr>" << endl;
1399 }
1400
1401 s <<
1402 "</tbody>" << endl <<
1403 "</table>" << endl <<
1404 "</form>" << endl;
1405
1406 Data cond1TestHeader;
1407 pos = mHttpParams.find("cond1TestHeader");
1408 if (pos != mHttpParams.end()) // found it
1409 {
1410 cond1TestHeader = pos->second;
1411 }
1412 Data cond2TestHeader;
1413 pos = mHttpParams.find("cond2TestHeader");
1414 if (pos != mHttpParams.end()) // found it
1415 {
1416 cond2TestHeader = pos->second;
1417 }
1418
1419 s <<
1420 "<h3>Test Filters</h3>" << endl <<
1421 "<form id=\"testFilter\" action=\"showFilters.html\" method=\"get\" name=\"testFilter\" enctype=\"application/x-www-form-urlencoded\">" << endl <<
1422 "<table" REPRO_BORDERLESS_TABLE_PROPS ">" << endl <<
1423 "<tr>" << endl <<
1424 " <td align=\"right\">Condition 1 Header:</td>" << endl <<
1425 " <td><input type=\"text\" name=\"cond1TestHeader\" placeholder=\"header value to match, e.g. sip:1234@example.com\" value=\"" << cond1TestHeader.xmlCharDataEncode() << "\" size=\"40\"/></td>" << endl <<
1426 "</tr>" << endl <<
1427 "<tr>" << endl <<
1428 " <td align=\"right\">Condition 2 Header:</td>" << endl <<
1429 " <td><input type=\"text\" name=\"cond2TestHeader\" placeholder=\"header value to match, empty if unused\" value=\"" << cond2TestHeader.xmlCharDataEncode() << "\" size=\"40\"/></td>" << endl <<
1430 " <td><input type=\"submit\" name=\"testFilter\" value=\"Test Filters\"/></td>" << endl <<
1431 "</tr>" << endl <<
1432 "</table>" << endl <<
1433 "</form>" << endl;
1434
1435 if(!cond1TestHeader.empty())
1436 {
1437 s << "<p class=\"notice\"><em>Test Result:</em> ";
1438 short action;
1439 Data actionData;
1440 if(mStore.mFilterStore.test(cond1TestHeader, cond2TestHeader, action, actionData))
1441 {
1442 switch(action)
1443 {
1445 s << "Match found, action=Reject " << actionData << endl;
1446 break;
1448 s << "Match found, action=SQL Query '" << actionData << "'" << endl;
1449 break;
1451 default:
1452 s << "Match found, action=Accept" << endl;
1453 break;
1454 }
1455 }
1456 else
1457 {
1458 s << "<em class=\"warn\">No matches</em>";
1459 }
1460 s << "</p>" << endl;
1461 }
1462}
1463
1464void
1465WebAdmin::buildAddRouteSubPage(DataStream& s)
1466{
1467 Dictionary::iterator pos;
1468
1469 pos = mHttpParams.find("routeUri");
1470 if (pos != mHttpParams.end())
1471 {
1472 Data routeUri = mHttpParams["routeUri"];
1473 Data routeDestination = mHttpParams["routeDestination"];
1474
1475 if (!routeUri.empty() && !routeDestination.empty())
1476 {
1477 if(mStore.mRouteStore.addRoute(mHttpParams["routeMethod"],
1478 mHttpParams["routeEvent"],
1479 routeUri,
1480 routeDestination,
1481 mHttpParams["routeOrder"].convertInt()))
1482 {
1483 s << "<p class=\"notice ok\"><em>Added</em> route for: " << routeUri << "</p>\n";
1484 }
1485 else
1486 {
1487 s << "<p class=\"notice err\"><em>Error</em> adding route, likely duplicate found.</p>\n";
1488 }
1489 }
1490 else
1491 {
1492 s << "<p class=\"notice err\"><em>Error</em> adding route. You must provide a URI and a route destination.</p>\n";
1493 }
1494 }
1495
1496 s <<
1497 "<h2>Add Route</h2>" << endl <<
1498 "<div class=\"help\">" << endl <<
1499 " <p>Static routes use (POSIX-standard) regular expressions to match and rewrite SIP" << endl <<
1500 " URIs. Method and Event narrow the route to a single request type; leave them empty" << endl <<
1501 " to match any. Order decides evaluation sequence when several routes could match --" << endl <<
1502 " lowest first.</p>" << endl <<
1503 " <p>The following sends all requests whose userpart is only digits to a gateway:</p>" << endl <<
1504 " <pre>URI: ^sip:([0-9]+)@example\\.com" << endl <<
1505 "Destination: sip:$1@gateway.example.com</pre>" << endl <<
1506 "</div>" << endl <<
1507 "<form id=\"addRouteForm\" method=\"get\" action=\"addRoute.html\" name=\"addRouteForm\">" << endl <<
1508 "<table" REPRO_BORDERLESS_TABLE_PROPS ">" << endl <<
1509
1510 "<tr>" << endl <<
1511 " <td align=\"right\" valign=\"middle\">URI:</td>" << endl <<
1512 " <td align=\"left\" valign=\"middle\"><input type=\"text\" name=\"routeUri\" size=\"40\" placeholder=\"^sip:([0-9]+)@example.com\"/></td>" << endl <<
1513 "</tr>" << endl <<
1514
1515 "<tr>" << endl <<
1516 " <td align=\"right\" valign=\"middle\">Method:</td>" << endl <<
1517 " <td align=\"left\" valign=\"middle\"><input type=\"text\" name=\"routeMethod\" size=\"40\" placeholder=\"INVITE, REGISTER... empty = any\"/></td>" << endl <<
1518 "</tr>" << endl <<
1519
1520 "<tr>" << endl <<
1521 " <td align=\"right\" valign=\"middle\">Event:</td>" << endl <<
1522 " <td align=\"left\" valign=\"middle\"><input type=\"text\" name=\"routeEvent\" size=\"40\" placeholder=\"presence, message-summary... empty = any\"/></td>" << endl <<
1523 "</tr>" << endl <<
1524
1525 "<tr>" << endl <<
1526 " <td align=\"right\" valign=\"middle\">Destination:</td>" << endl <<
1527 " <td align=\"left\" valign=\"middle\"><input type=\"text\" name=\"routeDestination\" size=\"40\" placeholder=\"sip:$1@gateway.example.com\"/></td>" << endl <<
1528 "</tr>" << endl <<
1529
1530 "<tr>" << endl <<
1531 " <td align=\"right\" valign=\"middle\">Order:</td>" << endl <<
1532 " <td align=\"left\" valign=\"middle\"><input type=\"text\" name=\"routeOrder\" size=\"8\" placeholder=\"0\"/></td>" << endl <<
1533 "</tr>" << endl <<
1534
1535 "<tr>" << endl <<
1536 " <td colspan=\"2\" align=\"right\" valign=\"middle\">" << endl <<
1537 " <input type=\"reset\" value=\"Cancel\"/>" << endl <<
1538 " <input type=\"submit\" name=\"routeAdd\" value=\"Add\"/>" << endl <<
1539 " </td>" << endl <<
1540 "</tr>" << endl <<
1541
1542 "</table>" << endl <<
1543 "</form>" << endl;
1544}
1545
1546void
1547WebAdmin::buildEditRouteSubPage(DataStream& s)
1548{
1549 Dictionary::iterator pos;
1550 pos = mHttpParams.find("key");
1551 if (pos != mHttpParams.end())
1552 {
1553 Data key = pos->second;
1554
1555 // !rwm! TODO check to see if we actually found a record corresponding to the key. how do we do that?
1556 DebugLog( << "Creating page to edit route " << key );
1557
1559
1560 s <<"<h2>Edit Route</h2>" << endl <<
1561 "<p>Editing Record with matching pattern: " << rec.mMatchingPattern << "</p>" << endl;
1562
1563 s <<
1564 "<form id=\"editRouteForm\" method=\"get\" action=\"showRoutes.html\" name=\"editRouteForm\">" << endl <<
1565 "<table" REPRO_BORDERLESS_TABLE_PROPS ">" << endl <<
1566 "<input type=\"hidden\" name=\"key\" value=\"" << key << "\"/>" << endl <<
1567 "<tr>" << endl <<
1568 "<td align=\"right\" valign=\"middle\">URI:</td>" << endl <<
1569 "<td align=\"left\" valign=\"middle\"><input type=\"text\" name=\"routeUri\" placeholder=\"^sip:([0-9]+)@example.com\" value=\"" << rec.mMatchingPattern << "\" size=\"40\"/></td>" << endl <<
1570 "</tr>" << endl <<
1571
1572 "<tr>" << endl <<
1573 "<td align=\"right\" valign=\"middle\">Method:</td>" << endl <<
1574 "<td align=\"left\" valign=\"middle\"><input type=\"text\" name=\"routeMethod\" placeholder=\"INVITE, REGISTER... empty = any\" value=\"" << rec.mMethod << "\" size=\"40\"/></td>" << endl <<
1575 "</tr>" << endl <<
1576
1577 "<tr>" << endl <<
1578 "<td align=\"right\" valign=\"middle\">Event:</td>" << endl <<
1579 "<td align=\"left\" valign=\"middle\"><input type=\"text\" name=\"routeEvent\" placeholder=\"presence, message-summary... empty = any\" value=\"" << rec.mEvent << "\" size=\"40\"/></td>" << endl <<
1580 "</tr>" << endl <<
1581
1582 "<tr>" << endl <<
1583 "<td align=\"right\" valign=\"middle\">Destination:</td>" << endl <<
1584 "<td align=\"left\" valign=\"middle\"><input type=\"text\" name=\"routeDestination\" placeholder=\"sip:$1@gateway.example.com\" value=\"" << rec.mRewriteExpression <<
1585 "\" size=\"40\"/></td>" << endl <<
1586 "</tr>" << endl <<
1587
1588 "<tr>" << endl <<
1589 "<td align=\"right\" valign=\"middle\">Order:</td>" << endl <<
1590 "<td align=\"left\" valign=\"middle\"><input type=\"text\" name=\"routeOrder\" placeholder=\"0\" value=\"" << rec.mOrder <<
1591 "\" size=\"4\"/></td>" << endl <<
1592 "</tr>" << endl <<
1593
1594 "<tr>" << endl <<
1595 " <td colspan=\"2\" align=\"right\" valign=\"middle\">" << endl <<
1596 " <input type=\"submit\" name=\"routeEdit\" value=\"Update\"/>" << endl <<
1597 " </td>" << endl <<
1598 "</tr>" << endl <<
1599
1600 "</table>" << endl <<
1601 "</form>" << endl;
1602 }
1603 else
1604 {
1605 // go back to show route page
1606 }
1607}
1608
1609void
1610WebAdmin::buildShowRoutesSubPage(DataStream& s)
1611{
1612 Dictionary::iterator pos;
1613 Data key;
1615
1616 if (!mRemoveSet.empty())
1617 {
1618 int j = 0;
1619 for (set<RemoveKey>::iterator i = mRemoveSet.begin(); i != mRemoveSet.end(); ++i)
1620 {
1621 mStore.mRouteStore.eraseRoute(i->mKey1);
1622 ++j;
1623 }
1624 s << "<p class=\"notice ok\"><em>Removed:</em> " << j << " records</p>" << endl;
1625 }
1626
1627 pos = mHttpParams.find("key");
1628 if (pos != mHttpParams.end()) // if a key parameter exists, use the key to update the record
1629 {
1630 key = pos->second;
1631
1632 // !rwm! TODO check to see if we actually found a record corresponding to the key. how do we do that?
1633 if (1)
1634 {
1635 Data method = mHttpParams["routeMethod"];
1636 Data event = mHttpParams["routeEvent"];
1637 Data matchingPattern = mHttpParams["routeUri"];
1638 Data rewriteExpression = mHttpParams["routeDestination"];
1639 int order = mHttpParams["routeOrder"].convertInt();
1640
1641 if (!matchingPattern.empty() && !rewriteExpression.empty())
1642 {
1643 // write out the updated record to the database now
1644 if(mStore.mRouteStore.updateRoute(key, method, event, matchingPattern, rewriteExpression, order))
1645 {
1646 s << "<p class=\"notice ok\"><em>Updated:</em> " << rec.mMatchingPattern << "</p>" << endl;
1647 }
1648 else
1649 {
1650 s << "<p class=\"notice err\"><em>Error</em> updating route: likely database error (check logs).</p>\n";
1651 }
1652 }
1653 else
1654 {
1655 s << "<p class=\"notice err\"><em>Error</em> updating route. You must provide a URI and a route destination.</p>\n";
1656 }
1657 }
1658 }
1659
1660 s <<
1661 "<h2>Routes</h2>" << endl <<
1662 "<form id=\"showRoutes\" action=\"showRoutes.html\" method=\"get\" name=\"showRoutes\" enctype=\"application/x-www-form-urlencoded\">" << endl <<
1663 // " <button name=\"removeAllRoute\" value=\"\" type=\"submit\">Remove All</button>" << endl <<
1664 "<table" REPRO_BORDERED_TABLE_PROPS ">" << endl <<
1665 "<thead><tr>" << endl <<
1666 " <td>URI</td>" << endl <<
1667 " <td>Method</td>" << endl <<
1668 " <td>Event</td>" << endl <<
1669 " <td>Destination</td>" << endl <<
1670 " <td>Order</td>" << endl <<
1671 " <td><input type=\"submit\" value=\"Remove\"/></td>" << endl <<
1672 "</tr></thead>" << endl <<
1673 "<tbody>" << endl;
1674
1675 for ( RouteStore::Key key = mStore.mRouteStore.getFirstKey();
1676 !key.empty();
1677 key = mStore.mRouteStore.getNextKey(key) )
1678 {
1680
1681 s << "<tr>" << endl <<
1682 "<td><a href=\"editRoute.html?key=";
1683 key.urlEncode(s);
1684 s <<
1685 "\">" << rec.mMatchingPattern << "</a></td>" << endl <<
1686 "<td>" << rec.mMethod << "</td>" << endl <<
1687 "<td>" << rec.mEvent << "</td>" << endl <<
1688 "<td>" << rec.mRewriteExpression << "</td>" << endl <<
1689 "<td>" << rec.mOrder << "</td>" << endl <<
1690 "<td><input type=\"checkbox\" name=\"remove." << key << "\"/></td>" << endl <<
1691 "</tr>" << endl;
1692 }
1693
1694 s <<
1695 "</tbody>" << endl <<
1696 "</table>" << endl <<
1697 "</form>" << endl;
1698
1699 int badUri = true;
1700 Uri uri;
1701 Data routeTestUri;
1702
1703 pos = mHttpParams.find("routeTestUri");
1704 if (pos != mHttpParams.end()) // found it
1705 {
1706 routeTestUri = pos->second;
1707 if ( routeTestUri != "sip:" )
1708 {
1709 try
1710 {
1711 uri = Uri(routeTestUri);
1712 badUri=false;
1713 }
1714 catch( BaseException& )
1715 {
1716 try
1717 {
1718 uri = Uri( Data("sip:")+routeTestUri );
1719 badUri=false;
1720 }
1721 catch( BaseException& )
1722 {
1723 }
1724 }
1725 }
1726 }
1727
1728 // !cj! - TODO - should input method and event type to test
1729 RouteStore::UriList routeList;
1730 if (!badUri)
1731 {
1732 routeList = mStore.mRouteStore.process(uri, Data("INVITE"), Data::Empty);
1733 }
1734
1735 s <<
1736 "<h3>Test Routes</h3>" << endl <<
1737 "<form id=\"testRoute\" action=\"showRoutes.html\" method=\"get\" name=\"testRoute\" enctype=\"application/x-www-form-urlencoded\">" << endl <<
1738 "<table" REPRO_BORDERLESS_TABLE_PROPS ">" << endl <<
1739 "<tr>" << endl <<
1740 " <td align=\"right\">Input:</td>" << endl <<
1741 " <td><input type=\"text\" name=\"routeTestUri\" placeholder=\"sip:1234@example.com\" value=\"" << uri << "\" size=\"40\"/></td>" << endl <<
1742 " <td><input type=\"submit\" name=\"testRoute\" value=\"Test Routes\"/></td>" << endl <<
1743 "</tr>" << endl;
1744
1745 bool first=true;
1746 for ( RouteStore::UriList::const_iterator i=routeList.begin();
1747 i != routeList.end(); i++)
1748 {
1749 s<<" <tr>" << endl;
1750 if (first)
1751 {
1752 first=false;
1753 s<<" <td align=\"right\">Targets:</td>" << endl;
1754 }
1755 else
1756 {
1757 s<<" <td align=\"right\"></td>" << endl;
1758 }
1759 s<<" <td><label>" << *i << "</label></td>" << endl;
1760 s<<" <td></td>" << endl;
1761 s<<" </tr>" << endl;
1762 }
1763
1764 // A test that matched nothing has to say so, otherwise the page looks
1765 // identical to one where the test was never run.
1766 if (first && !routeTestUri.empty())
1767 {
1768 s<<" <tr>" << endl;
1769 s<<" <td align=\"right\">Targets:</td>" << endl;
1770 s<<" <td><em class=\"warn\">"
1771 << (badUri ? "Not a valid SIP URI" : "No matches")
1772 << "</em></td>" << endl;
1773 s<<" <td></td>" << endl;
1774 s<<" </tr>" << endl;
1775 }
1776
1777
1778 s<<
1779 "</table>" << endl <<
1780 "</form>" << endl;
1781}
1782
1783void
1784WebAdmin::buildRegistrationsSubPage(DataStream& s)
1785{
1786 if (!mRemoveSet.empty())
1787 {
1788 int j = 0;
1789 for (set<RemoveKey>::iterator i = mRemoveSet.begin(); i != mRemoveSet.end(); ++i)
1790 {
1791 Uri aor(i->mKey1);
1793 Data::size_type bar1 = i->mKey2.find("|");
1794 Data::size_type bar2 = i->mKey2.find("|",bar1+1);
1795 Data::size_type bar3 = i->mKey2.find("|",bar2+1);
1796
1797 if(bar1==Data::npos || bar2 == Data::npos || bar3==Data::npos)
1798 {
1799 WarningLog(<< "Registration removal key was malformed: " << i->mKey2);
1800 continue;
1801 }
1802
1803 bool staticRegContact=false;
1804 try
1805 {
1806 resip::Data rawNameAddr = i->mKey2.substr(0,bar1).urlDecoded();
1807 rec.mContact = NameAddr(rawNameAddr);
1808 rec.mInstance = i->mKey2.substr(bar1+1,bar2-bar1-1).urlDecoded();
1809 rec.mRegId = i->mKey2.substr(bar2+1,Data::npos).convertInt();
1810 staticRegContact = i->mKey2.substr(bar3+1,Data::npos).convertInt() == 1;
1811
1812 // Remove from RegistrationPersistanceManager
1813 mRegDb.removeContact(aor, rec);
1814
1815 if(staticRegContact)
1816 {
1817 // Remove from StateRegStore
1818 mStore.mStaticRegStore.eraseStaticReg(aor, rec.mContact);
1819 }
1820
1821 ++j;
1822 }
1823 catch(resip::ParseBuffer::Exception& e)
1824 {
1825 WarningLog(<< "Registration removal key was malformed: " << e <<
1826 " Key was: " << i->mKey2);
1827 }
1828 }
1829 s << "<p class=\"notice ok\"><em>Removed:</em> " << j << " records</p>" << endl;
1830 }
1831
1832 Dictionary::iterator pos = mHttpParams.find("regAor");
1833 if (pos != mHttpParams.end() && (mHttpParams["action"] == "Add Static Registration")) // found
1834 {
1835 Data regAor = mHttpParams["regAor"];
1836 Data regContact = mHttpParams["regContact"];
1837 Data regPath = mHttpParams["regPath"];
1838
1840 try
1841 {
1842 rec.mContact = NameAddr(regContact);
1843 try
1844 {
1845 ParseBuffer pb(regPath);
1846 Data path;
1847 const char* anchor = pb.position();
1848 while(!pb.eof())
1849 {
1850 pb.skipToChar(Symbols::COMMA[0]);
1851 pb.data(path, anchor);
1852 rec.mSipPath.push_back(NameAddr(path));
1853 if(!pb.eof()) pb.skipChar(); // skip over comma
1854 anchor = pb.position();
1855 }
1856 try
1857 {
1859 rec.mSyncContact = true; // Tag this permanent contact as being a synchronized contact so that it will
1860 // not be synchronized to a paired server (this is actually configuration information)
1861
1862 // Add to DB Store
1863 Uri aor(regAor);
1864 if(mStore.mStaticRegStore.addStaticReg(aor, rec.mContact, rec.mSipPath))
1865 {
1866 // Add to RegistrationPersistanceManager
1867 mRegDb.updateContact(aor, rec);
1868
1869 s << "<p class=\"notice ok\"><em>Added</em> permanent registered contact for: " << regAor << "</p>\n";
1870 }
1871 else
1872 {
1873 s << "<p class=\"notice err\"><em>Error</em> adding static registration: likely database error (check logs).</p>\n";
1874 }
1875 }
1876 catch(resip::ParseBuffer::Exception& e)
1877 {
1878 WarningLog(<< "Registration add: aor " << regAor << " was malformed: " << e);
1879 s << "<p class=\"notice err\"><em>Error</em> parsing: AOR=" << regAor << "</p>\n";
1880 }
1881 }
1882 catch(resip::ParseBuffer::Exception& e)
1883 {
1884 WarningLog(<< "Registration add: path " << regPath << " was malformed: " << e);
1885 s << "<p class=\"notice err\"><em>Error</em> parsing: Path=" << regPath << "</p>\n";
1886 }
1887 }
1888 catch(resip::ParseBuffer::Exception& e)
1889 {
1890 WarningLog(<< "Registration add: contact " << regContact << " was malformed: " << e);
1891 s << "<p class=\"notice err\"><em>Error</em> parsing: Contact=" << regContact << "</p>\n";
1892 }
1893 }
1894
1895 s <<
1896 "<h2>Registrations</h2>" << endl <<
1897 "<form id=\"showReg\" method=\"get\" action=\"registrations.html\" name=\"showReg\" enctype=\"application/x-www-form-urlencoded\">" << endl <<
1898 //"<button name=\"removeAllReg\" value=\"\" type=\"button\">Remove All</button>" << endl <<
1899 //"<hr/>" << endl <<
1900
1901 "<h3>Add Static Registration</h3>" << endl <<
1902 "<p>A static registration is a permanent contact for an AOR: it never expires and is" << endl <<
1903 "restored on restart, so it does not need the device to send a REGISTER. Use it for" << endl <<
1904 "gateways and other endpoints that cannot register for themselves.</p>" << endl <<
1905 "<table" REPRO_BORDERLESS_TABLE_PROPS ">" << endl <<
1906 " <tr>" << endl <<
1907 " <td align=\"right\">AOR:</td>" << endl <<
1908 " <td><input type=\"text\" name=\"regAor\" size=\"46\" placeholder=\"sip:alice@example.com\"/></td>" << endl <<
1909 " </tr>" << endl <<
1910 " <tr>" << endl <<
1911 " <td align=\"right\">Contact:</td>" << endl <<
1912 " <td><input type=\"text\" name=\"regContact\" size=\"46\" placeholder=\"&lt;sip:alice@192.168.1.10:5060&gt;\"/></td>" << endl <<
1913 " </tr>" << endl <<
1914 " <tr>" << endl <<
1915 " <td align=\"right\">Path:</td>" << endl <<
1916 " <td><input type=\"text\" name=\"regPath\" size=\"46\" placeholder=\"optional, comma separated URIs\"/></td>" << endl <<
1917 " </tr>" << endl <<
1918 " <tr>" << endl <<
1919 " <td></td>" << endl <<
1920 " <td align=\"right\"><input type=\"submit\" name=\"action\" value=\"Add Static Registration\"/></td>" << endl <<
1921 " </tr>" << endl <<
1922 "</table>" << endl <<
1923
1924 "<h3>Active Registrations</h3>" << endl <<
1925 "<table" REPRO_BORDERED_TABLE_PROPS ">" << endl <<
1926
1927 "<thead><tr>" << endl <<
1928 " <td>AOR</td>" << endl <<
1929 " <td>Contact</td>" << endl <<
1930 " <td>User Agent</td>" << endl <<
1931 " <td>Instance ID</td>" << endl <<
1932 " <td>Reg ID</td>" << endl <<
1933 " <td>QValue</td>" << endl <<
1934 " <td>Path</td>" << endl <<
1935 " <td>Sync'd?</td>" << endl <<
1936 " <td>Expires In</td>" << endl <<
1937 " <td><input type=\"submit\" value=\"Remove\"/></td>" << endl <<
1938 "</tr></thead>" << endl <<
1939 "<tbody>" << endl;
1940
1943 mRegDb.getAors(aors);
1944 for ( RegistrationPersistenceManager::UriList::const_iterator
1945 aorIt = aors.begin(); aorIt != aors.end(); ++aorIt )
1946 {
1947 Uri aor = *aorIt;
1948 ContactList contacts;
1949 mRegDb.getContacts(aor, contacts);
1950
1951 bool first = true;
1952 for (ContactList::iterator i = contacts.begin();
1953 i != contacts.end(); ++i )
1954 {
1955 if(i->mRegExpires > now)
1956 {
1957 uint64_t secondsRemaining = i->mRegExpires - now;
1958
1959 s << "<tr>" << endl
1960 << " <td>" ;
1961 if (first)
1962 {
1963 s << Data::from(aor).xmlCharDataEncode();
1964 first = false;
1965 }
1966 s << "</td>" << endl
1967 << " <td>";
1968
1969 const ContactInstanceRecord& r = *i;
1970 const NameAddr& contact = r.mContact;
1971 const Data& instanceId = r.mInstance;
1972 int regId = r.mRegId;
1973
1974 s << Data::from(contact.uri()).xmlCharDataEncode();
1975 s << "</td>" << endl
1976 << " <td>";
1977
1979
1980 s <<"</td>" << endl
1981 << "<td>" << instanceId.xmlCharDataEncode()
1982 << "</td><td>" << regId
1983 << "</td><td>";
1984#ifdef RESIP_FIXED_POINT
1985 // If RESIP_FIXED_POINT is enabled then q-value is shown as an integer in the range of 0..1000 where 1000 = qvalue of 1.0
1986 s << (contact.exists(p_q) ? contact.param(p_q) : 1000) << "</td><td>";
1987#else
1988 s << (contact.exists(p_q) ? contact.param(p_q).floatVal() : 1.0f) << "</td><td>";
1989#endif
1990 NameAddrs::const_iterator naIt = r.mSipPath.begin();
1991 for(;naIt != r.mSipPath.end(); naIt++)
1992 {
1993 s << Data::from(naIt->uri()).xmlCharDataEncode() << "<br>" << endl;
1994 }
1995
1996 bool staticRegContact = r.mRegExpires == NeverExpire;
1997 if(!staticRegContact)
1998 {
1999 s << "</td><td>" << (r.mSyncContact ? "true" : "false") << "</td>" << endl;
2000 s << "<td>" << secondsRemaining << "s</td>" << endl;
2001 }
2002 else
2003 {
2004 s << "</td><td></td>" << endl;
2005 s << "</td><td>Never</td>" << endl;
2006 }
2007 s << " <td>"
2008 << "<input type=\"checkbox\" name=\"remove." << Data::from(aor).xmlCharDataEncode() << "\" value=\"" << Data::from(contact.uri()).urlEncoded()
2009 << "|" << instanceId.urlEncoded()
2010 << "|" << regId
2011 << "|" << (staticRegContact ? "1" : "0")
2012 << "\"/></td>" << endl
2013 << "</tr>" << endl;
2014 }
2015 else
2016 {
2017 // remove expired contact
2018 mRegDb.removeContact(aor, *i);
2019 }
2020 }
2021 }
2022
2023 s << "</tbody>" << endl <<
2024 "</table>" << endl <<
2025 "</form>" << endl;
2026}
2027
2028void
2029WebAdmin::buildPublicationsSubPage(DataStream& s)
2030{
2031 if (!mRemoveSet.empty())
2032 {
2033 int j = 0;
2034 for (set<RemoveKey>::iterator i = mRemoveSet.begin(); i != mRemoveSet.end(); ++i)
2035 {
2036 Data::size_type bar1 = i->mKey2.find("|");
2037
2038 if (bar1 == Data::npos)
2039 {
2040 WarningLog(<< "Publication removal key was malformed: " << i->mKey2);
2041 continue;
2042 }
2043
2044 try
2045 {
2046 Data eventType = i->mKey2.substr(0, bar1);
2047 Data eTag = i->mKey2.substr(bar1 + 1, Data::npos).urlDecoded();
2048 if (mPubDb.removeDocument(eventType, i->mKey1, eTag, Timer::getTimeSecs()))
2049 {
2050 ++j;
2051 }
2052 else
2053 {
2054 WarningLog(<< "Publication removal was unsuccessful: eventType=" << eventType << ", docKey=" << i->mKey1 << ", eTag=" << eTag);
2055 }
2056 }
2057 catch (resip::ParseBuffer::Exception& e)
2058 {
2059 WarningLog(<< "Publication removal key was malformed: " << e << " Key was: " << i->mKey2);
2060 }
2061 }
2062 s << "<p class=\"notice ok\"><em>Removed:</em> " << j << " records</p>" << endl;
2063 }
2064
2065 s <<
2066 "<h2>Publications</h2>" << endl <<
2067 "<form id=\"showPub\" method=\"get\" action=\"publications.html\" name=\"showPub\" enctype=\"application/x-www-form-urlencoded\">" << endl <<
2068 "<div class=space>" << endl <<
2069 "</div>" << endl <<
2070
2071 "<table" REPRO_BORDERED_TABLE_PROPS ">" << endl <<
2072
2073 "<thead><tr>" << endl <<
2074 " <td>AOR</td>" << endl <<
2075 " <td>Event Type</td>" << endl <<
2076 " <td>ETag</td>" << endl <<
2077 " <td>Data</td>" << endl <<
2078 " <td>Sync'd?</td>" << endl <<
2079 " <td>Expires In</td>" << endl <<
2080 " <td><input type=\"submit\" value=\"Remove\"/></td>" << endl <<
2081 "</tr></thead>" << endl <<
2082 "<tbody>" << endl;
2083
2085 mPubDb.lockDocuments();
2087 // Iterate through keys
2088 PublicationPersistenceManager::KeyToETagMap::iterator keyIt = publications.begin();
2089 for (; keyIt != publications.end(); keyIt++)
2090 {
2091 bool first = true;
2092 // Iterator through documents in sub-map
2093 PublicationPersistenceManager::ETagToDocumentMap::iterator eTagIt = keyIt->second.begin();
2094 for (; eTagIt != keyIt->second.end(); eTagIt++)
2095 {
2096 if (eTagIt->second.mExpirationTime > now)
2097 {
2098 uint64_t secondsRemaining = eTagIt->second.mExpirationTime - now;
2099
2100 s << "<tr>" << endl
2101 << " <td>";
2102 if (first)
2103 {
2104 s << eTagIt->second.mDocumentKey;
2105 }
2106 s << "</td>" << endl
2107 << " <td>";
2108
2109 if (first)
2110 {
2111 s << eTagIt->second.mEventType;
2112 first = false;
2113 }
2114 s << "</td>" << endl
2115 << " <td>";
2116 s << eTagIt->second.mETag;
2117 s << "</td>" << endl
2118 << " <td>";
2119 GenericPidfContents* pidf = dynamic_cast<GenericPidfContents*>(eTagIt->second.mContents.get());
2120 if (pidf)
2121 {
2122 s << (pidf->getSimplePresenceOnline() ? "open" : "closed");
2123 if (!pidf->getSimplePresenceNote().empty())
2124 {
2125 s << " - " << pidf->getSimplePresenceNote();
2126 }
2127 }
2128 s << "</td>" << endl;
2129
2130 s << "<td>" << (eTagIt->second.mSyncPublication ? "true" : "false") << "</td>" << endl
2131 << "<td>" << secondsRemaining << "s</td>" << endl
2132 << " <td>"
2133 << "<input type=\"checkbox\" name=\"remove." << eTagIt->second.mDocumentKey << "\" value=\"" << eTagIt->second.mEventType << "|" << eTagIt->second.mETag.urlEncoded()
2134 << "\"/></td>" << endl
2135 << "</tr>" << endl;
2136 }
2137 }
2138 }
2139 mPubDb.unlockDocuments();
2140 s << "</tbody>" << endl <<
2141 "</table>" << endl <<
2142 "</form>" << endl;
2143}
2144
2145void
2146WebAdmin::buildSettingsSubPage(DataStream& s)
2147{
2148 if (mHttpParams["action"] == "Clear DNS Cache")
2149 {
2150 mProxy.getStack().clearDnsCache();
2151 }
2152
2153 if (mHttpParams["action"] == "Reload DNS Servers")
2154 {
2155 mProxy.getStack().reloadDnsServers();
2156 }
2157
2158 s << "<h2>Logging and Admin</h2>" << endl
2159 << "<div class=\"toolbar\">" << endl
2160 << "<form id=\"logLevel\" method=\"get\" action=\"logLevel.html\" name=\"logLevel\">" << endl
2161 << " <label>Log level</label> <select name=\"level\">" << endl
2162 << " <option value=\"NONE\"" << (Log::level() == Log::None ? " selected" : "") << ">NONE" << (Log::level() == Log::None ? " *" : "") << "</option>" << endl
2163 << " <option value=\"CRIT\"" << (Log::level() == Log::Crit ? " selected" : "") << ">CRIT" << (Log::level() == Log::Crit ? " *" : "") << "</option>" << endl
2164 << " <option value=\"ERR\"" << (Log::level() == Log::Err ? " selected" : "") << ">ERR" << (Log::level() == Log::Err ? " *" : "") << "</option>" << endl
2165 << " <option value=\"WARNING\"" << (Log::level() == Log::Warning ? " selected" : "") << ">WARNING" << (Log::level() == Log::Warning ? " *" : "") << "</option>" << endl
2166 << " <option value=\"INFO\"" << (Log::level() == Log::Info ? " selected" : "") << ">INFO" << (Log::level() == Log::Info ? " *" : "") << "</option>" << endl
2167 << " <option value=\"DEBUG\"" << (Log::level() == Log::Debug ? " selected" : "") << ">DEBUG" << (Log::level() == Log::Debug ? " *" : "") << "</option>" << endl
2168 << " <option value=\"STACK\"" << (Log::level() == Log::Stack ? " selected" : "") << ">STACK" << (Log::level() == Log::Stack ? " *" : "") << "</option>" << endl
2169 << " </select>" << endl
2170 << " <input type=\"submit\" name=\"action\" value=\"Set level\"/>" << endl
2171 << "</form>" << endl;
2172
2173#ifdef USE_SSL
2174 s << "<form id=\"reloadCerts\" method=\"get\" action=\"reloadcerts.html\" name=\"reloadcerts\">" << endl
2175 << " <input type=\"submit\" name=\"action\" value=\"Reload Certificates\"/>" << endl
2176 << "</form>" << endl;
2177#endif
2178
2179 if (mProxy.getConfig().getConfigUnsignedShort("CommandPort", REPRO_DEFAULT_COMMAND_PORT) != 0)
2180 {
2181 s << "<form id=\"restartProxy\" method=\"get\" action=\"restart.html\" name=\"restart\">" << endl
2182 << " <input type=\"submit\" name=\"action\" value=\"Restart Proxy\"/>" << endl
2183 << "</form>" << endl;
2184 }
2185
2186 s << "</div>" << endl; // toolbar
2187
2188 s << "<h2>DNS Cache</h2>" << endl;
2189
2190 // Get Dns Cache
2191 {
2192 Lock lock(mDnsCacheMutex);
2193 mProxy.getStack().getDnsCacheDump(make_pair(0, 0), this);
2194 // Retrieving DNS cache is asyncronous
2195 // Use condition variable to wait for DNS results to be returned in onDnsCacheDumpRetrieved
2196 mDnsCacheCondition.wait(lock);
2197 s << "<pre>" << mDnsCache << "</pre>"
2198 << endl;
2199 }
2200
2201 s << "<div class=\"toolbar\">" << endl
2202 << "<form id=\"dnsButtons\" method=\"get\" action=\"settings.html\" name=\"dnsButtons\">" << endl
2203 << " <input type=\"submit\" name=\"action\" value=\"Clear DNS Cache\"/>" << endl
2204 << " <input type=\"submit\" name=\"action\" value=\"Reload DNS Servers\"/>" << endl
2205 << "</form>" << endl
2206 << "</div>" << endl;
2207
2208 {
2209 Data buffer;
2210 DataStream strm(buffer);
2211 mProxy.getStack().dump(strm);
2212 strm.flush();
2213 s << "<h2>Stack Info</h2>" << endl
2214 << "<pre>" << buffer << "</pre>"
2215 << endl;
2216 }
2217
2218 if(mProxy.getStack().getCongestionManager())
2219 {
2220 Data buffer;
2221 DataStream strm(buffer);
2223 s << "<h2>Congestion Manager Statistics</h2>" << endl
2224 << "<pre>" << buffer << "</pre>"
2225 << endl;
2226 }
2227
2228 s << "<h2>Settings</h2>" << endl <<
2229 "<pre>" << mProxy.getConfig() << "</pre>" << endl;
2230}
2231
2232void
2233WebAdmin::onDnsCacheDumpRetrieved(std::pair<unsigned long, unsigned long> key, const resip::Data& dnsEntryStrings)
2234{
2235 Lock lock(mDnsCacheMutex); (void)lock;
2236 if(dnsEntryStrings.empty())
2237 {
2238 mDnsCache = "<i>empty</i>";
2239 }
2240 else
2241 {
2242 mDnsCache = dnsEntryStrings;
2243 }
2244 mDnsCacheCondition.notify_one();
2245}
2246
2247void
2248WebAdmin::buildRestartSubPage(DataStream& s)
2249{
2250 unsigned short port = mProxy.getConfig().getConfigUnsignedShort("CommandPort", REPRO_DEFAULT_COMMAND_PORT);
2251 if (port != 0)
2252 {
2253 int sd = 0, rc;
2254 const char* host = "127.0.0.1";
2255
2256 struct addrinfo hints = {};
2257 struct addrinfo* res = nullptr;
2258 hints.ai_family = AF_INET;
2259 hints.ai_socktype = SOCK_STREAM;
2260
2261 if (getaddrinfo(host, nullptr, &hints, &res) == 0 && res != nullptr)
2262 {
2263 struct sockaddr_in servAddr = {};
2264 memcpy(&servAddr, res->ai_addr, res->ai_addrlen);
2265 servAddr.sin_port = htons(port);
2266 freeaddrinfo(res);
2267
2268 // Create TCP Socket
2269 sd = (int)socket(AF_INET, SOCK_STREAM, 0);
2270 if (sd >= 0)
2271 {
2272 // bind to any local interface/port
2273 struct sockaddr_in localAddr = {};
2274 localAddr.sin_family = AF_INET;
2275 localAddr.sin_addr.s_addr = htonl(INADDR_ANY);
2276 localAddr.sin_port = 0;
2277 rc = ::bind(sd, (struct sockaddr*)&localAddr, sizeof(localAddr));
2278 if (rc >= 0)
2279 {
2280 // Connect to server
2281 rc = ::connect(sd, (struct sockaddr*)&servAddr, sizeof(servAddr));
2282 if (rc >= 0)
2283 {
2284 Data request("<Restart>\r\n <Request>\r\b </Request>\r\n</Restart>\r\n");
2285 rc = send(sd, request.c_str(), (int)request.size(), 0);
2286 if (rc >= 0)
2287 {
2288 s << "<p class=\"notice ok\"><em>Restarting proxy...</em></p>" << endl;
2289 closeSocket(sd);
2290 return;
2291 }
2292 }
2293 }
2294 closeSocket(sd);
2295 }
2296 }
2297 s << "<p class=\"notice err\"><em>Error</em> issuing restart command.</p>" << endl;
2298 }
2299 else
2300 {
2301 s << "<p class=\"notice warn\"><em>Unavailable:</em> CommandServer must be running to use the restart feature.</p>" << endl;
2302 }
2303}
2304
2305void
2306WebAdmin::buildLogLevelSubPage(resip::DataStream& s)
2307{
2308 Dictionary::iterator pos;
2309 Data newLevel;
2310
2311 pos = mHttpParams.find("level");
2312 if (pos != mHttpParams.end()) // found user key
2313 {
2314 newLevel = pos->second;
2315 InfoLog(<<"new log level requested: " << newLevel);
2316
2317 Log::Level l = Log::toLevel(newLevel);
2318 Log::setLevel(l);
2319
2320 s << "<p class=\"notice ok\"><em>Log level changed</em> to " << newLevel.xmlCharDataEncode() << ".</p>" << endl;
2321 }
2322 else
2323 {
2324 WarningLog(<<"no log level specified");
2325 s << "<p class=\"notice err\"><em>Error</em> no log level specified.</p>" << endl;
2326 }
2327}
2328
2329void
2330WebAdmin::buildReloadCertsSubPage(resip::DataStream& s)
2331{
2332 mProxy.getStack().reloadCertificates();
2333 s << "<p class=\"notice ok\"><em>Reloaded certificates.</em></p>" << endl;
2334}
2335
2336Data
2337WebAdmin::buildUserPage()
2338{
2339 Data ret;
2340 {
2341 DataStream s(ret);
2342
2343 s << "<!DOCTYPE html>" << endl
2344 << "<html lang=\"en\">" << endl
2345 << "<head>" << endl
2346 << "<meta charset=\"utf-8\" />" << endl
2347 << "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />" << endl
2348 << "<title>Repro Proxy</title>" << endl
2349 << faviconLink()
2350 << standaloneStyle()
2351 << "</head>" << endl
2352 << "<body>" << endl
2353 << "<div class=\"card\">" << endl
2354 << " <div class=\"brand\">" << productMarkSvg() << "<span class=\"name\">Repro</span></div>" << endl;
2355
2356 //buildAddUserSubPage(s); // !cj! TODO - should do beter page here
2357
2358 s << "</div>" << endl
2359 << "</body>" << endl
2360 << "</html>" << endl;
2361
2362 s.flush();
2363 }
2364 return ret;
2365}
2366
2367Data
2368WebAdmin::buildCertPage(const Data& domain)
2369{
2370 resip_assert(!domain.empty());
2371#ifdef USE_SSL
2372 resip_assert( mProxy.getStack().getSecurity() );
2373 return mProxy.getStack().getSecurity()->getDomainCertDER(domain);
2374#else
2375 ErrLog( << "Proxy not build with support for certificates" );
2376 return Data::Empty;
2377#endif
2378}
2379
2380Data
2381WebAdmin::buildDefaultPage()
2382{
2383 Data ret;
2384 {
2385 DataStream s(ret);
2386
2387 s <<
2388 "<!DOCTYPE html>" << endl <<
2389 "<html lang=\"en\">" << endl <<
2390 "<head>" << endl <<
2391 "<meta charset=\"utf-8\" />" << endl <<
2392 "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />" << endl <<
2393 "<title>Repro Proxy Login</title>" << endl <<
2394 faviconLink() <<
2395 standaloneStyle() <<
2396 "</head>" << endl <<
2397
2398 "<body>" << endl <<
2399 " <div class=\"card\">" << endl <<
2400 " <div class=\"brand\">" << productMarkSvg() << "<span class=\"name\">Repro</span></div>" << endl <<
2401 " <div class=\"sub\">SIP Proxy &middot; Admin Console</div>" << endl <<
2402 " <a class=\"cta\" href=\"user.html\">Log in</a>" << endl <<
2403 " <p>Note: Web admin accounts are stored in a file (default filename is users.txt). "
2404 "You can create it with the <a href=\"https://httpd.apache.org/docs/current/programs/htdigest.html\">htdigest</a> utility.</p>" << endl <<
2405 " </div>" << endl <<
2406 "</body>" << endl <<
2407 "</html>" << endl;
2408
2409 s.flush();
2410 }
2411 return ret;
2412}
2413
2414
2415void
2416WebAdmin::setApiResponse(int pageNumber, int statusCode, const Data& jsonBody)
2417{
2418 setPage(jsonBody, pageNumber, statusCode, Mime("application", "json"));
2419}
2420
2421void
2423{
2424 // Called on the stack thread when a StatisticsMessage is delivered.
2425 // Copy the payload struct under the lock and wake any RestAdmin::handleStats
2426 // caller that is blocked waiting for fresh data. Serialization to JSON is
2427 // deferred to the REST handler so that the stack thread isn't doing any
2428 // more work here than necessary.
2429 {
2430 Lock lock(mStatsMutex); (void)lock;
2431 statsMessage.loadOut(mStatsPayload);
2432 mStatsReady = true;
2433 }
2434 mStatsCondition.notify_all();
2435}
2436
2437
2438/* ====================================================================
2439 * The Vovida Software License, Version 1.0
2440 *
2441 * Copyright (c) 2026 SIP Spectrum, Inc. https://www.sipspectrum.com
2442 * Copyright (c) 2000 Vovida Networks, Inc. All rights reserved.
2443 *
2444 * Redistribution and use in source and binary forms, with or without
2445 * modification, are permitted provided that the following conditions
2446 * are met:
2447 *
2448 * 1. Redistributions of source code must retain the above copyright
2449 * notice, this list of conditions and the following disclaimer.
2450 *
2451 * 2. Redistributions in binary form must reproduce the above copyright
2452 * notice, this list of conditions and the following disclaimer in
2453 * the documentation and/or other materials provided with the
2454 * distribution.
2455 *
2456 * 3. The names "VOCAL", "Vovida Open Communication Application Library",
2457 * and "Vovida Open Communication Application Library (VOCAL)" must
2458 * not be used to endorse or promote products derived from this
2459 * software without prior written permission. For written
2460 * permission, please contact vocal@vovida.org.
2461 *
2462 * 4. Products derived from this software may not be called "VOCAL", nor
2463 * may "VOCAL" appear in their name, without prior written
2464 * permission of Vovida Networks, Inc.
2465 *
2466 * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESSED OR IMPLIED
2467 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
2468 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE AND
2469 * NON-INFRINGEMENT ARE DISCLAIMED. IN NO EVENT SHALL VOVIDA
2470 * NETWORKS, INC. OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT DAMAGES
2471 * IN EXCESS OF $1,000, NOR FOR ANY INDIRECT, INCIDENTAL, SPECIAL,
2472 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
2473 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
2474 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
2475 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
2476 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
2477 * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
2478 * DAMAGE.
2479 *
2480 * ====================================================================
2481 */
#define DebugLog(args_)
Definition Logger.hxx:128
#define InfoLog(args_)
Definition Logger.hxx:131
#define WarningLog(args_)
Definition Logger.hxx:134
#define ErrLog(args_)
Definition Logger.hxx:137
#define resip_assert(x)
This file needs to work for both C and C++ code.
Handles cross-platform sockets compatibility.
void eraseAcl(const resip::Data &tlsPeerName, const resip::Data &address, const short &mask, const short &port, const short &family, const short &transport)
Definition AclStore.cxx:289
resip::Data getTlsPeerName(const resip::Data &key)
Definition AclStore.cxx:461
short getAddressMask(const resip::Data &key)
Definition AclStore.cxx:485
Key getNextTlsPeerNameKey(Key &key)
Definition AclStore.cxx:381
Key getFirstTlsPeerNameKey()
Definition AclStore.cxx:341
Key getNextAddressKey(Key &key)
Definition AclStore.cxx:441
resip::Tuple getAddressTuple(const resip::Data &key)
Definition AclStore.cxx:473
Key getFirstAddressKey()
Definition AclStore.cxx:401
bool addAcl(const resip::Data &tlsPeerName, const resip::Data &address, const short &mask, const short &port, const short &family, const short &transport)
Definition AclStore.cxx:62
bool addDomain(const resip::Data &domain, const int tlsPort)
std::map< resip::Data, AbstractDb::ConfigRecord > ConfigData
void eraseDomain(const resip::Data &domain)
const ConfigData & getConfigs() const
bool updateFilter(const resip::Data &originalKey, const resip::Data &cond1Header, const resip::Data &cond1Regex, const resip::Data &cond2Header, const resip::Data &cond2Regex, const resip::Data &method, const resip::Data &event, short action, const resip::Data &actionData, const short order)
Key getNextKey(Key &key)
bool addFilter(const resip::Data &cond1Header, const resip::Data &cond1Regex, const resip::Data &cond2Header, const resip::Data &cond2Regex, const resip::Data &method, const resip::Data &event, short action, const resip::Data &actionData, const short order)
void eraseFilter(const resip::Data &cond1Header, const resip::Data &cond1Regex, const resip::Data &cond2Header, const resip::Data &cond2Regex, const resip::Data &method, const resip::Data &event)
bool test(const resip::Data &cond1Header, const resip::Data &cond2Header, short &action, resip::Data &actionData)
AbstractDb::FilterRecord getFilterRecord(const resip::Data &key)
const resip::Data mRealm
Definition HttpBase.hxx:38
void setPage(const resip::Data &page, int pageNumber, int response=200, const resip::Mime &pType=resip::Mime("text","html"))
Definition HttpBase.cxx:220
ProxyConfig & getConfig() noexcept
Definition Proxy.hxx:89
resip::SipStack & getStack() noexcept
Definition Proxy.hxx:88
std::vector< resip::Uri > UriList
UriList process(const resip::Uri &ruri, const resip::Data &method, const resip::Data &event)
bool addRoute(const resip::Data &method, const resip::Data &event, const resip::Data &matchingPattern, const resip::Data &rewriteExpression, const short order)
AbstractDb::RouteRecord getRouteRecord(const resip::Data &key)
Key getNextKey(Key &key)
void eraseRoute(const resip::Data &method, const resip::Data &event, const resip::Data &matchingPattern, const short order)
bool updateRoute(const resip::Data &originalKey, const resip::Data &method, const resip::Data &event, const resip::Data &matchingPattern, const resip::Data &rewriteExpression, const short order)
bool addStaticReg(const resip::Uri &aor, const resip::NameAddr &contact, const resip::NameAddrs &path)
void eraseStaticReg(const resip::Uri &aor, const resip::NameAddr &contact)
FilterStore mFilterStore
Definition Store.hxx:40
AclStore mAclStore
Definition Store.hxx:37
ConfigStore mConfigStore
Definition Store.hxx:38
StaticRegStore mStaticRegStore
Definition Store.hxx:39
RouteStore mRouteStore
Definition Store.hxx:36
UserStore mUserStore
Definition Store.hxx:34
bool addUser(const resip::Data &user, const resip::Data &domain, const resip::Data &realm, const resip::Data &password, bool applyA1HashToPassword, const resip::Data &fullName, const resip::Data &emailAddress, const resip::Data &passwordHashAlt=resip::Data::Empty)
Definition UserStore.cxx:50
void eraseUser(const Key &key)
Definition UserStore.cxx:86
bool updateUser(const Key &originalKey, const resip::Data &user, const resip::Data &domain, const resip::Data &realm, const resip::Data &password, bool applyA1HashToPassword, const resip::Data &fullName, const resip::Data &emailAddress, const resip::Data &passwordHashAlt=resip::Data::Empty)
Definition UserStore.cxx:92
AbstractDb::UserRecord getUserInfo(const Key &key) const
Definition UserStore.cxx:36
static const VersionUtils & instance()
virtual void handleStatisticsMessage(resip::StatisticsMessage &statsMessage)
virtual void onDnsCacheDumpRetrieved(std::pair< unsigned long, unsigned long > key, const resip::Data &dnsEntryStrings)
void parseUserFile()
Load web admin users from a file.
Definition WebAdmin.cxx:203
void setApiResponse(int pageNumber, int statusCode, const resip::Data &jsonBody)
WebAdmin(Proxy &proxy, resip::RegistrationPersistenceManager &regDb, resip::PublicationPersistenceManager &pubDb, const resip::Data &realm, int port=5080, resip::IpVersion version=resip::V4, const resip::Data &ipAddr=resip::Data::Empty)
Definition WebAdmin.cxx:152
friend class RestAdmin
Definition WebAdmin.hxx:152
virtual ~WebAdmin()
Definition WebAdmin.cxx:183
virtual void buildPage(const resip::Data &method, const resip::Data &uri, int pageNumber, const resip::Data &user, const resip::Data &password)
Definition WebAdmin.cxx:298
The abstract base-class for all exceptions thrown by resip code.
Data getDomainCertDER(const Data &domainName) const
unsigned short getConfigUnsignedShort(const resip::Data &name, int defaultValue) const
virtual EncodeStream & encodeCurrentState(EncodeStream &strm) const =0
Get the current state of all monitored fifos.to a stream.
A single contact record, bound to an Aor during registration.
An iostream that operates on an existing Data.
An alternative to std::string, encapsulates an arbitrary buffer of bytes.
Definition Data.hxx:90
static const Data Empty
Constant that represents a zero-length data.
Definition Data.hxx:882
const char * c_str() const
Returns a null-terminated string representing.
Definition Data.cxx:1097
int replace(const Data &match, const Data &target, int max=INT_MAX)
Replaces up to max occurrences of the bytes match with target.
Definition Data.cxx:1964
Data xmlCharDataEncode() const
Performs in-place XML Character Data escaping of a Data.
Definition Data.cxx:1454
bool prefix(const Data &pre) const noexcept
Returns true if this Data starts with the bytes indicated by the passed-in Data.
Definition Data.cxx:1888
Data substr(size_type first, size_type count=Data::npos) const
Copies a portion of this Data into a new Data.
Definition Data.cxx:1932
bool empty() const noexcept
Checks whether the Data is empty.
Definition Data.hxx:591
Data urlDecoded() const
Performs in-place HTTP URL un-escaping of a Data.
Definition Data.cxx:1341
size_t size_type
Definition Data.hxx:94
size_type size() const noexcept
Returns the number of bytes in this Data.
Definition Data.hxx:599
EncodeStream & urlEncode(EncodeStream &s) const
Escapes a Data to a stream according to HTTP URL encoding rules.
Definition Data.cxx:1421
static Data from(const T &x)
Converts from arbitrary other type to Data.
Definition Data.hxx:398
static const size_type npos
Represents an impossible position; returned to indicate failure to find.
Definition Data.hxx:887
Data urlEncoded() const
Performs in-place HTTP URL escaping of a Data.
Definition Data.cxx:1331
const char * data() const noexcept
Returns a pointer to the contents of this Data.
Definition Data.hxx:608
Used to accumlate data written to the stream in a DigestBuffer and convert the data to Digest.
Data getHex()
Calls flush() on itself and returns the Digest data in hex format.
SIP body type for holding PIDF contents (MIME content-type application/pidf+xml).
static Level toLevel(const Data &l)
Definition Log.cxx:580
static void setLevel(Level level)
Set logging level for current thread.
Definition Log.cxx:423
static Level level()
Return logging level for current thread.
Definition Log.hxx:221
@ Debug
Definition Log.hxx:84
@ Stack
Definition Log.hxx:86
@ Warning
Definition Log.hxx:82
Represents the "media-type" element in the RFC 3261 grammar.
Definition Mime.hxx:20
Represents the "name-addr" and the "addr-spec" elements in the RFC 3261 grammar.
Definition NameAddr.hxx:19
bool exists(const Param< NameAddr > &paramType) const
Definition NameAddr.cxx:477
const Data & param(const ExtensionParameter &param) const
Const accessor for non-natively-supported parameter types.
Provides string-parsing functionality with protection from buffer overflow.
CurrentPosition skipToEnd()
CurrentPosition skipWhitespace()
CurrentPosition skipToChar(char c)
CurrentPosition position() const
CurrentPosition skipToOneOf(const char *cs)
CurrentPosition skipChar()
void data(Data &data, const char *start) const
make the passed in data share memory with the buffer (uses Data::Share)
BaseException used to indicate parse failures.
iterator end()
Returns an iterator pointing to the last header field value.
iterator begin()
Returns an iterator pointing to the first header field value.
void push_back(const T &t)
Inserts a header field value at the back of this container.
Abstract interface of a datastore of all publication documents processed by DUM.
std::map< resip::Data, ETagToDocumentMap > KeyToETagMap
virtual KeyToETagMap & getDocuments()=0
virtual bool removeDocument(const Data &eventType, const Data &documentKey, const Data &eTag, uint64_t lastUpdated, bool syncPublication=false)=0
Abstract interface of a datastore of all registered endpoints processed by DUM.
virtual update_status_t updateContact(const Uri &aor, const ContactInstanceRecord &rec)=0
virtual void removeContact(const Uri &aor, const ContactInstanceRecord &rec)=0
virtual void getContacts(const Uri &aor, ContactList &container)=0
virtual void getAors(UriList &container)=0
Security * getSecurity() const
Returns a pointer to the embedded Security object, 0 if not set.
EncodeStream & dump(EncodeStream &strm) const
output current state of the stack - for debug
void clearDnsCache()
Clear the DNS Cache.
void getDnsCacheDump(std::pair< unsigned long, unsigned long > key, GetDnsCacheDumpHandler *handler)
Get a string representation of the DNS Cache.
void reloadCertificates()
reload the TLS X.509 certificates
Definition SipStack.cxx:323
void reloadDnsServers()
Check if DnsServers list has changed, and if so reinitializes the DNS resolver (ares).
void loadOut(Payload &payload) const
static const char * COLON
Definition Symbols.hxx:15
static const char * COMMA
Definition Symbols.hxx:35
static uint64_t getTimeSecs()
Returns the current clock time in seconds.
Definition Timer.hxx:78
Data presentationFormat() const
Wrapper around the inet_top() method.
Definition Tuple.cxx:426
static const Data & toData(TransportType)
Converts the TransportType to a string representation of the transport type, e.g.
Definition Tuple.cxx:927
int getPort() const
Definition Tuple.cxx:466
TransportType getType() const
Definition Tuple.hxx:122
static TransportType toTransport(const Data &)
Converts a string representation of transport type, i.e.
Definition Tuple.cxx:921
Represents the "SIP-URI" and "SIPS-URI" elements in the RFC 3261 grammar.
Definition Uri.hxx:22
CongestionManager * getCongestionManager()
Accessor for the CongestionManager object the stack is using.
This Class is used to implement a primitive form of RPC using loose XML formatting.
dcm! – add UnusedChecking(_enum) below;
static const uint64_t NeverExpire
std::unique_lock< Mutex > Lock
A convenience class to lock a Lockable (such as a Mutex) on object creation, and unlock on destructio...
Definition Lock.hxx:28
int closeSocket(Socket fd)
Definition Socket.cxx:134
TransportType
An enumeration of transport protocols.
IpVersion
An enumeration of IP versions.
std::list< ContactInstanceRecord > ContactList
static const char * faviconLink()
Definition WebAdmin.cxx:82
#define REPRO_BORDERLESS_TABLE_PROPS
Definition WebAdmin.cxx:52
#define REPRO_BORDERED_TABLE_PROPS
Definition WebAdmin.cxx:53
#define REPRO_DEFAULT_COMMAND_PORT
Definition WebAdmin.cxx:60
static const char * standaloneStyle()
Definition WebAdmin.cxx:100
static const char * productMarkSvg()
Definition WebAdmin.cxx:65
unsigned __int64 uint64_t
Definition stdint.h:95
void list()
Definition userAdmin.cxx:66