mod_harbour.v2.1 and RESTful API

mod_harbour is an Apache module that allows to run PRGs directly on the web !!!
Post Reply
User avatar
ssbbs
Posts: 139
Joined: Mon Oct 17, 2005 3:03 am
Location: Taiwan
Has thanked: 8 times
Been thanked: 17 times

mod_harbour.v2.1 and RESTful API

Post by ssbbs »

Using mod_harbour.v2.1 to implement a RESTful API call

  • Since mod_harbour.v2.1 does not support URLs like https://xxx.xxx.xxx/api.prg/user/1, you need to add this functionality manually.
  • Modify mod_harbour.v2.1/source/ap_func_c.c and add the AP_GETURI() function:

    Code: Select all | Expand

    //----------------------------------------------------------------//
    // mh_GetUri() cannot retrieve URLs like: http?://xxx.xxx/api.prg/user/1
    // So we add this function to get the full URI
    HB_FUNC(AP_GETURI)
    {
       hb_retc(GetRequestRec()->uri);
    }
    
    b. Because JWT is required, download the file from: https://github.com/matteobaccan/HarbourJwt — you only need the jwt.prg file.
    c. Modify ./mod_harbour.v2.1/libmhapache.hbp and add: \your_path\jwt.prg
    d. Modify ./mod_harbour.v2.1/source/externs.hbx and add: extern AP_GETURI
    e. Recompile to generate libmhapache.dll and mod_harbour.v2.so
  • Modify the Apache httpd.conf configuration:
    You need to set two environment variables:

    Code: Select all | Expand

        SetEnv JWT_SECRET "your_sha256"
        SetEnv JWT_REFRESH_SECRET "your_sha256"
    

JWT_SECRET: Access Token, used to validate access requests
JWT_REFRESH_SECRET: Refresh Token, used to obtain a new Access Token when the old one expires
If your website manages multiple RESTful APIs at the same time, you can separate them using <Directory> blocks, for example:

Code: Select all | Expand

<Directory "{DOCUMENTPATH}/api">
    SetEnv JWT_SECRET "...."
    SetEnv JWT_REFRESH_SECRET "...."
</Directory>

<Directory "{DOCUMENTPATH}/zzz">
    SetEnv JWT_SECRET "...."
    SetEnv JWT_REFRESH_SECRET "...."
</Directory>

To generate the SHA256 values, use the following command and paste the result into JWT_SECRET and JWT_REFRESH_SECRET:

Code: Select all | Expand

    openssl rand -base64 32

Next is the [Frontend] test webpage content:

Code: Select all | Expand

