feat(bridge): port bridge_*.prg + wire HTTP↔bridge dispatcher
Pulls bridge_context.prg, bridge_request.prg, bridge_session.prg, and
bridge_cookie.prg from upstream fivenode into app/bridge/ so the
mod_harbour AP_* surface (AP_METHOD, AP_BODY, AP_ARGS, AP_USERIP,
AP_RPUTS, AP_JSONRESPONSE, AP_SETCONTENTTYPE, ctx_get, ctx_set, ...)
runs unchanged on top of fivenode_go's Go RTL.
bridge_main.prg deliberately omitted — its REQUEST sweep pulls in
TDrMySQL / hbct / hbcurl symbols fivenode_go neither has nor needs.
fivenode_go runs ahead-of-time with the Five compiler, so the
REQUEST trick that keeps fnb-runtime symbols alive isn't required.
One upstream patch was unavoidable: AP_RPUTS / AP_ECHO were variadic
(`( ... )`) and used PValue() to walk caller args. Five's PValue
returns the caller's LOCAL slot (not the actual variadic args, which
aren't copied into locals when declared params is 0), so the body
came back as "1" instead of the JSON payload. Collapsed both to a
single-argument form; every call site in fivenode_go already passes
exactly one value. Patched-out spots are marked with a TODO so we
can revert once Five gains real variadic PValue support.
app/bridge_server.prg ties it all together: starts httpserver on
:8090 with BRIDGEDISPATCH as the handler, hand-translates the Go
request hash into the ctx fields the AP_* layer reads, dispatches by
URL path (hard-coded /api/hello and /api/echo for now — file-name
dispatch lands in 1a.4), and assembles the response from the buffered
AP_* output + ctx_get("status") + ctx_get("headers_out").
Verified end-to-end:
GET /api/hello -> 200 JSON, method/ip echoed
POST /api/echo?lang=ko (16-byte body) -> 200 JSON, body_parsed,
query, user-agent
GET /api/nope -> 404 JSON
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
78
app/bridge/bridge_context.prg
Normal file
78
app/bridge/bridge_context.prg
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
/*
|
||||||
|
* bridge_context.prg - FiveNode Request Context (Harbour side)
|
||||||
|
* JSON-based context access, output buffer wrappers
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 Charles KWON ( Charles KWON Ohjun, charleskwonohjun@gmail.com )
|
||||||
|
* All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
FUNCTION BRIDGE_SETUP_ERRORBLOCK()
|
||||||
|
|
||||||
|
LOCAL nH := FCreate( hb_DirTemp() + "errblock_proof.txt" )
|
||||||
|
|
||||||
|
FWrite( nH, "ErrorBlock SET at " + Time() )
|
||||||
|
FClose( nH )
|
||||||
|
|
||||||
|
ErrorBlock( {| e | Break( e ) } )
|
||||||
|
|
||||||
|
RETURN NIL
|
||||||
|
|
||||||
|
FUNCTION BRIDGE_PING()
|
||||||
|
|
||||||
|
hb_MemoWrit( hb_DirTemp() + "ping_proof.txt", "PING_EXECUTED" )
|
||||||
|
_BRIDGE_SET_RESULT( "PONG" )
|
||||||
|
|
||||||
|
RETURN NIL
|
||||||
|
|
||||||
|
FUNCTION hb_bridge_get_output()
|
||||||
|
RETURN _OUT_GET()
|
||||||
|
|
||||||
|
FUNCTION hb_bridge_clear_output()
|
||||||
|
|
||||||
|
_OUT_CLEAR()
|
||||||
|
|
||||||
|
RETURN NIL
|
||||||
|
|
||||||
|
FUNCTION bridge_output_append( cText )
|
||||||
|
|
||||||
|
_OUT_APPEND( cText )
|
||||||
|
|
||||||
|
RETURN NIL
|
||||||
|
|
||||||
|
// ── Context value accessor (extract from JSON via hb_jsonDecode) ──
|
||||||
|
|
||||||
|
FUNCTION ctx_get( cKey, xDefault )
|
||||||
|
|
||||||
|
LOCAL cJson := _CTX_GET_JSON()
|
||||||
|
LOCAL hCtx
|
||||||
|
|
||||||
|
IF Empty( cJson )
|
||||||
|
RETURN xDefault
|
||||||
|
ENDIF
|
||||||
|
|
||||||
|
hb_jsonDecode( cJson, @hCtx )
|
||||||
|
|
||||||
|
IF ValType( hCtx ) == "H" .AND. hb_HHasKey( hCtx, cKey )
|
||||||
|
RETURN hCtx[ cKey ]
|
||||||
|
ENDIF
|
||||||
|
|
||||||
|
RETURN xDefault
|
||||||
|
|
||||||
|
FUNCTION ctx_set( cKey, xValue )
|
||||||
|
|
||||||
|
LOCAL cJson := _CTX_GET_JSON()
|
||||||
|
LOCAL hCtx
|
||||||
|
|
||||||
|
IF Empty( cJson )
|
||||||
|
hCtx := { => }
|
||||||
|
ELSE
|
||||||
|
hb_jsonDecode( cJson, @hCtx )
|
||||||
|
IF ValType( hCtx ) != "H"
|
||||||
|
hCtx := { => }
|
||||||
|
ENDIF
|
||||||
|
ENDIF
|
||||||
|
|
||||||
|
hCtx[ cKey ] := xValue
|
||||||
|
_CTX_SET_JSON( hb_jsonEncode( hCtx ) )
|
||||||
|
|
||||||
|
RETURN xValue
|
||||||
109
app/bridge/bridge_cookie.prg
Normal file
109
app/bridge/bridge_cookie.prg
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
/*
|
||||||
|
* bridge_cookie.prg - FiveNode
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 Charles KWON ( Charles KWON Ohjun, charleskwonohjun@gmail.com )
|
||||||
|
* All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// bridge_cookie.prg
|
||||||
|
// Cookie functions (6) - mod_harbourv2 100% compatible
|
||||||
|
|
||||||
|
FUNCTION fn_GetCookies( cKey )
|
||||||
|
|
||||||
|
LOCAL hCookies := _parse_cookies()
|
||||||
|
|
||||||
|
IF cKey == NIL
|
||||||
|
RETURN hCookies
|
||||||
|
ENDIF
|
||||||
|
|
||||||
|
IF hb_HHasKey( hCookies, cKey )
|
||||||
|
RETURN hCookies[ cKey ]
|
||||||
|
ENDIF
|
||||||
|
|
||||||
|
RETURN ""
|
||||||
|
|
||||||
|
FUNCTION AP_COOKIE_READ( cName )
|
||||||
|
RETURN fn_GetCookies( cName )
|
||||||
|
|
||||||
|
FUNCTION fn_SetCookie( cName, cValue, nSecs, cPath, cDomain, lHttps, lOnlyHttp )
|
||||||
|
|
||||||
|
LOCAL cCookie, aCookies
|
||||||
|
|
||||||
|
hb_default( @cPath, "/" )
|
||||||
|
hb_default( @lHttps, .F. )
|
||||||
|
hb_default( @lOnlyHttp, .F. )
|
||||||
|
|
||||||
|
cCookie := cName + "=" + cValue
|
||||||
|
|
||||||
|
IF nSecs != NIL
|
||||||
|
cCookie += "; Max-Age=" + hb_ntos( nSecs )
|
||||||
|
ENDIF
|
||||||
|
|
||||||
|
cCookie += "; Path=" + cPath
|
||||||
|
|
||||||
|
IF cDomain != NIL
|
||||||
|
cCookie += "; Domain=" + cDomain
|
||||||
|
ENDIF
|
||||||
|
|
||||||
|
IF lHttps
|
||||||
|
cCookie += "; Secure"
|
||||||
|
ENDIF
|
||||||
|
|
||||||
|
IF lOnlyHttp
|
||||||
|
cCookie += "; HttpOnly"
|
||||||
|
ENDIF
|
||||||
|
|
||||||
|
aCookies := ctx_get( "cookies_out", {} )
|
||||||
|
AAdd( aCookies, cCookie )
|
||||||
|
ctx_set( "cookies_out", aCookies )
|
||||||
|
|
||||||
|
RETURN NIL
|
||||||
|
|
||||||
|
FUNCTION AP_COOKIE_WRITE( cName, cValue, nExpire )
|
||||||
|
RETURN fn_SetCookie( cName, cValue, nExpire )
|
||||||
|
|
||||||
|
FUNCTION AP_COOKIE_REMOVE( cName )
|
||||||
|
RETURN fn_SetCookie( cName, "", 0 )
|
||||||
|
|
||||||
|
FUNCTION AP_COOKIE_CHECK_STRING( cString )
|
||||||
|
|
||||||
|
IF Empty( cString )
|
||||||
|
RETURN .F.
|
||||||
|
ENDIF
|
||||||
|
|
||||||
|
IF ";" $ cString .OR. Chr(13) $ cString .OR. Chr(10) $ cString
|
||||||
|
RETURN .F.
|
||||||
|
ENDIF
|
||||||
|
|
||||||
|
RETURN .T.
|
||||||
|
|
||||||
|
// ── Cookie parsing helper ──
|
||||||
|
|
||||||
|
STATIC FUNCTION _parse_cookies()
|
||||||
|
|
||||||
|
LOCAL hHeaders, cRaw, aParts, cPart, aPair
|
||||||
|
LOCAL hCookies := { => }
|
||||||
|
|
||||||
|
hHeaders := AP_HEADERSIN()
|
||||||
|
|
||||||
|
IF hb_HHasKey( hHeaders, "Cookie" )
|
||||||
|
cRaw := hHeaders[ "Cookie" ]
|
||||||
|
aParts := hb_ATokens( cRaw, ";" )
|
||||||
|
FOR EACH cPart IN aParts
|
||||||
|
cPart := AllTrim( cPart )
|
||||||
|
aPair := hb_ATokens( cPart, "=" )
|
||||||
|
IF Len( aPair ) >= 2
|
||||||
|
hCookies[ AllTrim( aPair[1] ) ] := AllTrim( aPair[2] )
|
||||||
|
ENDIF
|
||||||
|
NEXT
|
||||||
|
ENDIF
|
||||||
|
|
||||||
|
RETURN hCookies
|
||||||
|
|
||||||
|
// -- mod_harbour compatibility wrappers --
|
||||||
|
|
||||||
|
FUNCTION MH_GetCookies( cKey )
|
||||||
|
RETURN fn_GetCookies( cKey )
|
||||||
|
|
||||||
|
FUNCTION MH_SetCookie( cName, cValue, nSecs, cPath, cDomain, lHttps, lOnlyHttp )
|
||||||
|
RETURN fn_SetCookie( cName, cValue, nSecs, cPath, cDomain, lHttps, lOnlyHttp )
|
||||||
342
app/bridge/bridge_request.prg
Normal file
342
app/bridge/bridge_request.prg
Normal file
@@ -0,0 +1,342 @@
|
|||||||
|
/*
|
||||||
|
* bridge_request.prg - FiveNode
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 Charles KWON ( Charles KWON Ohjun, charleskwonohjun@gmail.com )
|
||||||
|
* All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// bridge_request.prg
|
||||||
|
// AP_* function reimplementation - 100% identical function names/signatures
|
||||||
|
|
||||||
|
// ── Request functions ──
|
||||||
|
|
||||||
|
FUNCTION AP_METHOD()
|
||||||
|
RETURN ctx_get( "method", "GET" )
|
||||||
|
|
||||||
|
FUNCTION AP_FILENAME()
|
||||||
|
RETURN ctx_get( "filename", "" )
|
||||||
|
|
||||||
|
FUNCTION AP_ARGS()
|
||||||
|
RETURN ctx_get( "query_string", "" )
|
||||||
|
|
||||||
|
FUNCTION AP_USERIP()
|
||||||
|
RETURN ctx_get( "remote_ip", "127.0.0.1" )
|
||||||
|
|
||||||
|
FUNCTION AP_BODY()
|
||||||
|
RETURN ctx_get( "body", "" )
|
||||||
|
|
||||||
|
FUNCTION AP_GETBODY()
|
||||||
|
RETURN ctx_get( "body", "" )
|
||||||
|
|
||||||
|
FUNCTION AP_GETENV( cVarName )
|
||||||
|
|
||||||
|
LOCAL hEnv := ctx_get( "env", { => } )
|
||||||
|
|
||||||
|
IF hb_HHasKey( hEnv, Upper( cVarName ) )
|
||||||
|
RETURN hEnv[ Upper( cVarName ) ]
|
||||||
|
ENDIF
|
||||||
|
|
||||||
|
RETURN hb_GetEnv( cVarName, "" )
|
||||||
|
|
||||||
|
// ── Headers In ──
|
||||||
|
|
||||||
|
FUNCTION AP_HEADERSIN()
|
||||||
|
RETURN ctx_get( "headers_in", { => } )
|
||||||
|
|
||||||
|
FUNCTION AP_HEADERSINCOUNT()
|
||||||
|
RETURN Len( ctx_get( "headers_in", { => } ) )
|
||||||
|
|
||||||
|
FUNCTION AP_HEADERSINKEY( n )
|
||||||
|
|
||||||
|
LOCAL hHeaders := ctx_get( "headers_in", { => } )
|
||||||
|
|
||||||
|
IF n >= 1 .AND. n <= Len( hHeaders )
|
||||||
|
RETURN hb_HKeyAt( hHeaders, n )
|
||||||
|
ENDIF
|
||||||
|
|
||||||
|
RETURN ""
|
||||||
|
|
||||||
|
FUNCTION AP_HEADERSINVAL( n )
|
||||||
|
|
||||||
|
LOCAL hHeaders := ctx_get( "headers_in", { => } )
|
||||||
|
|
||||||
|
IF n >= 1 .AND. n <= Len( hHeaders )
|
||||||
|
RETURN hb_HValueAt( hHeaders, n )
|
||||||
|
ENDIF
|
||||||
|
|
||||||
|
RETURN ""
|
||||||
|
|
||||||
|
// ── Headers Out ──
|
||||||
|
|
||||||
|
FUNCTION AP_HEADERSOUT()
|
||||||
|
RETURN ctx_get( "headers_out", { => } )
|
||||||
|
|
||||||
|
FUNCTION AP_HEADERSOUTCOUNT()
|
||||||
|
RETURN Len( ctx_get( "headers_out", { => } ) )
|
||||||
|
|
||||||
|
FUNCTION AP_HEADERSOUTKEY( n )
|
||||||
|
|
||||||
|
LOCAL hHeaders := ctx_get( "headers_out", { => } )
|
||||||
|
|
||||||
|
IF n >= 1 .AND. n <= Len( hHeaders )
|
||||||
|
RETURN hb_HKeyAt( hHeaders, n )
|
||||||
|
ENDIF
|
||||||
|
|
||||||
|
RETURN ""
|
||||||
|
|
||||||
|
FUNCTION AP_HEADERSOUTVAL( n )
|
||||||
|
|
||||||
|
LOCAL hHeaders := ctx_get( "headers_out", { => } )
|
||||||
|
|
||||||
|
IF n >= 1 .AND. n <= Len( hHeaders )
|
||||||
|
RETURN hb_HValueAt( hHeaders, n )
|
||||||
|
ENDIF
|
||||||
|
|
||||||
|
RETURN ""
|
||||||
|
|
||||||
|
FUNCTION AP_HEADERSOUTSET( cKey, cValue )
|
||||||
|
|
||||||
|
LOCAL hHeaders := ctx_get( "headers_out", { => } )
|
||||||
|
|
||||||
|
hHeaders[ cKey ] := cValue
|
||||||
|
ctx_set( "headers_out", hHeaders )
|
||||||
|
|
||||||
|
RETURN NIL
|
||||||
|
|
||||||
|
FUNCTION AP_SETCONTENTTYPE( cType )
|
||||||
|
RETURN AP_HEADERSOUTSET( "Content-Type", cType )
|
||||||
|
|
||||||
|
// ── Output ──
|
||||||
|
|
||||||
|
// fivenode_go patch: collapsed (...) -> single arg. Five's PValue returns
|
||||||
|
// the caller's LOCALs rather than the actual variadic args (declared
|
||||||
|
// params are 0 for `(...)`, so the runtime never copies args into the
|
||||||
|
// locals slot PValue reads). Every call site in this codebase passes
|
||||||
|
// exactly one value, so the collapse is behaviour-preserving.
|
||||||
|
// TODO: revert once Five gains true variadic PValue support.
|
||||||
|
FUNCTION AP_RPUTS( u )
|
||||||
|
IF ValType( u ) == "C"
|
||||||
|
bridge_output_append( u )
|
||||||
|
ELSE
|
||||||
|
bridge_output_append( fn_ValToChar( u ) )
|
||||||
|
ENDIF
|
||||||
|
RETURN NIL
|
||||||
|
|
||||||
|
FUNCTION AP_ECHO( u )
|
||||||
|
IF ValType( u ) == "C"
|
||||||
|
bridge_output_append( u )
|
||||||
|
ELSEIF ValType( u ) != "U"
|
||||||
|
bridge_output_append( fn_ValToChar( u ) )
|
||||||
|
ENDIF
|
||||||
|
RETURN NIL
|
||||||
|
|
||||||
|
// ── Parameter Parsing ──
|
||||||
|
|
||||||
|
FUNCTION AP_GetPairs( lUrlDecode )
|
||||||
|
|
||||||
|
LOCAL cArgs, aParams, aPair, hResult, cParam
|
||||||
|
|
||||||
|
hb_default( @lUrlDecode, .T. )
|
||||||
|
hResult := { => }
|
||||||
|
cArgs := AP_ARGS()
|
||||||
|
|
||||||
|
IF ! Empty( cArgs )
|
||||||
|
aParams := hb_ATokens( cArgs, "&" )
|
||||||
|
FOR EACH cParam IN aParams
|
||||||
|
aPair := hb_ATokens( cParam, "=" )
|
||||||
|
IF Len( aPair ) == 2
|
||||||
|
hResult[ aPair[ 1 ] ] := iif( lUrlDecode, hb_UrlDecode( aPair[ 2 ] ), aPair[ 2 ] )
|
||||||
|
ELSEIF Len( aPair ) == 1
|
||||||
|
hResult[ aPair[ 1 ] ] := ""
|
||||||
|
ENDIF
|
||||||
|
NEXT
|
||||||
|
ENDIF
|
||||||
|
|
||||||
|
RETURN hResult
|
||||||
|
|
||||||
|
FUNCTION AP_PostPairs( lUrlDecode )
|
||||||
|
|
||||||
|
LOCAL cBody, aParams, aPair, hResult, cParam
|
||||||
|
|
||||||
|
hb_default( @lUrlDecode, .T. )
|
||||||
|
hResult := { => }
|
||||||
|
cBody := AP_BODY()
|
||||||
|
|
||||||
|
IF ! Empty( cBody )
|
||||||
|
aParams := hb_ATokens( cBody, "&" )
|
||||||
|
FOR EACH cParam IN aParams
|
||||||
|
aPair := hb_ATokens( cParam, "=" )
|
||||||
|
IF Len( aPair ) == 2
|
||||||
|
hResult[ aPair[ 1 ] ] := iif( lUrlDecode, hb_UrlDecode( aPair[ 2 ] ), aPair[ 2 ] )
|
||||||
|
ELSEIF Len( aPair ) == 1
|
||||||
|
hResult[ aPair[ 1 ] ] := ""
|
||||||
|
ENDIF
|
||||||
|
NEXT
|
||||||
|
ENDIF
|
||||||
|
|
||||||
|
RETURN hResult
|
||||||
|
|
||||||
|
// ── URL decode ──
|
||||||
|
|
||||||
|
FUNCTION hb_UrlDecode( cString )
|
||||||
|
|
||||||
|
LOCAL cResult := "", i, c
|
||||||
|
|
||||||
|
cString := StrTran( cString, "+", " " )
|
||||||
|
i := 1
|
||||||
|
|
||||||
|
DO WHILE i <= Len( cString )
|
||||||
|
c := SubStr( cString, i, 1 )
|
||||||
|
IF c == "%" .AND. i + 2 <= Len( cString )
|
||||||
|
cResult += Chr( hb_HexToNum( SubStr( cString, i + 1, 2 ) ) )
|
||||||
|
i += 3
|
||||||
|
ELSE
|
||||||
|
cResult += c
|
||||||
|
i++
|
||||||
|
ENDIF
|
||||||
|
ENDDO
|
||||||
|
|
||||||
|
RETURN cResult
|
||||||
|
|
||||||
|
// ── Path/URL ──
|
||||||
|
|
||||||
|
FUNCTION fn_PathUrl()
|
||||||
|
RETURN ctx_get( "uri", "/" )
|
||||||
|
|
||||||
|
FUNCTION fn_PathBase( cDirFile )
|
||||||
|
|
||||||
|
LOCAL cRoot := ctx_get( "document_root", "." )
|
||||||
|
|
||||||
|
IF cDirFile != NIL
|
||||||
|
RETURN cRoot + "/" + cDirFile
|
||||||
|
ENDIF
|
||||||
|
|
||||||
|
RETURN cRoot
|
||||||
|
|
||||||
|
FUNCTION fn_Include( cFile )
|
||||||
|
RETURN hb_MemoRead( fn_PathBase( cFile ) )
|
||||||
|
|
||||||
|
FUNCTION fn_GetUri()
|
||||||
|
|
||||||
|
LOCAL cScheme := iif( ctx_get( "https", .F. ), "https", "http" )
|
||||||
|
LOCAL cHost := ""
|
||||||
|
LOCAL hHeaders := AP_HEADERSIN()
|
||||||
|
|
||||||
|
IF hb_HHasKey( hHeaders, "Host" )
|
||||||
|
cHost := hHeaders[ "Host" ]
|
||||||
|
ENDIF
|
||||||
|
|
||||||
|
RETURN cScheme + "://" + cHost + ctx_get( "uri", "/" )
|
||||||
|
|
||||||
|
FUNCTION fn_Redirect( cUrl )
|
||||||
|
|
||||||
|
AP_HEADERSOUTSET( "Location", cUrl )
|
||||||
|
ctx_set( "status", 302 )
|
||||||
|
|
||||||
|
RETURN NIL
|
||||||
|
|
||||||
|
// ── Data Conversion ──
|
||||||
|
|
||||||
|
FUNCTION fn_ValToChar( u )
|
||||||
|
|
||||||
|
LOCAL cType := ValType( u )
|
||||||
|
|
||||||
|
SWITCH cType
|
||||||
|
CASE "C" ; RETURN u
|
||||||
|
CASE "N" ; RETURN hb_ntos( u )
|
||||||
|
CASE "D" ; RETURN DToC( u )
|
||||||
|
CASE "L" ; RETURN iif( u, ".T.", ".F." )
|
||||||
|
CASE "U" ; RETURN "NIL"
|
||||||
|
CASE "A" ; RETURN hb_jsonEncode( u )
|
||||||
|
CASE "H" ; RETURN hb_jsonEncode( u )
|
||||||
|
CASE "T" ; RETURN hb_TToC( u )
|
||||||
|
ENDSWITCH
|
||||||
|
|
||||||
|
RETURN "(" + cType + ")"
|
||||||
|
|
||||||
|
// ctx_get / ctx_set are defined as PUBLIC in bridge_context.prg
|
||||||
|
|
||||||
|
// -- mod_harbour compatibility wrappers --
|
||||||
|
|
||||||
|
FUNCTION MH_PathUrl()
|
||||||
|
RETURN fn_PathUrl()
|
||||||
|
|
||||||
|
FUNCTION MH_PathBase( cDirFile )
|
||||||
|
RETURN fn_PathBase( cDirFile )
|
||||||
|
|
||||||
|
FUNCTION MH_Include( cFile )
|
||||||
|
RETURN fn_Include( cFile )
|
||||||
|
|
||||||
|
FUNCTION mh_GetUri()
|
||||||
|
RETURN fn_GetUri()
|
||||||
|
|
||||||
|
FUNCTION mh_Redirect( cUrl )
|
||||||
|
RETURN fn_Redirect( cUrl )
|
||||||
|
|
||||||
|
FUNCTION MH_ValToChar( u )
|
||||||
|
RETURN fn_ValToChar( u )
|
||||||
|
|
||||||
|
// ── JSON Response helpers ──
|
||||||
|
|
||||||
|
FUNCTION AP_JSONRESPONSE( xData, nStatus )
|
||||||
|
|
||||||
|
IF nStatus != NIL
|
||||||
|
ctx_set( "status", nStatus )
|
||||||
|
ENDIF
|
||||||
|
|
||||||
|
AP_SETCONTENTTYPE( "application/json" )
|
||||||
|
AP_RPUTS( hb_jsonEncode( xData ) )
|
||||||
|
|
||||||
|
RETURN NIL
|
||||||
|
|
||||||
|
FUNCTION AP_SETSTATUS( nStatus )
|
||||||
|
|
||||||
|
ctx_set( "status", nStatus )
|
||||||
|
|
||||||
|
RETURN NIL
|
||||||
|
|
||||||
|
// ── Auth (read-only from .prg, set by Node.js only) ──
|
||||||
|
|
||||||
|
FUNCTION fn_Auth()
|
||||||
|
|
||||||
|
LOCAL cAuth := _AUTH_GET()
|
||||||
|
LOCAL hAuth
|
||||||
|
|
||||||
|
IF Empty( cAuth )
|
||||||
|
RETURN { "authenticated" => .F. }
|
||||||
|
ENDIF
|
||||||
|
|
||||||
|
hb_jsonDecode( cAuth, @hAuth )
|
||||||
|
|
||||||
|
IF ValType( hAuth ) != "H"
|
||||||
|
RETURN { "authenticated" => .F. }
|
||||||
|
ENDIF
|
||||||
|
|
||||||
|
RETURN hAuth
|
||||||
|
|
||||||
|
FUNCTION fn_IsAuth()
|
||||||
|
RETURN fn_HGetDef( fn_Auth(), "authenticated", .F. )
|
||||||
|
|
||||||
|
FUNCTION fn_AuthUser()
|
||||||
|
RETURN fn_HGetDef( fn_Auth(), "user", "" )
|
||||||
|
|
||||||
|
FUNCTION fn_AuthRole()
|
||||||
|
RETURN fn_HGetDef( fn_Auth(), "role", "" )
|
||||||
|
|
||||||
|
// ── Hash helper ──
|
||||||
|
|
||||||
|
FUNCTION fn_HGetDef( hHash, cKey, xDefault )
|
||||||
|
RETURN hb_HGetDef( hHash, cKey, xDefault )
|
||||||
|
|
||||||
|
// ── JSON Body helper ──
|
||||||
|
|
||||||
|
FUNCTION AP_JSONBODY()
|
||||||
|
|
||||||
|
LOCAL cBody := AP_BODY()
|
||||||
|
LOCAL xResult
|
||||||
|
|
||||||
|
IF ! Empty( cBody )
|
||||||
|
hb_jsonDecode( cBody, @xResult )
|
||||||
|
ENDIF
|
||||||
|
|
||||||
|
RETURN xResult
|
||||||
266
app/bridge/bridge_session.prg
Normal file
266
app/bridge/bridge_session.prg
Normal file
@@ -0,0 +1,266 @@
|
|||||||
|
/*
|
||||||
|
* bridge_session.prg - FiveNode
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 Charles KWON ( Charles KWON Ohjun, charleskwonohjun@gmail.com )
|
||||||
|
* All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "hbclass.ch"
|
||||||
|
|
||||||
|
// bridge_session.prg
|
||||||
|
// Session management - mod_harbourv2 MH_Sessions 100% compatible
|
||||||
|
|
||||||
|
THREAD STATIC ts_oSession
|
||||||
|
|
||||||
|
// ── Public API ──
|
||||||
|
|
||||||
|
FUNCTION fn_SessionInit( cName, nExpired )
|
||||||
|
|
||||||
|
hb_default( @cName, "MHSID" )
|
||||||
|
hb_default( @nExpired, 3600 )
|
||||||
|
|
||||||
|
ts_oSession := MH_Sessions():New( cName, nExpired )
|
||||||
|
ts_oSession:Init()
|
||||||
|
|
||||||
|
RETURN NIL
|
||||||
|
|
||||||
|
FUNCTION fn_Session( cKey, uValue )
|
||||||
|
|
||||||
|
IF ts_oSession == NIL
|
||||||
|
RETURN NIL
|
||||||
|
ENDIF
|
||||||
|
|
||||||
|
IF cKey == NIL
|
||||||
|
RETURN ts_oSession:hData
|
||||||
|
ENDIF
|
||||||
|
|
||||||
|
IF uValue != NIL
|
||||||
|
ts_oSession:hData[ cKey ] := uValue
|
||||||
|
RETURN uValue
|
||||||
|
ENDIF
|
||||||
|
|
||||||
|
IF hb_HHasKey( ts_oSession:hData, cKey )
|
||||||
|
RETURN ts_oSession:hData[ cKey ]
|
||||||
|
ENDIF
|
||||||
|
|
||||||
|
RETURN NIL
|
||||||
|
|
||||||
|
FUNCTION fn_SessionWrite()
|
||||||
|
|
||||||
|
IF ts_oSession != NIL
|
||||||
|
ts_oSession:Write()
|
||||||
|
ENDIF
|
||||||
|
|
||||||
|
RETURN NIL
|
||||||
|
|
||||||
|
FUNCTION fn_SessionEnd()
|
||||||
|
|
||||||
|
IF ts_oSession != NIL
|
||||||
|
ts_oSession:End()
|
||||||
|
ts_oSession := NIL
|
||||||
|
ENDIF
|
||||||
|
|
||||||
|
RETURN NIL
|
||||||
|
|
||||||
|
FUNCTION fn_SessionActive()
|
||||||
|
RETURN ts_oSession != NIL
|
||||||
|
|
||||||
|
FUNCTION fn_oSession( o )
|
||||||
|
|
||||||
|
IF o != NIL
|
||||||
|
ts_oSession := o
|
||||||
|
ENDIF
|
||||||
|
|
||||||
|
RETURN ts_oSession
|
||||||
|
|
||||||
|
FUNCTION fn_Garbage()
|
||||||
|
|
||||||
|
IF ts_oSession != NIL
|
||||||
|
ts_oSession:Garbage()
|
||||||
|
ENDIF
|
||||||
|
|
||||||
|
RETURN NIL
|
||||||
|
|
||||||
|
// ── MH_Sessions class ──
|
||||||
|
|
||||||
|
CREATE CLASS MH_Sessions
|
||||||
|
|
||||||
|
VAR cName
|
||||||
|
VAR cId
|
||||||
|
VAR nExpired
|
||||||
|
VAR hData
|
||||||
|
VAR cSessionPath
|
||||||
|
VAR lDestroy
|
||||||
|
|
||||||
|
METHOD New( cName, nExpired )
|
||||||
|
METHOD Init()
|
||||||
|
METHOD SetId()
|
||||||
|
METHOD SessionFile()
|
||||||
|
METHOD Read_CSID()
|
||||||
|
METHOD Read()
|
||||||
|
METHOD Write()
|
||||||
|
METHOD Data( cKey, uValue )
|
||||||
|
METHOD End()
|
||||||
|
METHOD Garbage( dMaxDate )
|
||||||
|
METHOD IsFile()
|
||||||
|
METHOD Info()
|
||||||
|
|
||||||
|
ENDCLASS
|
||||||
|
|
||||||
|
METHOD New( cName, nExpired ) CLASS MH_Sessions
|
||||||
|
|
||||||
|
::cName := cName
|
||||||
|
::nExpired := nExpired
|
||||||
|
::hData := { => }
|
||||||
|
::cId := ""
|
||||||
|
::lDestroy := .F.
|
||||||
|
::cSessionPath := ctx_get( "document_root", "." ) + "/.sessions"
|
||||||
|
|
||||||
|
IF ! hb_DirExists( ::cSessionPath )
|
||||||
|
hb_DirCreate( ::cSessionPath )
|
||||||
|
ENDIF
|
||||||
|
|
||||||
|
RETURN Self
|
||||||
|
|
||||||
|
METHOD Init() CLASS MH_Sessions
|
||||||
|
|
||||||
|
::Read_CSID()
|
||||||
|
|
||||||
|
IF Empty( ::cId )
|
||||||
|
::SetId()
|
||||||
|
fn_SetCookie( ::cName, ::cId, ::nExpired, "/", NIL, .F., .T. )
|
||||||
|
ELSE
|
||||||
|
::Read()
|
||||||
|
ENDIF
|
||||||
|
|
||||||
|
RETURN NIL
|
||||||
|
|
||||||
|
METHOD SetId() CLASS MH_Sessions
|
||||||
|
|
||||||
|
::cId := hb_MD5( hb_TToS( hb_DateTime() ) + hb_ntos( hb_RandomInt( 100000, 999999 ) ) )
|
||||||
|
|
||||||
|
RETURN ::cId
|
||||||
|
|
||||||
|
METHOD SessionFile() CLASS MH_Sessions
|
||||||
|
RETURN ::cSessionPath + "/" + ::cId + ".session"
|
||||||
|
|
||||||
|
METHOD Read_CSID() CLASS MH_Sessions
|
||||||
|
|
||||||
|
LOCAL cCookieVal := fn_GetCookies( ::cName )
|
||||||
|
|
||||||
|
IF ! Empty( cCookieVal )
|
||||||
|
::cId := cCookieVal
|
||||||
|
ENDIF
|
||||||
|
|
||||||
|
RETURN ::cId
|
||||||
|
|
||||||
|
METHOD Read() CLASS MH_Sessions
|
||||||
|
|
||||||
|
LOCAL cFile := ::SessionFile()
|
||||||
|
LOCAL cContent
|
||||||
|
|
||||||
|
IF hb_FileExists( cFile )
|
||||||
|
cContent := hb_MemoRead( cFile )
|
||||||
|
IF ! Empty( cContent )
|
||||||
|
hb_jsonDecode( cContent, @::hData )
|
||||||
|
IF ::hData == NIL
|
||||||
|
::hData := { => }
|
||||||
|
ENDIF
|
||||||
|
ENDIF
|
||||||
|
ENDIF
|
||||||
|
|
||||||
|
RETURN NIL
|
||||||
|
|
||||||
|
METHOD Write() CLASS MH_Sessions
|
||||||
|
|
||||||
|
LOCAL cFile
|
||||||
|
|
||||||
|
IF ::lDestroy
|
||||||
|
cFile := ::SessionFile()
|
||||||
|
IF hb_FileExists( cFile )
|
||||||
|
FErase( cFile )
|
||||||
|
ENDIF
|
||||||
|
RETURN NIL
|
||||||
|
ENDIF
|
||||||
|
|
||||||
|
cFile := ::SessionFile()
|
||||||
|
hb_MemoWrit( cFile, hb_jsonEncode( ::hData ) )
|
||||||
|
|
||||||
|
RETURN NIL
|
||||||
|
|
||||||
|
METHOD Data( cKey, uValue ) CLASS MH_Sessions
|
||||||
|
|
||||||
|
IF cKey == NIL
|
||||||
|
RETURN ::hData
|
||||||
|
ENDIF
|
||||||
|
|
||||||
|
IF uValue != NIL
|
||||||
|
::hData[ cKey ] := uValue
|
||||||
|
RETURN uValue
|
||||||
|
ENDIF
|
||||||
|
|
||||||
|
IF hb_HHasKey( ::hData, cKey )
|
||||||
|
RETURN ::hData[ cKey ]
|
||||||
|
ENDIF
|
||||||
|
|
||||||
|
RETURN NIL
|
||||||
|
|
||||||
|
METHOD End() CLASS MH_Sessions
|
||||||
|
|
||||||
|
::lDestroy := .T.
|
||||||
|
::Write()
|
||||||
|
fn_SetCookie( ::cName, "", 0, "/" )
|
||||||
|
|
||||||
|
RETURN NIL
|
||||||
|
|
||||||
|
METHOD Garbage( dMaxDate ) CLASS MH_Sessions
|
||||||
|
|
||||||
|
LOCAL aFiles, cFile
|
||||||
|
|
||||||
|
hb_default( @dMaxDate, Date() - 1 )
|
||||||
|
aFiles := Directory( ::cSessionPath + "/*.session" )
|
||||||
|
|
||||||
|
FOR EACH cFile IN aFiles
|
||||||
|
IF cFile[ 3 ] < dMaxDate
|
||||||
|
FErase( ::cSessionPath + "/" + cFile[ 1 ] )
|
||||||
|
ENDIF
|
||||||
|
NEXT
|
||||||
|
|
||||||
|
RETURN NIL
|
||||||
|
|
||||||
|
METHOD IsFile() CLASS MH_Sessions
|
||||||
|
RETURN hb_FileExists( ::SessionFile() )
|
||||||
|
|
||||||
|
METHOD Info() CLASS MH_Sessions
|
||||||
|
RETURN { ;
|
||||||
|
"id" => ::cId, ;
|
||||||
|
"name" => ::cName, ;
|
||||||
|
"expired" => ::nExpired, ;
|
||||||
|
"path" => ::cSessionPath, ;
|
||||||
|
"file" => ::SessionFile(), ;
|
||||||
|
"data" => ::hData, ;
|
||||||
|
"destroy" => ::lDestroy ;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- mod_harbour compatibility wrappers --
|
||||||
|
|
||||||
|
FUNCTION mh_SessionInit( cName, nExpired )
|
||||||
|
RETURN fn_SessionInit( cName, nExpired )
|
||||||
|
|
||||||
|
FUNCTION mh_Session( cKey, uValue )
|
||||||
|
RETURN fn_Session( cKey, uValue )
|
||||||
|
|
||||||
|
FUNCTION mh_SessionWrite()
|
||||||
|
RETURN fn_SessionWrite()
|
||||||
|
|
||||||
|
FUNCTION mh_SessionEnd()
|
||||||
|
RETURN fn_SessionEnd()
|
||||||
|
|
||||||
|
FUNCTION mh_SessionActive()
|
||||||
|
RETURN fn_SessionActive()
|
||||||
|
|
||||||
|
FUNCTION mh_oSession( o )
|
||||||
|
RETURN fn_oSession( o )
|
||||||
|
|
||||||
|
FUNCTION mh_Garbage()
|
||||||
|
RETURN fn_Garbage()
|
||||||
91
app/bridge_server.prg
Normal file
91
app/bridge_server.prg
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
// app/bridge_server.prg — 1a.3-3 end-to-end glue.
|
||||||
|
//
|
||||||
|
// Wires httpserver (Go) → bridge_capi (Go) → bridge_*.prg so a PRG
|
||||||
|
// handler written in the mod_harbour AP_* style runs inside the
|
||||||
|
// fivenode_go single binary exactly as it would have run inside the
|
||||||
|
// koffi/N-API fivenode bridge.
|
||||||
|
//
|
||||||
|
// Demo routing is hard-coded to two paths so we can curl the binary
|
||||||
|
// and see the AP_* surface working end-to-end. File-name dispatch
|
||||||
|
// (POST /api/foo.prg → app/api/foo.prg) is sub-phase 1a.4 work.
|
||||||
|
|
||||||
|
FUNCTION Main()
|
||||||
|
LOCAL cErr := HTTP_SERVER_START( ":8090", "BRIDGEDISPATCH" )
|
||||||
|
IF cErr != NIL
|
||||||
|
? "httpserver:", cErr
|
||||||
|
ENDIF
|
||||||
|
RETURN NIL
|
||||||
|
|
||||||
|
// Entry point invoked by httpserver. hReq comes from
|
||||||
|
// buildRequestHash in hbrtl_ext/httpserver/server.go.
|
||||||
|
FUNCTION BridgeDispatch( hReq )
|
||||||
|
LOCAL hCtx
|
||||||
|
|
||||||
|
// Translate the Go-side request hash into the ctx fields the
|
||||||
|
// mod_harbour AP_* PRG layer expects to read.
|
||||||
|
hCtx := { ;
|
||||||
|
"method" => hReq[ "method" ], ;
|
||||||
|
"filename" => hReq[ "path" ], ;
|
||||||
|
"query_string" => hReq[ "query" ], ;
|
||||||
|
"body" => hReq[ "body" ], ;
|
||||||
|
"remote_ip" => RemoteIPOnly( hReq[ "remote_addr" ] ), ;
|
||||||
|
"headers_in" => hReq[ "headers" ], ;
|
||||||
|
"headers_out" => { => }, ;
|
||||||
|
"status" => 200 ;
|
||||||
|
}
|
||||||
|
_CTX_SET_JSON( hb_jsonEncode( hCtx ) )
|
||||||
|
_OUT_CLEAR()
|
||||||
|
|
||||||
|
// Hard-coded route dispatch. Replace with file-name dispatch in
|
||||||
|
// 1a.4 once we want to land the labdb API surface unchanged.
|
||||||
|
DO CASE
|
||||||
|
CASE hReq[ "path" ] == "/api/hello"
|
||||||
|
ApiHello()
|
||||||
|
CASE hReq[ "path" ] == "/api/echo"
|
||||||
|
ApiEcho()
|
||||||
|
OTHERWISE
|
||||||
|
ctx_set( "status", 404 )
|
||||||
|
AP_JSONRESPONSE( { "error" => "not found", "path" => hReq[ "path" ] } )
|
||||||
|
ENDCASE
|
||||||
|
|
||||||
|
// Assemble hResp from the buffered AP_* output.
|
||||||
|
RETURN { ;
|
||||||
|
"status" => ctx_get( "status", 200 ), ;
|
||||||
|
"headers" => ctx_get( "headers_out", { => } ), ;
|
||||||
|
"body" => _OUT_GET() ;
|
||||||
|
}
|
||||||
|
|
||||||
|
FUNCTION ApiHello()
|
||||||
|
AP_JSONRESPONSE( { ;
|
||||||
|
"ok" => .t., ;
|
||||||
|
"msg" => "hello from fivenode_go bridge layer", ;
|
||||||
|
"method" => AP_METHOD(), ;
|
||||||
|
"ip" => AP_USERIP() ;
|
||||||
|
} )
|
||||||
|
RETURN NIL
|
||||||
|
|
||||||
|
FUNCTION ApiEcho()
|
||||||
|
LOCAL cBody := AP_BODY()
|
||||||
|
LOCAL hPayload := IIF( Empty( cBody ), { => }, hb_jsonDecode( cBody ) )
|
||||||
|
AP_JSONRESPONSE( { ;
|
||||||
|
"ok" => .t., ;
|
||||||
|
"method" => AP_METHOD(), ;
|
||||||
|
"query" => AP_ARGS(), ;
|
||||||
|
"body_len" => Len( cBody ), ;
|
||||||
|
"body_parsed" => hPayload, ;
|
||||||
|
"user_agent" => hb_HGetDef( ctx_get( "headers_in", { => } ), "user-agent", "?" ) ;
|
||||||
|
} )
|
||||||
|
RETURN NIL
|
||||||
|
|
||||||
|
// Strip the ":port" portion of a Go RemoteAddr so AP_USERIP returns
|
||||||
|
// the bare IP the way mod_harbour does.
|
||||||
|
FUNCTION RemoteIPOnly( cAddr )
|
||||||
|
LOCAL n
|
||||||
|
IF Empty( cAddr )
|
||||||
|
RETURN ""
|
||||||
|
ENDIF
|
||||||
|
n := RAt( ":", cAddr )
|
||||||
|
IF n > 0
|
||||||
|
RETURN Left( cAddr, n - 1 )
|
||||||
|
ENDIF
|
||||||
|
RETURN cAddr
|
||||||
Reference in New Issue
Block a user