fixed a few bugs in distant chat: disabled history (for now), improved tunnel handling

This commit is contained in:
csoler 2015-12-04 00:06:14 -05:00
parent b198f1a007
commit 318be3a2ad
4 changed files with 157 additions and 140 deletions

View File

@ -381,7 +381,12 @@ bool p3ChatService::sendChat(ChatId destination, std::string msg)
#endif #endif
RsServer::notify()->notifyChatMessage(message); RsServer::notify()->notifyChatMessage(message);
mHistoryMgr->addMessage(message);
// cyril: history is temporarily diabled for distant chat, since we need to store the full tunnel ID, but then
// at loading time, the ID is not known so that chat window shows 00000000 as a peer.
if(!message.chat_id.isDistantChatId())
mHistoryMgr->addMessage(message);
checkSizeAndSendMessage(ci); checkSizeAndSendMessage(ci);
@ -752,7 +757,13 @@ bool p3ChatService::handleRecvChatMsgItem(RsChatMsgItem *ci)
cm.incoming = true; cm.incoming = true;
cm.online = true; cm.online = true;
RsServer::notify()->notifyChatMessage(cm); RsServer::notify()->notifyChatMessage(cm);
mHistoryMgr->addMessage(cm);
// cyril: history is temporarily diabled for distant chat, since we need to store the full tunnel ID, but then
// at loading time, the ID is not known so that chat window shows 00000000 as a peer.
if(!cm.chat_id.isDistantChatId())
mHistoryMgr->addMessage(cm);
return true ; return true ;
} }

View File

