859 lines
38 KiB
Plaintext
859 lines
38 KiB
Plaintext
<cfcomponent extends="taffy.core.api">
|
||
<!---<cfset this.name = "deck_api" />--->
|
||
<cfset this.mappings["/resources"] = expandPath("./resources")/>
|
||
<cfset this.mappings["/taffy"] = expandPath("./taffy")/>
|
||
<cfset this.mappings["/lib"] = expandPath("./lib")/>
|
||
|
||
<cflock scope="application" type="exclusive" timeout="3"><!---*** не уверен, что это необходимо--->
|
||
<cftry>
|
||
<!---production environment--->
|
||
<cfinclude template="../../conf/prod.cfm"/>
|
||
<cfcatch type="ANY">
|
||
<cftry>
|
||
<!---staging environment--->
|
||
<cfinclude template="../../conf/stage.cfm"/>
|
||
<cfcatch type="ANY">
|
||
<cftry>
|
||
<!--- development environment --->
|
||
<cfinclude template="../../conf/dev.cfm"/>
|
||
<cfcatch type="ANY">
|
||
<!---configure right here--->
|
||
<!---для ясности--->
|
||
<cfset this.config.environment = "dev-default" />
|
||
<cfset this.config.datasource = "cmdb" />
|
||
<cfset this.config.is_dev_default = "true" /><!---это виднее в сообщении об ошибке--->
|
||
</cfcatch>
|
||
</cftry>
|
||
|
||
</cfcatch>
|
||
</cftry>
|
||
</cfcatch>
|
||
</cftry>
|
||
|
||
<cfset this.datasource = this.config.datasource />
|
||
<cfset request.config = this.config />
|
||
</cflock>
|
||
|
||
<cfscript>
|
||
this.name = hash(getCurrentTemplatePath());
|
||
//variables.framework = structNew(); //
|
||
variables.framework.debugKey = "debug";
|
||
variables.framework.reloadKey = "reload";
|
||
variables.framework.reloadPassword = "true";
|
||
variables.framework.reloadOnEveryRequest = true;
|
||
variables.framework.serializer = "taffy.core.nativeJsonSerializer";
|
||
//variables.framework.serializer = "JsonUtilSerializer";
|
||
variables.framework.dashboardKey = "dashboard";
|
||
variables.framework.disableDashboard = false;
|
||
variables.framework.unhandledPaths = "/flex2gateway";
|
||
variables.framework.allowCrossDomain = true; // соответствующие заголовки, включая Access-Control-Allow-Origin выставляются только при наличии заголовка origin в реквесте
|
||
//variables.framework.allowCrossDomain = ""; // перепишем явно, возвращая Origin
|
||
|
||
//variables.framework.docs={};
|
||
variables.framework.docs.APIName="Deck API";
|
||
variables.framework.docs.APIVersion="0.111"; /*$ git config --global --unset user.password*/
|
||
|
||
|
||
variables.framework.globalHeaders = structNew();
|
||
variables.framework.globalHeaders["Access-Control-Expose-Headers"] = "Location";
|
||
variables.framework.globalHeaders["Access-Control-Allow-Credentials"] = "true";
|
||
variables.framework.globalHeaders["X-Application-Version"] = variables.framework.docs.APIVersion;
|
||
//variables.framework.globalHeaders["Access-Control-Allow-Origin"] = "*";
|
||
|
||
request.UNDEFINED_USR_ID=-1;
|
||
request.ANONYMOUS_USR_ID=2;
|
||
request.GUEST_USR_ID=3
|
||
|
||
////////////////////////////////////////////////////////
|
||
request.ORCHESTRATOR_AUTH = createObject("java", "java.lang.System").getEnv("ORCHESTRATOR_AUTH");
|
||
if (isNull(request.ORCHESTRATOR_AUTH)) {
|
||
request.ORCHESTRATOR_AUTH = "Basic c3ZjX2xrdGVzdDoxMTgzZDIyNzc1OWExYjFkZmVjM2M3NTFiMzIyNzdiYTQ3";//*** времянка, дубль из deck/Application.cfm
|
||
}
|
||
//*** почему-то scope this ведет себя совершенно не так, как request и variables
|
||
// в нем, когда переменная получает значение Empty:null, можно выполнить writedump, а в других получаем ошибку, что такой переменной нет
|
||
request.locateIamService=locateIamService; //for possible debug
|
||
//request.IAM_SERVICE_URL=this.iamServiceUrl;
|
||
</cfscript>
|
||
|
||
|
||
<cffunction name="rethrow" returntype="void">
|
||
<!--- https://www.raymondcamden.com/2004/03/09/3089633C-9FA0-606B-3F540AE9642A795F --->
|
||
<cftry>
|
||
<cfcatch>
|
||
<cfrethrow/>
|
||
</cfcatch>
|
||
</cftry>
|
||
<cfthrow type="Context validation error" message="RETHROW() called outside TRY-CATCH"/>
|
||
</cffunction>
|
||
|
||
<cffunction name="castToBool" returntype="any">
|
||
<!--- приводит поле резалтсета к булевому типу, пригодному для сериализации в json --->
|
||
<cfargument name="x"/>
|
||
<cfif isNull(arguments.x) OR isEmpty(arguments.x)>
|
||
<cfreturn arguments.x/><!--- при сериализации преобразуется в null --->
|
||
<cfelse>
|
||
<cfreturn (arguments.x NEQ 0)/><!--- при сериализации дает false/true --->
|
||
</cfif>
|
||
</cffunction>
|
||
<cfset request.castToBool=#castToBool#/>
|
||
|
||
<cffunction name="castToBoolSimple" returntype="any">
|
||
<!---
|
||
приводит поле резалтсета к булевому типу, пригодному для сериализации в json,
|
||
вместо null возвращает false
|
||
--->
|
||
<cfargument name="x"/>
|
||
<cfreturn (arguments.x NEQ 0)/><!--- при сериализации дает false/true --->
|
||
</cffunction>
|
||
<cfset request.castToBoolSimple=#castToBoolSimple#/>
|
||
|
||
<!--- почему-то эта функция недоступна onApplicationStart --->
|
||
<!--- <cffunction
|
||
name="getStand"
|
||
access="private"
|
||
returntype="string"
|
||
output="true">
|
||
|
||
<cftry>
|
||
<cfquery name=qConfig>
|
||
select value as stand from config
|
||
where name='STAND'
|
||
</cfquery>
|
||
<cfreturn qConfig.stand/>
|
||
|
||
<cfcatch type="ANY">
|
||
|
||
</cfcatch>
|
||
</cftry>
|
||
<cfreturn ""/>
|
||
</cffunction> --->
|
||
|
||
|
||
<cffunction name="onApplicationStart">
|
||
<!--- <cfset application.dbUser = "username" />
|
||
<cfset application.dbPass = "password" />--->
|
||
<!---<cfset application._taffy.settings.allowCrossDomain = true/>--->
|
||
<cfset this.iamServiceUrl=locateIamService()/> <!--- ***при недоступности будет падать все --->
|
||
<cfreturn super.onApplicationStart() />
|
||
</cffunction>
|
||
|
||
|
||
|
||
<cffunction name="locateIamService"><!--- *** архитектурный колхоз --->
|
||
<!--- почему-то мы не видим других функций, когда нас вызывают onApplicationStart. С другой стороны, а почему вообще должны вызывать без перезагрузки приложения --->
|
||
<cfset var stand=""/>
|
||
<cftry>
|
||
<cfquery name=qConfig>
|
||
select value as stand from config
|
||
where name='STAND'
|
||
</cfquery>
|
||
<cfset stand = qConfig.stand/>
|
||
|
||
<cfswitch expression=#stand#>
|
||
<cfcase value=",dev">
|
||
<cfreturn "https://auth-api-dev.ngcloud.ru/api/v1/user"/>
|
||
</cfcase>
|
||
<cfcase value="test">
|
||
<cfreturn "https://auth-api-test.ngcloud.ru/api/v1/user"/>
|
||
</cfcase>
|
||
<cfcase value="prod">
|
||
<cfreturn "https://auth-api.ngcloud.ru/api/v1/user"/>
|
||
</cfcase>
|
||
<cfdefaultcase><!--- proceed to the legacy locator ---></cfdefaultcase>
|
||
</cfswitch>
|
||
|
||
<cfcatch type="ANY">
|
||
<!--- do nothing, proceed to the legacy locator --->
|
||
</cfcatch>
|
||
</cftry>
|
||
|
||
<!--- вот если ничего не прописано, попробуем по-старинке --->
|
||
<cfloop list="https://auth-api-dev.ngcloud.ru/api/v1/user,https://auth-api-test.ngcloud.ru/api/v1/user,https://auth-api.ngcloud.ru/api/v1/user" index="serviceUrl"><!--- *** начиная с девелоперского --->
|
||
<cftry>
|
||
<cfhttp url=#serviceUrl# method="OPTIONS" result="resp" timeout=1/>
|
||
|
||
<cfreturn serviceUrl/>
|
||
<cfcatch type="any">
|
||
<!--- <cfdump var=#serviceUrl#/>
|
||
<cfdump var=#cfcatch#/><cfabort/> --->
|
||
</cfcatch>
|
||
</cftry>
|
||
</cfloop>
|
||
<cfthrow message="IAM service unavailable" detail="cannot find IAM"/>
|
||
</cffunction>
|
||
|
||
<!--- <cffunction name="onRequest">
|
||
<cfargument name="template" type="string" required="true"/>
|
||
<cfset request.startTickCount=getTickCount()/>
|
||
<!---<cfheader name="Access-Control-Allow-Origin" value="*"/>--->
|
||
<cfreturn super.onRequest(template) />
|
||
</cffunction> --->
|
||
|
||
|
||
|
||
<cfscript>
|
||
|
||
function onTaffyRequest(verb, cfc, requestArguments, mimeExt, headers){
|
||
/* https://docs.taffy.io/#/3.5.0 */
|
||
/*
|
||
//allow white-listed requests through
|
||
|
||
if (cfc == "login"){
|
||
return true;
|
||
}
|
||
|
||
//otherwise require a device token
|
||
if (!structKeyExists(requestArguments, "deviceToken")){
|
||
return newRepresentation().noData().withStatus(401, "Authentication Required");
|
||
|
||
//and make sure it's valid
|
||
}else if (!validateToken(requestArguments.deviceToken)){
|
||
|
||
return newRepresentation().noData().withStatus(403, "Not Authorized");
|
||
}
|
||
|
||
//return representationOf(requestArguments);
|
||
//if a token is included, and valid, allow the request to continue
|
||
return true;
|
||
*/
|
||
////////////////////////////////////////////////
|
||
// самым грубым образом получаем данные от IDP, не обрабатывая исключения
|
||
|
||
|
||
//if (variables.framework.allowCrossDomain EQ "") {
|
||
//corsHeaders(); // *** нужно, чтобы variables.framework.allowCrossDomain="" иначе задвоятся заголовки
|
||
//}
|
||
|
||
//if (UCase(arguments.verb) EQ 'OPTIONS') return newRepresentation().noData().withStatus("204","No Data").withHeaders({"Content-Type":"application/json;charset=utf-8"});
|
||
if (uCase(arguments.verb) EQ 'OPTIONS') return true;
|
||
if (lCase(arguments.cfc) EQ 'err') return true;
|
||
if (lCase(arguments.cfc) EQ 'throw') return true;
|
||
|
||
var auth="";
|
||
if (structKeyExists(headers,"Authorization")) {
|
||
auth=headers.Authorization;
|
||
} else {
|
||
return representationOf("Authorization header expected").withStatus(401);
|
||
}
|
||
var result="";
|
||
try {
|
||
var httpService = new http(method = "GET", charset = "utf-8", url = #this.iamServiceUrl#, timeout="5");
|
||
httpService.addParam(type = "HEADER", name = "Accept", value = "application/json");
|
||
httpService.addParam(type = "HEADER", name = "Authorization", value = "#auth#"); //passthrough
|
||
//writedump(this.iamServiceUrl);abort;
|
||
var resp = httpService.send();
|
||
//if (resp.status_code NEQ 200) throw("IDP response not OK");
|
||
//writedump(resp);abort;
|
||
var prefix = resp.getPrefix();
|
||
if (prefix.status_code NEQ 200) {
|
||
//writedump(this.iamServiceUrl);
|
||
//writedump(resp);
|
||
return representationOf( {"iamServiceUrl"=this.iamServiceUrl, "idpResponse"=resp} ).withStatus(500);
|
||
//abort;
|
||
//throw("IDP response not OK");
|
||
}
|
||
result = prefix.filecontent;
|
||
//writedump(result);abort;
|
||
} catch (e) {
|
||
/*if (fileExists("#GetDirectoryFromPath(GetCurrentTemplatePath())#/etc/local-debug")) { //true for local debug without IDP, etc/* does not go to repository
|
||
var result='{"accounts":[{"login":"","type":"telegram"}],"avatar":["d2d2b7ac-50af-432b-b7c4-f7d6561e288a"],"company":"ООО «НУБЕС»","companyId":"8ec70ac0-546d-42a7-8cff-339c8fb51a23","contactId":"983967a3-58c2-4cdd-84a5-8b427ccfac82","email":"smishchuk@nubes.ru","externalUser":false,"fio":{"fullName":"Мищук Сергей ","name":"Сергей","secondName":"","surname":"Мищук"},"groupIds":["94bf5be7-52f4-5c13-87c3-513786934685","ab12b6b8-0265-4683-a5c2-5e1a74a55216","aff008c3-7443-50f6-86e1-ecff3cd3b04d","d6000da0-c9aa-55eb-9882-f118b432730b","d89a33dc-3177-5854-9580-f7f860a5ab7c","ddfe2555-9ef4-42cb-9a2b-7f24e61e7747","df12926f-ecb8-5479-857f-6d291464baad","fda5c295-230a-5025-9797-b8b4e99e08aa","2be9b4b6-94d1-59f9-a649-cab228d82169","64fe6f5c-a91b-5fea-a7ca-d9823701ebd4"],"integration":{"serviceId":""},"login":"smishchuk@nubes.ru","mobilePhone":[],"position":"","userId":"d72530e1-66a4-412f-a046-38153c4e5405"}';}
|
||
*/
|
||
//writedump(resp);
|
||
//return representationOf( {"exception"=e} );
|
||
return representationOf( {"exception"=e, "idpResponse"=result} ).withStatus(200);
|
||
}
|
||
|
||
//writeDump(result); abort;
|
||
try {
|
||
var idpUserData=deserializeJson(result);
|
||
arguments.requestArguments.companyUid=idpUserData.companyId;//GUID!
|
||
arguments.requestArguments.usrUid=idpUserData.contactId;//userId; //GUID!
|
||
} catch (e) {
|
||
return representationOf( {"exception"=e, "idpResponse"=result} );
|
||
}
|
||
|
||
var usrCustomerInfo=getUsrCustomerInfo(idpUserData.contactId);
|
||
|
||
if (lCase(arguments.cfc) EQ 'user') {
|
||
if (structIsEmpty(usrCustomerInfo)) return representationOf("User information not found").withStatus(404);
|
||
}
|
||
|
||
if (lCase(arguments.cfc) EQ 'notification_ls') { //*** костыль: для нотификаций можно адресоваться к пользователю без контракта
|
||
if (structIsEmpty(usrCustomerInfo)) {
|
||
arguments.requestArguments.usrId=-1; //Integer!
|
||
arguments.requestArguments.contragentId=-1; //Integer
|
||
arguments.requestArguments.contractId=-1; //Integer
|
||
arguments.requestArguments.specificationId=-1; //Integer
|
||
|
||
return true;
|
||
}
|
||
}
|
||
|
||
if (structIsEmpty(usrCustomerInfo)) return representationOf("Cannot find default specification for current user #result#").withStatus(422); //это создает довольно много лишних движений при отладке
|
||
|
||
arguments.requestArguments.usrId=usrCustomerInfo.usrId; //Integer!
|
||
arguments.requestArguments.contragentId=usrCustomerInfo.contragentId; //Integer
|
||
arguments.requestArguments.contractId=usrCustomerInfo.contractId; //Integer
|
||
arguments.requestArguments.specificationId=usrCustomerInfo.specificationId; //Integer
|
||
|
||
return true;
|
||
|
||
|
||
////////////////////////////////////////////////////////////////////////////
|
||
//ensure https because of basic authentication
|
||
if (!lcase(request.config.environment) == "development"){
|
||
if (!cgi.server_port_secure) { return newRepresentation().noData().withStatus("403.4", "SSL Required");}
|
||
}
|
||
|
||
request.usr_id=request.UNDEFINED_USR_ID;
|
||
|
||
if (checkAuth()) {return true;}
|
||
|
||
//return representationOf(arguments.headers);
|
||
//return representationOf(arguments.cfc);
|
||
var authToken="";
|
||
if (structKeyExists(arguments.headers,"x-auth-token")) {authToken=structFind(arguments.headers,"x-auth-token");}
|
||
var issuedFor="";
|
||
if (structKeyExists(arguments.headers,"x-issued-for")) {issuedFor=structFind(arguments.headers,"x-issued-for");}
|
||
if (validatePin(authToken,issuedFor, arguments.verb, arguments.cfc)) return true; //side effect: sets request.usr_id!
|
||
|
||
if (!validateCredentials(getBasicAuthCredentials())) {
|
||
return newRepresentation().noData().withStatus("401", "Invalid Credentials").withHeaders({"WWW-Authenticate":"Basic realm=mms-api"});
|
||
}
|
||
return true;
|
||
}
|
||
|
||
//private function validateToken(token){ return false; }
|
||
|
||
//refactoring required
|
||
private function validateCredentials(credentials) {//return true;
|
||
if (arguments.credentials.username == "medved" /*AND credentials.password == "ghtdtlrhjcfdxtu!Y"*/) {
|
||
request.usr_id=-3;
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
</cfscript>
|
||
|
||
<!--- стоит ли сообщать отдельно об экспирации пина
|
||
нужно блокировать перебор, можно делать пару pin-issuedFor
|
||
вероятно, лучше сделать токен девайса
|
||
--->
|
||
|
||
|
||
|
||
<cffunction name="checkAuth">
|
||
<!---https://www.sjoerdlangkemper.nl/2018/09/12/authorization-header-and-cors/--->
|
||
<cfset var jwtHelper=CreateObject("component","lib.jwt").init()/>
|
||
<cfset var headers=#GetHttpRequestData().headers#/>
|
||
<cftry>
|
||
<cfset var rawToken=right(headers.Authorization,len(headers.Authorization)-len('bearer '))/>
|
||
<cfset var token=jwtHelper.decode(token=rawToken, key=request.config.IDP_certificate.keys[1], algorithms='RS256')/>
|
||
|
||
<cfset var login=token.preferred_username/>
|
||
<!---отрезаем первичный домен --->
|
||
<cfset login=ReplaceNoCase(login,"#request.config.auth_domain_suffix#","")/>
|
||
<cfquery name="local.qUsr">
|
||
select usr_id from usr where login=<cfqueryparam cfsqltype="cf_sql_varchar" value="#login#"/>
|
||
</cfquery>
|
||
|
||
<cfif #local.qUsr.recordCount# EQ 0>
|
||
<cfheader statuscode="401" statustext="User not found in local database"/>
|
||
<cfreturn false/>
|
||
</cfif>
|
||
|
||
<cfset request.usr_id=local.qUsr.usr_id/>
|
||
|
||
<cfcatch type="database">
|
||
<cfheader statuscode="500" statustext="Internal Server Error - Database Error"/>
|
||
<cfoutput>#cfcatch.message# : #cfcatch.detail#</cfoutput>
|
||
<cfreturn false/>
|
||
</cfcatch>
|
||
|
||
<cfcatch type="ANY">
|
||
<cfif !GetHttpRequestData().method EQ "OPTIONS">
|
||
<cfoutput>#cfcatch.message# : #cfcatch.detail#</cfoutput>
|
||
<cfreturn false/>
|
||
</cfif>
|
||
</cfcatch>
|
||
</cftry>
|
||
|
||
<cfreturn true/>
|
||
</cffunction>
|
||
|
||
|
||
<cffunction name="getUsrCustomerInfo">
|
||
<cfargument name="usrUid"/>
|
||
|
||
<!--- глупейшее определение спецификации по умолчанию (надо переделать, чтобы создавалась, или вообще просто записывать факты без спеки). Но помним, что при постановке на тестирование цены еше не фиксированы --->
|
||
<!--- заметим, что у нас контрагент может быть определен параллельно, от IDP --->
|
||
<cfset local={}/>
|
||
|
||
<!--- Во избежании потери времени при отладке временно сделаем создание дефолтового контракта и спецификации, если их нет --->
|
||
<!--- *** хватается первый попавшийся контракт и спецификация --->
|
||
<cfquery name="local.qGetCustomerInfo">
|
||
select u.usr_id, c.contragent_id, c.contract_id, s.specification_id
|
||
from usr u
|
||
join contract c on (u.contragent_id=c.contragent_id)
|
||
join specification s on (c.contract_id=s.contract_id)
|
||
where idp_usr_uid=<cfqueryparam cfsqltype="cf_sql_other" value="#arguments.usrUid#" null=#!isValid('guid',arguments.usrUid)#/>
|
||
order by specification_id desc limit 1;
|
||
</cfquery>
|
||
<!--- <cfdump var=#local.qGetCustomerInfo#/><cfabort/> --->
|
||
<cfif local.qGetCustomerInfo.recordCount GT 0>
|
||
<cfreturn {"usrId"=#local.qGetCustomerInfo.usr_id#,
|
||
"contragentId"=#local.qGetCustomerInfo.specification_id#,
|
||
"contractId"=#local.qGetCustomerInfo.specification_id#,
|
||
"specificationId"=#local.qGetCustomerInfo.specification_id#
|
||
}/>
|
||
<cfelse>
|
||
<cfreturn {}/> <!--- неуклюжий способ обозначить ошибку --->
|
||
</cfif>
|
||
</cffunction>
|
||
|
||
|
||
<cffunction name="corsHeaders">
|
||
<!--- фрагмент взят из taffy/core/api.cfc и немного переписан --->
|
||
<cfset var _taffyRequest=request._taffyRequest/>
|
||
<cfset local={}/>
|
||
|
||
<cfset local.allowVerbs = uCase(structKeyList(_taffyRequest.matchDetails.methods)) />
|
||
<cfif true
|
||
AND listFindNoCase('PUT,PATCH,DELETE,OPTIONS',_taffyRequest.verb)
|
||
AND NOT listFind(local.allowVerbs,'OPTIONS')>
|
||
<cfset local.allowVerbs = listAppend(local.allowVerbs,'OPTIONS') />
|
||
</cfif>
|
||
<cfif structKeyExists(_taffyRequest.headers, "origin") >
|
||
|
||
<cfheader name="Access-Control-Allow-Origin" value="#_taffyRequest.headers.origin#" />
|
||
|
||
<cfheader name="Access-Control-Allow-Methods" value="#local.allowVerbs#" />
|
||
<!--- Why do we parrot back these headers? See: https://github.com/atuttle/Taffy/issues/144 --->
|
||
<cfif not structKeyExists(_taffyRequest.headers, "Access-Control-Request-Headers")>
|
||
<cfheader name="Access-Control-Allow-Headers" value="Origin, Authorization, X-CSRF-Token, X-Requested-With, Content-Type, X-HTTP-Method-Override, Accept, Referrer, User-Agent" />
|
||
<cfelse>
|
||
<!--- parrot back all of the request headers to allow the request to continue (can we improve on this?) --->
|
||
<cfset local.allowedHeaders = {} />
|
||
<cfloop list="Origin,Authorization,X-CSRF-Token,X-Requested-With,Content-Type,X-HTTP-Method-Override,Accept,Referrer,User-Agent" index="local.h">
|
||
<cfset local.allowedHeaders[local.h] = 1 />
|
||
</cfloop>
|
||
<cfset local.requestedHeaders = _taffyRequest.headers['Access-Control-Request-Headers'] />
|
||
<cfloop list="#local.requestedHeaders#" index="local.i">
|
||
<cfset local.allowedHeaders[ local.i ] = 1 />
|
||
</cfloop>
|
||
<cfheader name="Access-Control-Allow-Headers" value="#structKeyList(local.allowedHeaders)#" />
|
||
</cfif>
|
||
</cfif>
|
||
</cffunction>
|
||
|
||
<!--- :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: --->
|
||
<!--- :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: --->
|
||
|
||
<!--- Overriding TAFFY short-circuit logic (taffy/core/api.cfc) --->
|
||
<cffunction name="onRequest" output="true" returntype="boolean">
|
||
<cfargument name="targetPage" type="string" required="true" />
|
||
|
||
<cfset request.startTickCount=getTickCount()/><!--- *** --->
|
||
|
||
<cfset var _taffyRequest = {} />
|
||
<cfset var local = {} />
|
||
<cfset var m = '' />
|
||
<cfset request._taffyRequest = _taffyRequest />
|
||
<cfset local.debug = false />
|
||
|
||
<cfset _taffyRequest.metrics = {} />
|
||
<cfset m = _taffyRequest.metrics />
|
||
<cfset m.init = getTickCount() />
|
||
|
||
<!--- enable/disable debug output per settings --->
|
||
<cfif not structKeyExists(url, application._taffy.settings.debugKey)>
|
||
<cfsetting showdebugoutput="false" />
|
||
</cfif>
|
||
|
||
<!--- display api dashboard if requested --->
|
||
<cfif
|
||
NOT structKeyExists(url,application._taffy.settings.endpointURLParam)
|
||
AND NOT structKeyExists(form,application._taffy.settings.endpointURLParam)
|
||
AND len(cgi.path_info) lte 1
|
||
AND listFindNoCase(cgi.script_name, "index.cfm", "/") EQ listLen(cgi.script_name, "/")>
|
||
<cfif NOT application._taffy.settings.disableDashboard>
|
||
<cfif StructKeyExists( URL, "docs" )>
|
||
<cfinclude template="#application._taffy.settings.docsPath#" />
|
||
<cfelse>
|
||
<cfinclude template="../dashboard/dashboard.cfm" />
|
||
</cfif>
|
||
<cfabort />
|
||
<cfelse>
|
||
<cfif len(application._taffy.settings.disabledDashboardRedirect)>
|
||
<cflocation url="#application._taffy.settings.disabledDashboardRedirect#" addtoken="false" />
|
||
<cfabort />
|
||
<cfelseif application._taffy.settings.showDocsWhenDashboardDisabled>
|
||
<cfinclude template="#application._taffy.settings.docsPath#" />
|
||
<cfabort />
|
||
<cfelse>
|
||
<cfset throwError(403, "Forbidden") />
|
||
</cfif>
|
||
</cfif>
|
||
</cfif>
|
||
|
||
<!--- get request details --->
|
||
<cfset m.beforeParse = getTickCount() />
|
||
<cfset local.parsed = parseRequest() />
|
||
<cfset m.afterParse = getTickCount() />
|
||
<cfset structAppend(_taffyRequest, local.parsed) />
|
||
<cfset m.parseTime = m.afterParse - m.beforeParse />
|
||
|
||
<!--- CORS headers (so that CORS can pass even if the resource throws an exception) --->
|
||
<cfset local.allowVerbs = uCase(structKeyList(_taffyRequest.matchDetails.methods)) />
|
||
<cfif (application._taffy.settings.allowCrossDomain eq true or len(application._taffy.settings.allowCrossDomain) gt 0)
|
||
AND listFindNoCase('PUT,PATCH,DELETE,OPTIONS',_taffyRequest.verb)
|
||
AND NOT listFind(local.allowVerbs,'OPTIONS')>
|
||
<cfset local.allowVerbs = listAppend(local.allowVerbs,'OPTIONS') />
|
||
</cfif>
|
||
<cfif structKeyExists(_taffyRequest.headers, "origin") AND (application._taffy.settings.allowCrossDomain eq true or len(application._taffy.settings.allowCrossDomain) gt 0)>
|
||
<cfif application._taffy.settings.allowCrossDomain eq true>
|
||
<!--- <cfheader name="Access-Control-Allow-Origin" value="*" /> ---><!--- *** dirty hack 2024-10-23 17:59:43--->
|
||
<cfheader name="Access-Control-Allow-Origin" value="#_taffyRequest.headers.origin#" />
|
||
<cfelse>
|
||
<!---
|
||
The Access-Control-Allow-Origin header can only have 1 value so we check to see if the Origin header is
|
||
in the list of origins specified in the config setting and parrot back the Origin header if so.
|
||
We also need to add the Access-Control-Allow-Credentials header and set it to true for those type requests
|
||
--->
|
||
<cfset local.domains = listToArray( application._taffy.settings.allowCrossDomain, ', ;' )>
|
||
<cfif structKeyExists(_taffyRequest.headers, "origin")>
|
||
<cfloop from="1" to="#arrayLen( local.domains )#" index="local.i">
|
||
<cfif lcase( rereplace( _taffyRequest.headers.origin, "(http|https):\/\/", "", "all" ) ) EQ lcase( rereplace( local.domains[ local.i ], "(http|https):\/\/", "", "all" ) ) >
|
||
<cfheader name="Access-Control-Allow-Origin" value="#_taffyRequest.headers.origin#" />
|
||
<cfheader name="Access-Control-Allow-Credentials" value="true" />
|
||
<cfbreak>
|
||
</cfif>
|
||
</cfloop>
|
||
</cfif>
|
||
</cfif>
|
||
<cfheader name="Access-Control-Allow-Methods" value="#local.allowVerbs#" />
|
||
<!--- Why do we parrot back these headers? See: https://github.com/atuttle/Taffy/issues/144 --->
|
||
<cfif not structKeyExists(_taffyRequest.headers, "Access-Control-Request-Headers")>
|
||
<cfheader name="Access-Control-Allow-Headers" value="Origin, Authorization, X-CSRF-Token, X-Requested-With, Content-Type, X-HTTP-Method-Override, Accept, Referrer, User-Agent" />
|
||
<cfelse>
|
||
<!--- parrot back all of the request headers to allow the request to continue (can we improve on this?) --->
|
||
<cfset local.allowedHeaders = {} />
|
||
<cfloop list="Origin,Authorization,X-CSRF-Token,X-Requested-With,Content-Type,X-HTTP-Method-Override,Accept,Referrer,User-Agent" index="local.h">
|
||
<cfset local.allowedHeaders[local.h] = 1 />
|
||
</cfloop>
|
||
<cfset local.requestedHeaders = _taffyRequest.headers['Access-Control-Request-Headers'] />
|
||
<cfloop list="#local.requestedHeaders#" index="local.i">
|
||
<cfset local.allowedHeaders[ local.i ] = 1 />
|
||
</cfloop>
|
||
<cfheader name="Access-Control-Allow-Headers" value="#structKeyList(local.allowedHeaders)#" />
|
||
</cfif>
|
||
</cfif>
|
||
|
||
<!--- global headers --->
|
||
<cfset addHeaders(getGlobalHeaders()) />
|
||
|
||
<!---
|
||
Now we know everything we need to know to service the request. let's service it!
|
||
--->
|
||
|
||
<!--- ...after we let the api developer know all of the request details first... --->
|
||
<cfset m.beforeOnTaffyRequest = getTickCount() />
|
||
<cfset _taffyRequest.continue = onTaffyRequest(
|
||
_taffyRequest.verb
|
||
,_taffyRequest.matchDetails.beanName
|
||
,_taffyRequest.requestArguments
|
||
,_taffyRequest.returnMimeExt
|
||
,_taffyRequest.headers
|
||
,_taffyRequest.methodMetadata
|
||
,local.parsed.matchDetails.srcUri
|
||
) />
|
||
<cfset m.afterOnTaffyRequest = getTickCount() />
|
||
<cfset m.otrTime = m.afterOnTaffyRequest - m.beforeOnTaffyRequest />
|
||
|
||
<cfif not structKeyExists(_taffyRequest, "continue")>
|
||
<!--- developer forgot to return true --->
|
||
<cfthrow
|
||
message="Error in your onTaffyRequest method"
|
||
detail="Your onTaffyRequest method returned no value. Expected: Return TRUE or call noData()/representationOf()."
|
||
errorcode="400"
|
||
/>
|
||
</cfif>
|
||
|
||
<cfif isObject(_taffyRequest.continue)>
|
||
<!--- inspection complete but request has been aborted by developer; return custom response --->
|
||
<cfset _taffyRequest.result = duplicate(_taffyRequest.continue) />
|
||
<cfset structDelete(_taffyRequest, "continue")/>
|
||
<cfset m.resourceTime = 0 />
|
||
<cfelse>
|
||
<!--- inspection complete and request allowed by developer --->
|
||
|
||
<!--- handle requests for simulated responses --->
|
||
<cfif structKeyExists(_taffyRequest.requestArguments, application._taffy.settings.simulateKey) and _taffyRequest.requestArguments[application._taffy.settings.simulateKey] eq application._taffy.settings.simulatePassword>
|
||
<!--- is there a simulated response? --->
|
||
<cfset sampler = 'sample#_taffyRequest.method#Response' />
|
||
<cfif structKeyExists(_taffyRequest.matchDetails.metadata, sampler)>
|
||
<!--- get simulated response --->
|
||
<cfinvoke
|
||
component="#application._taffy.factory.getBean(_taffyRequest.matchDetails.beanName)#"
|
||
method="#sampler#"
|
||
returnvariable="_taffyRequest.result"
|
||
/>
|
||
<cfset _taffyRequest.result = rep(_taffyRequest.result) />
|
||
<cfelse>
|
||
<!--- no method for simulated response, so return 400 --->
|
||
<cfset _taffyRequest.result = noData().withStatus(400, "No Sample Response Available") />
|
||
</cfif>
|
||
<cfelse>
|
||
<!--- send request to service --->
|
||
<cfif structKeyExists(_taffyRequest.matchDetails.methods, _taffyRequest.verb)>
|
||
<!--- check the cache before we call the resource --->
|
||
<cfset m.cacheCheckTime = getTickCount() />
|
||
<cfset local.cacheKey = getCacheKey(
|
||
_taffyRequest.matchDetails.beanName
|
||
,_taffyRequest.requestArguments
|
||
,local.parsed.matchDetails.srcUri
|
||
) />
|
||
<cfif ucase(_taffyRequest.verb) eq "GET" and validCacheExists(local.cacheKey)>
|
||
<cfset m.cacheCheckTime = getTickCount() - m.cacheCheckTime />
|
||
<cfset m.cacheGetTime = getTickCount() />
|
||
<cfset _taffyRequest.result = getCachedResponse(local.cacheKey) />
|
||
<cfset m.cacheGetTime = m.cacheGetTime - getTickCount() />
|
||
<cfelse>
|
||
<cfif ucase(_taffyRequest.verb) eq "GET">
|
||
<cfset m.cacheCheckTime = getTickCount() - m.cacheCheckTime />
|
||
<cfelse>
|
||
<cfset structDelete(m, "cacheCheckTime") />
|
||
</cfif>
|
||
<!--- returns a representation-object --->
|
||
<cfset m.beforeResource = getTickCount() />
|
||
<cfinvoke
|
||
component="#application._taffy.factory.getBean(_taffyRequest.matchDetails.beanName)#"
|
||
method="#_taffyRequest.method#"
|
||
argumentcollection="#_taffyRequest.requestArguments#"
|
||
returnvariable="_taffyRequest.result"
|
||
/>
|
||
<cfset m.afterResource = getTickCount() />
|
||
<cfset m.resourceTime = m.afterResource - m.beforeResource />
|
||
<cfif !isDefined("_taffyRequest.result")>
|
||
<cfthrow
|
||
message="Resource did not return a value"
|
||
detail="The resource is expected to return a call to rep()/representationOf() or noData(). It appears there was no return at all."
|
||
errorcode="taffy.resources.ResourceReturnsNothing"
|
||
/>
|
||
</cfif>
|
||
<!--- If the type returned is not an instance of baseSerializer, wrap it with a call to rep().
|
||
This way we can directly return the object instead of a serializer from resource actions. --->
|
||
<cfif !isInstanceOf(_taffyRequest.result, "taffy.core.baseSerializer")>
|
||
<cfset _taffyRequest.result = rep(_taffyRequest.result) />
|
||
</cfif>
|
||
<cfif ucase(_taffyRequest.verb) eq "GET" and structKeyExists(local, "cacheKey")>
|
||
<cfset m.cacheSaveStart = getTickCount() />
|
||
<cfset setCachedResponse(local.cacheKey, _taffyRequest.result) />
|
||
<cfset m.cacheSaveTime = getTickCount() - m.cacheSaveStart />
|
||
</cfif>
|
||
</cfif>
|
||
<cfelseif NOT listFind(local.allowVerbs,_taffyRequest.verb)>
|
||
<!--- if the verb is not implemented, refuse the request --->
|
||
<cfheader name="ALLOW" value="#local.allowVerbs#" />
|
||
<cfset throwError(405, "Method Not Allowed") />
|
||
<cfelse>
|
||
<!--- create dummy response for cross domain OPTIONS request --->
|
||
<cfset _taffyRequest.resultHeaders = structNew() />
|
||
<cfset _taffyRequest.statusArgs = structNew() />
|
||
<cfset _taffyRequest.statusArgs.statusCode = 200 />
|
||
<cfset _taffyRequest.statusArgs.statusText = 'OK' />
|
||
</cfif>
|
||
</cfif>
|
||
|
||
</cfif>
|
||
<!--- make sure the requested mime type is available --->
|
||
<cfif not mimeSupported(_taffyRequest.returnMimeExt)>
|
||
<cfset throwError(400, "Requested format not available (#_taffyRequest.returnMimeExt#)") />
|
||
</cfif>
|
||
|
||
<cfif structKeyExists(_taffyRequest,'result')>
|
||
<!--- get status code --->
|
||
<cfset _taffyRequest.statusArgs = structNew() />
|
||
<cfset _taffyRequest.statusArgs.statusCode = _taffyRequest.result.getStatus() />
|
||
<cfset _taffyRequest.statusArgs.statusText = _taffyRequest.result.getStatusText() />
|
||
<!--- get custom headers --->
|
||
<cfinvoke
|
||
component="#_taffyRequest.result#"
|
||
method="getHeaders"
|
||
returnvariable="_taffyRequest.resultHeaders"
|
||
/>
|
||
</cfif>
|
||
|
||
<cfsetting enablecfoutputonly="true" />
|
||
<cfcontent reset="true" type="#getReturnMimeAsHeader(_taffyRequest.returnMimeExt)#; charset=utf-8" />
|
||
<cfheader statuscode="#_taffyRequest.statusArgs.statusCode#" statustext="#_taffyRequest.statusArgs.statusText#" />
|
||
|
||
<!--- headers --->
|
||
<cfset addHeaders(_taffyRequest.resultHeaders) />
|
||
|
||
<!--- add ALLOW header for current resource, which describes available verbs --->
|
||
<cfheader name="ALLOW" value="#local.allowVerbs#" />
|
||
|
||
<!--- metrics headers that should always apply --->
|
||
<cfheader name="X-TIME-IN-PARSE" value="#m.parseTime#" />
|
||
<cfheader name="X-TIME-IN-ONTAFFYREQUEST" value="#m.otrTime#" />
|
||
<cfif structKeyExists(m, "resourceTime")>
|
||
<cfheader name="X-TIME-IN-RESOURCE" value="#m.resourceTime#" />
|
||
</cfif>
|
||
<cfif structKeyExists(m, "cacheCheckTime")>
|
||
<cfheader name="X-TIME-IN-CACHE-CHECK" value="#m.cacheCheckTime#" />
|
||
</cfif>
|
||
<cfif structKeyExists(m, "cacheGetTime")>
|
||
<cfheader name="X-TIME-IN-CACHE-GET" value="#m.cacheGetTime#" />
|
||
</cfif>
|
||
<cfif structKeyExists(m, "cacheSaveTime")>
|
||
<cfheader name="X-TIME-IN-CACHE-SAVE" value="#m.cacheSaveTime#" />
|
||
</cfif>
|
||
|
||
<cfif application._taffy.settings.exposeHeaders>
|
||
<cfset local.exposeHeaderList = structKeyList(_taffyRequest.resultHeaders) />
|
||
<cfset local.exposeHeaderValue = "" />
|
||
<cfif application._taffy.settings.useEtags and _taffyRequest.verb eq "GET" and _taffyRequest.result.getType() eq "textual">
|
||
<cfset local.exposeHeaderList = listAppend(local.exposeHeaderList, "Etag") />
|
||
</cfif>
|
||
<cfloop list="#local.exposeHeaderList#" index="local.exposeHeader">
|
||
<!--- filter out default simple response headers: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Headers --->
|
||
<cfif not listFindNoCase("Cache-Control,Content-Language,Content-Type,Expires,Last-Modified,Pragma", local.exposeHeader)>
|
||
<cfset local.exposeHeaderValue = listAppend(local.exposeHeaderValue, local.exposeHeader) />
|
||
</cfif>
|
||
</cfloop>
|
||
<cfif listLen(local.exposeHeaderValue) gt 0>
|
||
<cfheader name="Access-Control-Expose-Headers" value="#local.exposeHeaderValue#" />
|
||
</cfif>
|
||
</cfif>
|
||
|
||
<!--- result data --->
|
||
<cfif structKeyExists(_taffyRequest,'result')>
|
||
<cfset _taffyRequest.resultType = _taffyRequest.result.getType() />
|
||
<cfset local.resultSerialized = '' />
|
||
|
||
<cfif _taffyRequest.resultType eq "textual">
|
||
<!--- serialize the representation's data into the requested mime type --->
|
||
<cfset _taffyRequest.metrics.beforeSerialize = getTickCount() />
|
||
<cfinvoke
|
||
component="#_taffyRequest.result#"
|
||
method="getAs#_taffyRequest.returnMimeExt#"
|
||
returnvariable="_taffyRequest.resultSerialized"
|
||
/>
|
||
<cfset _taffyRequest.metrics.afterSerialize = getTickCount() />
|
||
<cfset m.serializeTime = m.afterSerialize - m.beforeSerialize />
|
||
<cfheader name="X-TIME-IN-SERIALIZE" value="#m.serializeTime#" />
|
||
|
||
<!--- apply jsonp wrapper if requested --->
|
||
<cfif structKeyExists(_taffyRequest, "jsonpCallback")>
|
||
<cfset _taffyRequest.resultSerialized = _taffyRequest.jsonpCallback & "(" & _taffyRequest.resultSerialized & ");" />
|
||
</cfif>
|
||
|
||
<!--- don't return data if etags are enabled and the data hasn't changed --->
|
||
<cfif application._taffy.settings.useEtags and _taffyRequest.verb eq "GET">
|
||
<!--- etag values are quoted per: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag --->
|
||
<cfif structKeyExists(server, "lucee")>
|
||
<!--- hashCode() will not work for lucee, see issue #354 --->
|
||
<cfset _taffyRequest.serverEtag = '"' & hash(_taffyRequest.resultSerialized) & '"' />
|
||
<cfelse>
|
||
<cfset _taffyRequest.serverEtag = '"' & _taffyRequest.result.getData().hashCode() & '"' />
|
||
</cfif>
|
||
<cfif structKeyExists(_taffyRequest.headers, "If-None-Match")>
|
||
<cfset _taffyRequest.clientEtag = _taffyRequest.headers['If-None-Match'] />
|
||
|
||
<cfif len(_taffyRequest.clientEtag) gt 0 and _taffyRequest.clientEtag eq _taffyRequest.serverEtag>
|
||
<cfheader statuscode="304" statustext="Not Modified" />
|
||
<cfcontent reset="true" type="#application._taffy.settings.mimeExtensions[_taffyRequest.returnMimeExt]#; charset=utf-8" />
|
||
<cfreturn true />
|
||
<cfelse>
|
||
<cfheader name="Etag" value="#_taffyRequest.serverEtag#" />
|
||
</cfif>
|
||
<cfelse>
|
||
<cfheader name="Etag" value="#_taffyRequest.serverEtag#" />
|
||
</cfif>
|
||
</cfif>
|
||
|
||
<cfset m.done = getTickCount() />
|
||
<cfset m.taffyTime = m.done - m.init - m.parseTime - m.otrTime - m.serializeTime />
|
||
<cfif structKeyExists(m, "resourceTime")>
|
||
<cfset m.taffyTime -= m.resourceTime />
|
||
</cfif>
|
||
<cfheader name="X-TIME-IN-TAFFY" value="#m.taffyTime#" />
|
||
|
||
<cfcontent reset="true" type="#application._taffy.settings.mimeExtensions[_taffyRequest.returnMimeExt]#; charset=utf-8" />
|
||
<cfif _taffyRequest.resultSerialized neq ('"' & '"')>
|
||
<cfset local.resultSerialized = _taffyRequest.resultSerialized />
|
||
</cfif>
|
||
<!--- debug output --->
|
||
<cfif structKeyExists(url, application._taffy.settings.debugKey)>
|
||
<cfset local.debug = true />
|
||
</cfif>
|
||
|
||
<cfelseif _taffyRequest.resultType eq "filename">
|
||
<cfset m.done = getTickCount() />
|
||
<cfset m.taffyTime = m.done - m.init - m.parseTime - m.otrTime - m.resourceTime />
|
||
<cfheader name="X-TIME-IN-TAFFY" value="#m.taffyTime#" />
|
||
<cfcontent reset="true" file="#_taffyRequest.result.getFileName()#" type="#_taffyRequest.result.getFileMime()#" deletefile="#_taffyRequest.result.getDeleteFile()#" />
|
||
|
||
<cfelseif _taffyRequest.resultType eq "filedata">
|
||
<cfset m.done = getTickCount() />
|
||
<cfset m.taffyTime = m.done - m.init - m.parseTime - m.otrTime - m.resourceTime />
|
||
<cfheader name="X-TIME-IN-TAFFY" value="#m.taffyTime#" />
|
||
<cfcontent reset="true" variable="#_taffyRequest.result.getFileData()#" type="#_taffyRequest.result.getFileMime()#" />
|
||
|
||
<cfelseif _taffyRequest.resultType eq "imagedata">
|
||
<cfset m.done = getTickCount() />
|
||
<cfset m.taffyTime = m.done - m.init - m.parseTime - m.otrTime - m.resourceTime />
|
||
<cfheader name="X-TIME-IN-TAFFY" value="#m.taffyTime#" />
|
||
<cfcontent reset="true" variable="#_taffyRequest.result.getImageData()#" type="#_taffyRequest.result.getFileMime()#" />
|
||
|
||
</cfif>
|
||
</cfif>
|
||
|
||
<cfset local.resultSerialized = "" />
|
||
<cfif structKeyExists( _taffyRequest, "resultSerialized" )>
|
||
<cfset local.resultSerialized = _taffyRequest.resultSerialized />
|
||
</cfif>
|
||
|
||
<cfset local.result = StructNew() />
|
||
<cfif structKeyExists( _taffyRequest, "result" )>
|
||
<cfset local.result = _taffyRequest.result.getData() />
|
||
</cfif>
|
||
|
||
<!--- ...after the service has finished... --->
|
||
<cfset m.beforeOnTaffyRequestEnd = getTickCount() />
|
||
<cfset onTaffyRequestEnd(
|
||
_taffyRequest.verb
|
||
,_taffyRequest.matchDetails.beanName
|
||
,_taffyRequest.requestArguments
|
||
,_taffyRequest.returnMimeExt
|
||
,_taffyRequest.headers
|
||
,_taffyRequest.methodMetadata
|
||
,local.parsed.matchDetails.srcUri
|
||
,local.resultSerialized
|
||
,local.result
|
||
,_taffyRequest.statusArgs.statusCode
|
||
) />
|
||
<cfset m.otreTime = getTickCount() - m.beforeOnTaffyRequestEnd />
|
||
<cfheader name="X-TIME-IN-ONTAFFYREQUESTEND" value="#m.otreTime#" />
|
||
|
||
<cfif len(trim(local.resultSerialized))>
|
||
<cfoutput>#local.resultSerialized#</cfoutput>
|
||
</cfif>
|
||
<!--- debug output --->
|
||
<cfif local.debug>
|
||
<cfoutput><h3>Request Details:</h3><cfdump var="#_taffyRequest#"></cfoutput>
|
||
</cfif>
|
||
|
||
<cfreturn true />
|
||
</cffunction>
|
||
|
||
|
||
<!--- <cffunction name="checkForLocalDebug">
|
||
<cfreturn fileExists("#GetDirectoryFromPath(GetCurrentTemplatePath())#/etc/local-debug")>
|
||
</cffunction> --->
|
||
|
||
|
||
</cfcomponent>
|