<!DOCTYPE html>
<html lang="zh-TW">
<head>
    <meta charset="UTF--8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>mod_harbour RESTful API 測試</title>
    <style>
        body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; line-height: 1.6; padding: 20px; max-width: 800px; margin: auto; }
        h1, h2 { border-bottom: 2px solid #eee; padding-bottom: 10px; }
        .container { display: flex; gap: 20px; }
        .controls, .output { flex: 1; min-width: 0; /* 解決 flex 項目被內容撐開的問題 */ }
        button { display: block; width: 100%; padding: 10px; margin-bottom: 10px; border: 1px solid #ccc; background-color: #f0f0f0; cursor: pointer; }
        button:hover { background-color: #e0e0e0; }
        input { width: calc(100% - 22px); padding: 10px; margin-bottom: 10px; }
        pre { background-color: #2d2d2d; color: #f1f1f1; padding: 15px; border-radius: 5px; white-space: pre; overflow-x: auto; }
        label { font-weight: bold; margin-bottom: 5px; display: block; }
    </style>
</head>
<body>

<h1>mod_harbour RESTful API 測試</h1>

<div class="container">
    <div class="controls">
        <h2>控制面板</h2>

        <button onclick="getAllUsers()">GET /users (取得所有使用者)</button>

        <hr>
        <label for="userId">使用者 ID:</label>
        <input type="text" id="userId" placeholder="輸入 ID (例如: 1)" value="1">
        <button onclick="getUserById()">GET /users/{id} (取得單一使用者)</button>
        <button onclick="deleteUser()">DELETE /users/{id} (刪除使用者)</button>

        <hr>
        <label for="userName">姓名:</label>
        <input type="text" id="userName" placeholder="輸入姓名">
        <label for="userEmail">Email:</label>
        <input type="text" id="userEmail" placeholder="輸入 Email">
        <button onclick="createUser()">POST /users (新增使用者)</button>
        <button onclick="updateUser()">PUT /users/{id} (更新使用者)</button>

        <hr>
        <h2>身分驗證 (JWT)</h2>
        <label for="loginUser">使用者名稱:</label>
        <input type="text" id="loginUser" value="admin">
        <label for="loginPass">密碼:</label>
        <input type="password" id="loginPass" value="password">
        <button onclick="login()">POST /login (登入並取得 Token)</button>

        <button onclick="getProfile()">GET /profile (測試保護路由)</button>
        <button onclick="logout()">登出 (清除 Token)</button>

    </div>
    <div class="output">
        <h2>輸出結果</h2>
        <pre id="response">點擊按鈕以發送請求...</pre>
    </div>
</div>

<script>
    // API 的基礎 URL,指向 .prg 原始碼檔案
    const API_BASE_URL = '/RESTful/api.prg';
    // 頁面載入時,嘗試從 localStorage 讀取 token
    let jwtToken = localStorage.getItem('jwtToken'); 

    // 如果一開始就有 token,可以提示使用者
    if (jwtToken) {
        console.log('偵測到已儲存的 Token,目前為登入狀態。');
    }

    const responseElement = document.getElementById('response');

    // 新增:嘗試刷新 Access Token 的函式
    async function tryRefreshToken() {
        try {
            // 注意:這裡不需要手動帶入 headers,因為瀏覽器會自動帶上 HttpOnly cookie
            const response = await fetch(API_BASE_URL + '/refresh_token', { method: 'POST' });

            if (!response.ok) {
                return false;
            }
            const data = await response.json();
            if (data.token) {
                jwtToken = data.token;
                localStorage.setItem('jwtToken', jwtToken);
                return true;
            }
            return false;
        } catch (error) {
            console.error('Error during token refresh:', error);
            return false;
        }
    }

    async function apiCall(method, path, body = null) {
        const url = API_BASE_URL + path;
        const options = {
            method: method,
            headers: {}
        };

        options.headers['Content-Type'] = 'application/json';
        if (jwtToken) {
            options.headers['Authorization'] = `Bearer ${jwtToken}`;
        };

        if (body) {
            options.body = JSON.stringify(body);
        }

        try {
            responseElement.textContent = `正在發送 ${method} 請求至 ${url} ...`;
            let response = await fetch(url, options);

            // 新增:如果收到 401 (Unauthorized),嘗試刷新 token 並重試
            if (response.status === 401 && path !== '/login' && path !== '/refresh_token') {
                console.log('Access Token 可能已過期,正在嘗試刷新...');
                const refreshSuccess = await tryRefreshToken();

                if (refreshSuccess) {
                    console.log('Token 刷新成功,正在重試原始請求...');
                    // 使用新的 token 更新 Authorization 標頭
                    options.headers['Authorization'] = `Bearer ${jwtToken}`;
                    // 重試請求
                    response = await fetch(url, options);
                } else {
                    console.log('Token 刷新失敗,請重新登入。');
                    alert('您的連線階段已過期,請重新登入。');
                    await logout(); // 清理前端和後端的登入狀態
                    return;
                }
            }

            let responseText = `狀態: ${response.status} ${response.statusText}\n\n`;
            
            // 處理沒有內容的回應 (例如 204 No Content)
            if (response.status === 204) {
                responseText += "操作成功,無返回內容。";
            } else {
                // 如果是登入成功,儲存新的 access token
                if (path === '/login' && response.ok) {
                    const data = await response.json();
                    localStorage.setItem('jwtToken', data.token); // 將 Token 儲存到 localStorage
                    jwtToken = data.token;
                    alert('登入成功!Access Token 已儲存。');
                    responseText += JSON.stringify(data, null, 2);
                    responseElement.textContent = responseText;
                    return; // 結束函式
                }

                const data = await response.json();
                responseText += JSON.stringify(data, null, 2);
            }
            
            responseElement.textContent = responseText;

        } catch (error) {
            responseElement.textContent = `發生錯誤:\n${error}`;
        }
    }

    function getAllUsers() {
        apiCall('GET', '/users');
    }

    function getUserById() {
        const id = document.getElementById('userId').value;
        if (!id) {
            alert('請輸入使用者 ID');
            return;
        }
        apiCall('GET', `/users/${id}`);
    }

    function createUser() {
        const name = document.getElementById('userName').value;
        const email = document.getElementById('userEmail').value;

        if (!name || !email) {
            alert('請輸入姓名和 Email');
            return;
        }

        const body = { name, email };
        apiCall('POST', '/users', body);
    }

    function updateUser() {
        const id = document.getElementById('userId').value;
        const name = document.getElementById('userName').value;
        const email = document.getElementById('userEmail').value;

        if (!id) {
            alert('請輸入要更新的使用者 ID');
            return;
        }

        if (!name && !email) {
            alert('請至少輸入姓名或 Email 來進行更新');
            return;
        }

        const body = {};
        if (name) body.name = name;
        if (email) body.email = email;

        apiCall('PUT', `/users/${id}`, body);
    }

    function deleteUser() {
        const id = document.getElementById('userId').value;
        if (!id) {
            alert('請輸入要刪除的使用者 ID');
            return;
        }
        if (confirm(`確定要刪除 ID 為 ${id} 的使用者嗎?`)) {
            apiCall('DELETE', `/users/${id}`);
        }
    }

    function login() {
        const username = document.getElementById('loginUser').value;
        const password = document.getElementById('loginPass').value;

        if (!username || !password) {
            alert('請輸入使用者名稱和密碼');
            return;
        }

        const body = { username, password };
        jwtToken = null; // 登入前先清除記憶體中的舊 token
        localStorage.removeItem('jwtToken'); // 同時清除 localStorage 中的舊 token
        apiCall('POST', '/login', body);
    }

    function getProfile() {
        if (!jwtToken) {
            alert('請先登入以取得 Token!');
            return;
        }
        apiCall('GET', '/profile');
    }

    async function logout() {
        // 先呼叫後端 API 來清除 HttpOnly cookie
        await apiCall('POST', '/logout');

        // 然後清理前端的 token
        jwtToken = null;
        localStorage.removeItem('jwtToken');
        alert('已登出成功。');
    }

</script>

</body>
</html>

Next is the [Backend] main program:

Code: Select all | Expand

<?prg
/*
 * api.prg (範例程式)
 * 這是一個使用 mod_harbour v2.1 開發的簡單 RESTful API 範例 (物件導向版本)。
 *
 * 這個版本將原來的程序化程式碼重構為一個名為 UserApi 的類別,
 * 使得程式碼結構更清晰、更易於維護和擴展。
 *
 * 主要功能:
 * - 管理 "users" 資源,提供 CRUD 操作。
 * - 使用 JWT (JSON Web Token) 進行身分驗證。
 * - 實現 Refresh Token 機制以延長使用者登入狀態。
 * - 採用物件導向設計,將所有相關邏輯封裝在 UserApi 類別中。
 */
#include "hbclass.ch"
// --- 主程序 ---
PROCEDURE Main()

   // 取得當前請求的 HTTP 方法 (GET, POST, PUT, DELETE)。
   LOCAL cMethod  := AP_Method()
   // 使用我們在 C 原始碼中新增的 AP_GetUri() 函式
   LOCAL hHeaders := AP_HeadersIn()
   LOCAL cURI     := AP_GetUri() // 請求的 URL 路徑,例如 /RESTful/api.prg/users/1
   LOCAL cBody    := AP_GetBody() // 預先讀取 body,避免在多個方法中重複讀取
   LOCAL oApi

   // 建立 API 物件並傳入請求資訊
   oApi := UserApi():New( cMethod, cURI, hHeaders, cBody )

   // 執行請求處理
   oApi:HandleRequest()

RETURN

// --- API 類別定義 ---
CLASS UserApi

   // --- 資料成員 (Properties) ---
   // 模擬資料庫
   DATA aUsers          INIT {}
   // 檔案快取時間戳
   DATA tsLastModTime     INIT NIL

   // JWT 密鑰
   DATA cJwtSecret        INIT ""
   DATA cJwtRefreshSecret INIT ""

   // 請求相關資料
   DATA cMethod         INIT ""
   DATA cURI            INIT ""
   DATA hHeaders        INIT NIL
   DATA cBody           INIT ""

   // --- 方法 (Methods) ---
   METHOD New( cMethod, cURI, hHeaders, cBody )
   METHOD HandleRequest()

   // API 端點方法
   METHOD GetAllUsers()
   METHOD GetUser( nID )
   METHOD CreateUser()
   METHOD UpdateUser( nID )
   METHOD DeleteUser( nID )
   METHOD Login()
   METHOD GetProfile()
   METHOD RefreshToken()
   METHOD Logout()

   // 輔助方法
   METHOD VerifyToken() PROTECTED
   METHOD SendError( nStatusCode, cMessage ) PROTECTED
   METHOD FindUser( nID ) PROTECTED
   METHOD FindUserIndex( nID ) PROTECTED
   METHOD GetNextID() PROTECTED
   METHOD LoadData() PROTECTED
   METHOD SaveData() PROTECTED
   METHOD InitData() PROTECTED
   METHOD GetCookieValue( cCookieHeader, cCookieName ) PROTECTED

ENDCLASS
// --- 類別實作 ---
....
....
?>

Important!! Please reconfigure your own Access Token and Refresh Token lifecycle!!

You can view full code from:https://www4.zzz.com.tw/phpBB3/viewtopic.php?f=2&t=370

line ID: ssbbstw
WeChat ID: ssbbstw
russimicro
Posts: 413
Joined: Sun Jan 31, 2010 3:30 pm
Location: Bucaramanga - Colombia
Been thanked: 5 times

Re: mod_harbour.v2.1 and RESTful API

Post by russimicro »

Good evening..

could you show us how to compile with msvc64 2019 or 2022.. and the link to download the version of Harbour according to msvc64..

JONSSON RUSSI

User avatar
ssbbs
Posts: 139
Joined: Mon Oct 17, 2005 3:03 am
Location: Taiwan
Has thanked: 8 times
Been thanked: 17 times

Re: mod_harbour.v2.1 and RESTful API

Post by ssbbs »

I am using MSVC64 with VS Studio 2019
Method to compile mod_harbour.v2.1:
1.Download the project from GitHub
2.Run the file inside named go64.bat

After execution, it will generate:
libmhapache.dll
mod_harbour.v2.so

russimicro wrote: Fri Feb 06, 2026 3:39 am

could you show us how to compile with msvc64 2019 or 2022.. and the link to download the version of Harbour according to msvc64..

line ID: ssbbstw
WeChat ID: ssbbstw
russimicro
Posts: 413
Joined: Sun Jan 31, 2010 3:30 pm
Location: Bucaramanga - Colombia
Been thanked: 5 times

Re: mod_harbour.v2.1 and RESTful API

Post by russimicro »

Good evening..
I managed to compile partially... it gives me these errors...
function CURL_EASY_ER_BUFF_GET() ; return function CURL_MULTI_ADD_HANDLE() ; return function CURL_MULTI_CLEANUP() ; return function CURL_MULTI_INFO_READ() ; return function CURL_MULTI_INIT() ; return function CURL_MULTI_PERFORM() ; return function CURL_MULTI_POLL() ; return function CURL_MULTI_REMOVE_HANDLE() ; return function CURL_WS_RECV() ; return function CURL_WS_SEND() ; return function BIO_GET_CONN_ADDRESS() ; return function SSL_CTX_USE_RSAPRIVATEKEY() ; return ...

I defined them ... to bypass the error.

But the libmhapache.dll generates an error...
Is it possible for you to share the mod_harbour and the Harbour version you use..

Thanks

User avatar
ssbbs
Posts: 139
Joined: Mon Oct 17, 2005 3:03 am
Location: Taiwan
Has thanked: 8 times
Been thanked: 17 times

Re: mod_harbour.v2.1 and RESTful API

Post by ssbbs »

disable:
#{allmsvc}hbcurl.hbc

add:
#[hbcurl]
{msvc64}-lhbcurl

old libcurl is:
{msvc64}-llibcurl-x64

my libcurl is:
#[curl]
{msvc64}-llibcurl

line ID: ssbbstw
WeChat ID: ssbbstw
russimicro
Posts: 413
Joined: Sun Jan 31, 2010 3:30 pm
Location: Bucaramanga - Colombia
Been thanked: 5 times

Re: mod_harbour.v2.1 and RESTful API

Post by russimicro »

-hbdynvm
{msvc64}-ooutput/win64/libmhapache
{msvc}-ooutput/win32/libmhapache
{gcc}-o./output/linux/libmhapache.so
{linux|darwin}-static

-mt
-trace
-info
-rebuild

-l-hbfship
-l-hbxpp

{msvc}-llibssl-1_1
{msvc}-llibcrypto-1_1
{msvc64}-llibcrypto-1_1-x64
{msvc64}-llibssl-1_1-x64
#{msvc64}-llibcurl-x64
{msvc}-llibcurl
{msvc64}-lhbcurl
{msvc64}-llibcurl

{allmsvc}-lapr-1
{allmsvc}-laprutil-1
{allmsvc}-llibapr-1
{allmsvc}-llibaprutil-1
{allmsvc}-llibhttpd

-Ld:\xampp\apache\lib

-id:\xampp\apache\include
source/mh_apache.prg
source/ap_func.prg
source/ap_func_c.c
source/cookies.prg
source/legacy_func.prg
source/legacy_func_c.c
source/main.prg
source/persistence.prg
source/prepro.prg
source/preproPHP.prg
source/trace.prg
source/errorsys.prg
source/sessions.prg
source/jwt.prg

{allmsvc}-cflag=-Id:\xampp\apache\include
{gcc}-cflag=-I/usr/include/apache2
{gcc}-cflag+=-I/usr/include/apr-1.0
{allmsvc}-id:\xampp\apache\include
{darwin}-cflag=-I/usr/include/apache2
{darwin}-cflag+=-I/usr/include/apr-1.0
{darwin}-cflag+=-I/usr/local/Cellar/httpd/2.4.47/include/httpd
{darwin}-cflag+=-I/usr/local/Cellar/apr-util/1.6.1_3/libexec/include/apr-1
{darwin}-cflag+=-I/usr/local/Cellar/apr/1.7.0_2/libexec/include/apr-1

{allmsvc}hbwin.hbc
{allmsvc}hbhpdf.hbc
{allmsvc}xhb.hbc
{allmsvc}hbct.hbc
#{allmsvc}hbcurl.hbc
{allmsvc}hbmzip.hbc
{allmsvc}hbziparc.hbc
{allmsvc}hbmemio.hbc
{allmsvc}hbnetio.hbc
{allmsvc}hbssl.hbc
{allmsvc}hbmisc.hbc

{gcc}-dflag=@libs.txt
{darwin}-dflag=@libs_osx.txt

{darwin}-FCoreFoundation
{darwin}-FSecurity

//********************************************

These lib...
libcrypto-1_1-x64.lib
libssl-1_1-x64.lib
it can't find them...
I took them from C:harbour_binaries-masterWindowsharbour_windows_64.ziplibwinmsvc64
and these I downloaded from the web...
ssleay32.lib
libeay32.lib

//*************************************************

error :
libhpdf.lib(hpdfpdfa.obj) : error LNK2019: s¡mbolo externo impctime64 sin resolver al que se hace referencia en la funci¢n HPDF_PDFA_GenerateID
libhpdf.lib(hpdfpdfa.obj) : error LNK2019: s¡mbolo externo imptime64 sin resolver al que se hace referencia en la funci¢n HPDF_PDFA_GenerateID
...
...
...

It seems like it might be the Harbour version for msv64.. could you share it..?

russimicro
Posts: 413
Joined: Sun Jan 31, 2010 3:30 pm
Location: Bucaramanga - Colombia
Been thanked: 5 times

Re: mod_harbour.v2.1 and RESTful API

Post by russimicro »

I also need to include the TDOLPHIN and HBZEBRA libraries

User avatar
ssbbs
Posts: 139
Joined: Mon Oct 17, 2005 3:03 am
Location: Taiwan
Has thanked: 8 times
Been thanked: 17 times

Re: mod_harbour.v2.1 and RESTful API

Post by ssbbs »

about PDF:
I only use:
{allmsvc}hbhpdf.hbc
no need "libhpdf.lib"

about SSL:
ssleay32.lib
libeay32.lib
is openssl 1.0.2(u)
I'm use v3.4.1.

What library is need about you?

line ID: ssbbstw
WeChat ID: ssbbstw
russimicro
Posts: 413
Joined: Sun Jan 31, 2010 3:30 pm
Location: Bucaramanga - Colombia
Been thanked: 5 times

Re: mod_harbour.v2.1 and RESTful API

Post by russimicro »

Good evening... S

ince I am starting with mod_harbour, could you provide the mod_harbour and the harbour you use... and if they are the latest versions, even better. I also need to be able to include the dolphin library.

Thanks

JONSSON RUSSI

User avatar
ssbbs
Posts: 139
Joined: Mon Oct 17, 2005 3:03 am
Location: Taiwan
Has thanked: 8 times
Been thanked: 17 times

Re: mod_harbour.v2.1 and RESTful API

Post by ssbbs »

You’d better learn to compile on your own, or it’ll be a hassle when you need to fix issues!
If you run into any issues, feel free to bring them up for discussion.

russimicro wrote: Sun Feb 08, 2026 1:08 am

Good evening... S

ince I am starting with mod_harbour, could you provide the mod_harbour and the harbour you use... and if they are the latest versions, even better. I also need to be able to include the dolphin library.

line ID: ssbbstw
WeChat ID: ssbbstw
Post Reply