@ -45,7 +45,7 @@
#include "p3gxstunnel.h" #include "p3gxstunnel.h"
#define DEBUG_GXS_TUNNEL //#define DEBUG_GXS_TUNNEL
static const uint32_t GXS_TUNNEL_KEEP_ALIVE_TIMEOUT = 6 ; // send keep alive packet so as to avoid tunnel breaks. static const uint32_t GXS_TUNNEL_KEEP_ALIVE_TIMEOUT = 6 ; // send keep alive packet so as to avoid tunnel breaks.
@ -352,98 +352,109 @@ void p3GxsTunnelService::handleRecvTunnelDataItem(const RsGxsTunnelId& tunnel_id
void p3GxsTunnelService::handleRecvStatusItem(const RsGxsTunnelId& tunnel_id, RsGxsTunnelStatusItem *cs) void p3GxsTunnelService::handleRecvStatusItem(const RsGxsTunnelId& tunnel_id, RsGxsTunnelStatusItem *cs)
{ {
std::vector<uint32_t> notifications ; std::vector<uint32_t> notifications ;
std::set<RsGxsTunnelClientService*> clients ; std::set<RsGxsTunnelClientService*> clients ;
#ifdef DEBUG_GXS_TUNNEL #ifdef DEBUG_GXS_TUNNEL
std::cerr << "p3GxsTunnelService::handleRecvStatusItem(): tunnel_id=" << tunnel_id << " status=" << cs->status << std::endl; std::cerr << "p3GxsTunnelService::handleRecvStatusItem(): tunnel_id=" << tunnel_id << " status=" << cs->status << std::endl;
#endif #endif
switch(cs->status) switch(cs->status)
{ {
case RS_GXS_TUNNEL_FLAG_CLOSING_DISTANT_CONNECTION: case RS_GXS_TUNNEL_FLAG_CLOSING_DISTANT_CONNECTION:
{ {
RS_STACK_MUTEX(mGxsTunnelMtx); /********** STACK LOCKED MTX ******/ RS_STACK_MUTEX(mGxsTunnelMtx); /********** STACK LOCKED MTX ******/
std::map<RsGxsTunnelId,GxsTunnelPeerInfo>::iterator it = _gxs_tunnel_contacts.find(tunnel_id) ; std::map<RsGxsTunnelId,GxsTunnelPeerInfo>::iterator it = _gxs_tunnel_contacts.find(tunnel_id) ;
if(it == _gxs_tunnel_contacts.end()) if(it == _gxs_tunnel_contacts.end())
{ {
std::cerr << "(EE) Cannot mark tunnel connection as closed. No connection openned for tunnel id " << tunnel_id << ". Unexpected situation." << std::endl; std::cerr << "(EE) Cannot mark tunnel connection as closed. No connection openned for tunnel id " << tunnel_id << ". Unexpected situation." << std::endl;
return ; return ;
} }
if(it->second.direction == RsTurtleGenericDataItem::DIRECTION_CLIENT)
{
#ifdef DEBUG_GXS_TUNNEL #ifdef DEBUG_GXS_TUNNEL
std::cerr << " This is server side. Marking distant chat as remotely closed for tunnel id " << tunnel_id << std::endl; std::cerr << " Marking distant chat as remotely closed for tunnel id " << tunnel_id << std::endl;
#endif #endif
it->second.status = RS_GXS_TUNNEL_STATUS_REMOTELY_CLOSED ; if(it->second.direction == RsTurtleGenericDataItem::DIRECTION_CLIENT)
notifications.push_back(RS_GXS_TUNNEL_STATUS_REMOTELY_CLOSED) ; {
} it->second.status = RS_GXS_TUNNEL_STATUS_REMOTELY_CLOSED ;
} // nothing more to do, because the decryption routing will update the last_contact time when decrypting. #ifdef DEBUG_GXS_TUNNEL
break ; std::cerr << " This is server side. The tunnel cannot be re-openned, so we give it up." << std::endl;
#endif
}
else
{
it->second.status = RS_GXS_TUNNEL_STATUS_TUNNEL_DN ;
#ifdef DEBUG_GXS_TUNNEL
std::cerr << " This is client side. The tunnel will be re-openned automatically." << std::endl;
#endif
}
case RS_GXS_TUNNEL_FLAG_KEEP_ALIVE: notifications.push_back(RS_GXS_TUNNEL_STATUS_REMOTELY_CLOSED) ;
} // nothing more to do, because the decryption routing will update the last_contact time when decrypting.
break ;
case RS_GXS_TUNNEL_FLAG_KEEP_ALIVE:
#ifdef DEBUG_GXS_TUNNEL #ifdef DEBUG_GXS_TUNNEL
std::cerr << "GxsTunnelService::handleRecvGxsTunnelStatusItem(): received keep alive packet for inactive tunnel! peerId=" << cs->PeerId() << " tunnel=" << tunnel_id << std::endl; std::cerr << "GxsTunnelService::handleRecvGxsTunnelStatusItem(): received keep alive packet for inactive tunnel! peerId=" << cs->PeerId() << " tunnel=" << tunnel_id << std::endl;
#endif #endif
break ; break ;
case RS_GXS_TUNNEL_FLAG_ACK_DISTANT_CONNECTION: case RS_GXS_TUNNEL_FLAG_ACK_DISTANT_CONNECTION:
{ {
#ifdef DEBUG_GXS_TUNNEL #ifdef DEBUG_GXS_TUNNEL
std::cerr << "Received ACK item from the distant peer!" << std::endl; std::cerr << "Received ACK item from the distant peer!" << std::endl;
#endif #endif
// in this case we notify the clients using this tunnel. // in this case we notify the clients using this tunnel.
notifications.push_back(RS_GXS_TUNNEL_STATUS_CAN_TALK) ; notifications.push_back(RS_GXS_TUNNEL_STATUS_CAN_TALK) ;
} }
break ; break ;
default: default:
std::cerr << "(EE) unhandled tunnel status " << std::hex << cs->status << std::dec << std::endl; std::cerr << "(EE) unhandled tunnel status " << std::hex << cs->status << std::dec << std::endl;
break ; break ;
} }
// notify all clients // notify all clients
#ifdef DEBUG_GXS_TUNNEL #ifdef DEBUG_GXS_TUNNEL
std::cerr << " notifying clients. Prending notifications: " << notifications.size() << std::endl; std::cerr << " notifying clients. Prending notifications: " << notifications.size() << std::endl;
#endif #endif
if(notifications.size() > 0) if(notifications.size() > 0)
{ {
RS_STACK_MUTEX(mGxsTunnelMtx); /********** STACK LOCKED MTX ******/ RS_STACK_MUTEX(mGxsTunnelMtx); /********** STACK LOCKED MTX ******/
std::map<RsGxsTunnelId,GxsTunnelPeerInfo>::iterator it = _gxs_tunnel_contacts.find(tunnel_id) ; std::map<RsGxsTunnelId,GxsTunnelPeerInfo>::iterator it = _gxs_tunnel_contacts.find(tunnel_id) ;
#ifdef DEBUG_GXS_TUNNEL #ifdef DEBUG_GXS_TUNNEL
std::cerr << " " << it->second.client_services.size() << " client services for tunnel id " << tunnel_id << std::endl; std::cerr << " " << it->second.client_services.size() << " client services for tunnel id " << tunnel_id << std::endl;
#endif #endif
for(std::set<uint32_t>::const_iterator it2(it->second.client_services.begin());it2!=it->second.client_services.end();++it2) for(std::set<uint32_t>::const_iterator it2(it->second.client_services.begin());it2!=it->second.client_services.end();++it2)
{ {
std::map<uint32_t,RsGxsTunnelClientService*>::const_iterator it3=mRegisteredServices.find(*it2) ; std::map<uint32_t,RsGxsTunnelClientService*>::const_iterator it3=mRegisteredServices.find(*it2) ;
if(it3 != mRegisteredServices.end()) if(it3 != mRegisteredServices.end())
clients.insert(it3->second) ; clients.insert(it3->second) ;
} }
} }
#ifdef DEBUG_GXS_TUNNEL #ifdef DEBUG_GXS_TUNNEL
std::cerr << " notifying " << clients.size() << " clients." << std::endl; std::cerr << " notifying " << clients.size() << " clients." << std::endl;
#endif #endif
for(std::set<RsGxsTunnelClientService*>::const_iterator it(clients.begin());it!=clients.end();++it) for(std::set<RsGxsTunnelClientService*>::const_iterator it(clients.begin());it!=clients.end();++it)
for(uint32_t i=0;i<notifications.size();++i) for(uint32_t i=0;i<notifications.size();++i)
{ {
(*it)->notifyTunnelStatus(tunnel_id,notifications[i]) ; (*it)->notifyTunnelStatus(tunnel_id,notifications[i]) ;
#ifdef DEBUG_GXS_TUNNEL #ifdef DEBUG_GXS_TUNNEL
std::cerr << " notifying client " << (void*)(*it) << " of status " << notifications[i] << std::endl; std::cerr << " notifying client " << (void*)(*it) << " of status " << notifications[i] << std::endl;
#endif #endif
} }
} }
bool p3GxsTunnelService::handleTunnelRequest(const RsFileHash& hash,const RsPeerId& /*peer_id*/) bool p3GxsTunnelService::handleTunnelRequest(const RsFileHash& hash,const RsPeerId& /*peer_id*/)
@ -1422,87 +1433,87 @@ bool p3GxsTunnelService::getTunnelInfo(const RsGxsTunnelId& tunnel_id,GxsTunnelI
bool p3GxsTunnelService::closeExistingTunnel(const RsGxsTunnelId& tunnel_id, uint32_t service_id) bool p3GxsTunnelService::closeExistingTunnel(const RsGxsTunnelId& tunnel_id, uint32_t service_id)
{ {
// two cases: // two cases:
// - client needs to stop asking for tunnels => remove the hash from the list of tunnelled files // - client needs to stop asking for tunnels => remove the hash from the list of tunnelled files
// - server needs to only close the window and let the tunnel die. But the window should only open // - server needs to only close the window and let the tunnel die. But the window should only open
// if a message arrives. // if a message arrives.
TurtleFileHash hash ; TurtleFileHash hash ;
TurtleVirtualPeerId vpid ; TurtleVirtualPeerId vpid ;
bool close_tunnel = false ; bool close_tunnel = false ;
int direction ; int direction ;
{ {
RsStackMutex stack(mGxsTunnelMtx); /********** STACK LOCKED MTX ******/ RsStackMutex stack(mGxsTunnelMtx); /********** STACK LOCKED MTX ******/
std::map<RsGxsTunnelId,GxsTunnelPeerInfo>::iterator it = _gxs_tunnel_contacts.find(tunnel_id) ; std::map<RsGxsTunnelId,GxsTunnelPeerInfo>::iterator it = _gxs_tunnel_contacts.find(tunnel_id) ;
if(it == _gxs_tunnel_contacts.end()) if(it == _gxs_tunnel_contacts.end())
{ {
std::cerr << "(EE) Cannot close distant tunnel connection. No connection openned for tunnel id " << tunnel_id << std::endl; std::cerr << "(EE) Cannot close distant tunnel connection. No connection openned for tunnel id " << tunnel_id << std::endl;
// We cannot stop tunnels, since their peer id is lost. Anyway, they'll die of starving. // We cannot stop tunnels, since their peer id is lost. Anyway, they'll die of starving.
return false ; return false ;
} }
vpid = it->second.virtual_peer_id ; vpid = it->second.virtual_peer_id ;
std::map<TurtleVirtualPeerId, GxsTunnelDHInfo>::const_iterator it2 = _gxs_tunnel_virtual_peer_ids.find(vpid) ; std::map<TurtleVirtualPeerId, GxsTunnelDHInfo>::const_iterator it2 = _gxs_tunnel_virtual_peer_ids.find(vpid) ;
if(it2 != _gxs_tunnel_virtual_peer_ids.end()) if(it2 != _gxs_tunnel_virtual_peer_ids.end())
hash = it2->second.hash ; hash = it2->second.hash ;
// check how many clients are used. If empty, close the tunnel // check how many clients are used. If empty, close the tunnel
std::set<uint32_t>::iterator it3 = it->second.client_services.find(service_id) ; std::set<uint32_t>::iterator it3 = it->second.client_services.find(service_id) ;
if(it3 == it->second.client_services.end()) if(it3 == it->second.client_services.end())
{ {
std::cerr << "(EE) service id not currently using that tunnel. This is an error." << std::endl; std::cerr << "(EE) service id not currently using that tunnel. This is an error." << std::endl;
return false; return false;
} }
it->second.client_services.erase(it3) ; it->second.client_services.erase(it3) ;
direction = it->second.direction ; direction = it->second.direction ;
if(it->second.client_services.empty()) if(it->second.client_services.empty())
close_tunnel = true ; close_tunnel = true ;
} }
if(close_tunnel && direction == RsTurtleGenericTunnelItem::DIRECTION_SERVER) // nothing more to do for server side. if(close_tunnel)
{ {
// send a status item saying that we're closing the connection // send a status item saying that we're closing the connection
#ifdef DEBUG_GXS_TUNNEL #ifdef DEBUG_GXS_TUNNEL
std::cerr << " Sending a ACK to close the tunnel since we're managing it and it's not used by any service. tunnel id=." << tunnel_id << std::endl; std::cerr << " Sending a ACK to close the tunnel since we're managing it and it's not used by any service. tunnel id=." << tunnel_id << std::endl;
#endif #endif
RsGxsTunnelStatusItem *cs = new RsGxsTunnelStatusItem ; RsGxsTunnelStatusItem *cs = new RsGxsTunnelStatusItem ;
cs->status = RS_GXS_TUNNEL_FLAG_CLOSING_DISTANT_CONNECTION; cs->status = RS_GXS_TUNNEL_FLAG_CLOSING_DISTANT_CONNECTION;
cs->PeerId(RsPeerId(tunnel_id)) ; cs->PeerId(RsPeerId(tunnel_id)) ;
locked_sendEncryptedTunnelData(cs) ; // that needs to be done off-mutex and before we close the tunnel also ignoring failure. locked_sendEncryptedTunnelData(cs) ; // that needs to be done off-mutex and before we close the tunnel also ignoring failure.
if(direction == RsTurtleGenericTunnelItem::DIRECTION_SERVER) // nothing more to do for server side.
{
#ifdef DEBUG_GXS_TUNNEL #ifdef DEBUG_GXS_TUNNEL
std::cerr << " This is client side. Stopping tunnel manageement for tunnel_id " << tunnel_id << std::endl; std::cerr << " This is client side. Stopping tunnel manageement for tunnel_id " << tunnel_id << std::endl;
#endif #endif
mTurtle->stopMonitoringTunnels( hash ) ; // still valid if the hash is null mTurtle->stopMonitoringTunnels( hash ) ; // still valid if the hash is null
} }
if(close_tunnel) RsStackMutex stack(mGxsTunnelMtx); /********** STACK LOCKED MTX ******/
{ std::map<RsGxsTunnelId,GxsTunnelPeerInfo>::iterator it = _gxs_tunnel_contacts.find(tunnel_id) ;
RsStackMutex stack(mGxsTunnelMtx); /********** STACK LOCKED MTX ******/
std::map<RsGxsTunnelId,GxsTunnelPeerInfo>::iterator it = _gxs_tunnel_contacts.find(tunnel_id) ;
if(it == _gxs_tunnel_contacts.end()) // server side. Nothing to do. if(it == _gxs_tunnel_contacts.end()) // server side. Nothing to do.
{ {
std::cerr << "(EE) Cannot close chat associated to tunnel id " << tunnel_id << ": not found." << std::endl; std::cerr << "(EE) Cannot close chat associated to tunnel id " << tunnel_id << ": not found." << std::endl;
return false ; return false ;
} }
_gxs_tunnel_contacts.erase(it) ; _gxs_tunnel_contacts.erase(it) ;
// GxsTunnelService::removeVirtualPeerId() will be called by the turtle service. // GxsTunnelService::removeVirtualPeerId() will be called by the turtle service.
} }
return true ; return true ;
} }
bool p3GxsTunnelService::getTunnelsInfo(std::vector<RsGxsTunnelService::GxsTunnelInfo> &infos) bool p3GxsTunnelService::getTunnelsInfo(std::vector<RsGxsTunnelService::GxsTunnelInfo> &infos)

View File

@ -28,10 +28,14 @@
// Generic tunnel service // Generic tunnel service
// //
// Preconditions: // Preconditions:
// * multiple services can use the same tunnel // * the secured tunnel service takes care of:
// * tunnels are automatically encrypted and ensure transport (items stored in a queue until ACKed by the other side) // - tunnel health: tunnels are kept alive using special items, re-openned when necessary, etc.
// - transport: items are ACK-ed and re-sent if never received
// - encryption: items are all encrypted and authenticated using PFS(DH)+HMAC(sha1)+AES(128)
// * each tunnel is associated to a specific GXS id on both sides. Consequently, services that request tunnels from different IDs to a // * each tunnel is associated to a specific GXS id on both sides. Consequently, services that request tunnels from different IDs to a
// server for the same GXS id need to be handled correctly. // server for the same GXS id need to be handled correctly.
// * client services must register to the secured tunnel service if they want to use it.
// * multiple services can use the same tunnel. Items contain a service Id that is obtained when registering to the secured tunnel service.
// //
// GUI // GUI
// * the GUI should show for each tunnel: // * the GUI should show for each tunnel:
@ -41,7 +45,7 @@
// - number of pending items (and total size) // - number of pending items (and total size)
// - number ACKed items both ways. // - number ACKed items both ways.
// //
// we can use an additional tab "Authenticated tunnels" in the statistics->turtle window // We can use an additional tab "Authenticated tunnels" in the statistics->turtle window for that purpose.
// //
// Interaction with services: // Interaction with services:
// //
@ -52,14 +56,20 @@
// Data is send to a service ID (could be any existing service ID). The endpoint of the tunnel must register each service, in order to // Data is send to a service ID (could be any existing service ID). The endpoint of the tunnel must register each service, in order to
// allow the data to be transmitted/sent from/to that service. Otherwise an error is issued. // allow the data to be transmitted/sent from/to that service. Otherwise an error is issued.
// //
// Encryption
// * the whole tunnel traffic is encrypted using AES-128 with random IV
// * a random key is established using DH key exchange for each connection (establishment of a new virtual peer)
// * encrypted items are authenticated with HMAC(sha1).
// * DH public keys are the only chunks of data that travel un-encrypted along the tunnel. They are
// signed to avoid any MITM interactions. No time-stamp is used in DH exchange since a replay attack would not work.
//
// Algorithms // Algorithms
// //
// Tunnel establishment
// * we need two layers: the turtle layer, and the GXS id layer. // * we need two layers: the turtle layer, and the GXS id layer.
// - for each pair of GXS ids talking, a single turtle tunnel is used // - for each pair of GXS ids talking, a single turtle tunnel is used
// - that tunnel can be shared by multiple services using it. // - that tunnel can be shared by multiple services using it.
// - services are responsoble for asking tunnels and also droppping them when unused. // - services are responsoble for asking tunnels and also droppping them when unused.
// - at the turtle layer, the tunnel will be closed only when no service uses it. // - at the turtle layer, the tunnel will be effectively closed only when no service uses it.
// * IDs // * IDs
// TurtleVirtualPeerId: // TurtleVirtualPeerId:
// - Used by tunnel service for each turtle tunnel // - Used by tunnel service for each turtle tunnel
@ -72,25 +82,18 @@
// - accept virtual peers from turtle tunnel service. The hash for that VP only depends on the server GXS id at server side, which is our // - accept virtual peers from turtle tunnel service. The hash for that VP only depends on the server GXS id at server side, which is our
// own ID at server side, and destination ID at client side. What happens if two different clients request to talk to the same GXS id? (same hash) // own ID at server side, and destination ID at client side. What happens if two different clients request to talk to the same GXS id? (same hash)
// They should use different virtual peers, so it should be ok. // They should use different virtual peers, so it should be ok.
// - multiple tunnels may end up to the same hash, but will correspond to different GXS tunnels since the GXS id in the other side is different.
// //
// Turtle hash: [ 0 ---------------15 16---19 ] // Turtle hash: [ 0 ---------------15 16---19 ]
// Destination Source // Destination Random
// //
// We Use 16 bytes to target the exact destination of the hash. The source part is just 4 arbitrary bytes that need to be different for all source // We Use 16 bytes to target the exact destination of the hash. The source part is just 4 arbitrary bytes that need to be different for all source
// IDs that come from the same peer, which is quite likely to be sufficient. The real source of the tunnel will make itself known when sending the // IDs that come from the same peer, which is quite likely to be sufficient. The real source of the tunnel will make itself known when sending the
// DH key. // DH key.
// //
// Another option is to use random bytes in 16-19. But then, we would digg multiple times different tunnels between the same two peers even when requesting
// a GXS tunnel for the same pair of GXS ids. Is that a problem?
// - that solves the problem of colliding source GXS ids (since 4 bytes is too small)
// - the DH will make it clear that we're talking to the same person if it already exist.
//
// * at the GXS layer // * at the GXS layer
// - we should be able to have as many tunnels as they are different couples of GXS ids to interact. That means the tunnel should be determined // - we should be able to have as many tunnels as they are different couples of GXS ids to interact. That means the tunnel should be determined
// by a mix between our own GXS id and the GXS id we're talking to. That is what the TunnelVirtualPeer is. // by a mix between our own GXS id and the GXS id we're talking to. That is what the TunnelVirtualPeer is.
// //
// //
// RequestTunnel(source_own_id,destination_id) - // RequestTunnel(source_own_id,destination_id) -
// | | // | |
@ -108,14 +111,6 @@
// | | // | |
// +---------------- notify client service that Peer(destination_id, tunnel_hash) is ready to talk to | // +---------------- notify client service that Peer(destination_id, tunnel_hash) is ready to talk to |
// - // -
// Notes
// * one other option would be to make the turtle hash depend on both GXS ids in a way that it is possible to find which are the two ids on the server side.
// but that would prevent the use of unknown IDs, which we would like to offer as well.
// Without this, it's not possible to request two tunnels to a same server GXS id but from a different client GXS id. Indeed, if the two hashes are the same,
// from the same peer, the tunnel names will be identical and so will be the virtual peer ids, if the route is the same (because of multi-tunneling, they
// will be different if the route is different).
//
// *
#include <turtle/turtleclientservice.h> #include <turtle/turtleclientservice.h>
#include <retroshare/rsgxstunnel.h> #include <retroshare/rsgxstunnel.h>

View File

@ -195,8 +195,8 @@ TurtleRouterStatistics::TurtleRouterStatistics(QWidget *parent)
_tunnel_statistics_F->setFrameStyle(QFrame::NoFrame); _tunnel_statistics_F->setFrameStyle(QFrame::NoFrame);
_tunnel_statistics_F->setFocusPolicy(Qt::NoFocus); _tunnel_statistics_F->setFocusPolicy(Qt::NoFocus);
routertabWidget->addTab(new TurtleRouterDialog(),QString(tr("Tunnels"))); routertabWidget->addTab(new TurtleRouterDialog(),QString(tr("Anonymous tunnels")));
routertabWidget->addTab(new GxsTunnelsDialog(),QString(tr("Authenticated pipes"))); routertabWidget->addTab(new GxsTunnelsDialog(),QString(tr("Authenticated tunnels")));
float fontHeight = QFontMetricsF(font()).height(); float fontHeight = QFontMetricsF(font()).height();
float fact = fontHeight/14.0; float fact = fontHeight/14.0;