家用路由器也能充當Web伺服器?路由器外掛開發心得

閔立發表於2021-12-28
  • 起因

  最近剛剛結束考研,開始有時間寫文章了。在複習的時候中,經常忍不住折騰各種東西,於是有一天看中了我手上的華為路由器。什麼?華為路由器,你可能有這樣的疑問,華為路由器不是自研的晶片嗎,就像我手上這臺華為路由器,是華為自己研發的凌霄晶片,沒有對外開放,怎麼折騰呢?於是就有了以下的研究歷程。

  • 折騰什麼

  首先,能夠折騰什麼呢?就像我手上的樹莓派一樣,刷個OpenWrt系統輕而易舉。可能有些人會有一些疑問,OpenWrt系統是什麼?其實這就是一個開源的路由器作業系統,很多路由器的系統都是在此基礎上進行開發的,這個系統的可玩性很高。但是華為路由器連韌體下載都沒有開放,折騰系統這條路就不太合適了。開發路由器外掛呢?貌似可行,但此時我只知道路由器外掛只能在華為路由器專用的市場上安裝,而且路由器買了幾年了,也就那麼幾個外掛,主要都是IOT家電控制類的應用,但是這條路理論上可行,於是決定折騰路由器外掛開發。

  • 申請Debug版本韌體

  目前華為路由器只要是有外掛應用市場的,理論上都支援路由器外掛開發,其它品牌的路由器很多也是支援的,不過每種路由器開發的方式都不一樣,可以參考官方提供的文件。目前我手上只有華為的路由器,型號是榮耀路由Pro2,這是幾年前的一個路由器,已經都下市了,韌體也不更新了,通過華為官網的文件,我傳送路由器序列號給華為聯絡郵箱,等待路由器適配完成,更新一下韌體,就轉到了Debug版本。

  • 瞭解外掛系統

  華為路由器執行了OpenEE開發平臺,外掛就是在此基礎上進行開發,同時路由器硬體通過OSGI介面對外提供呼叫能力,外掛執行在JVM上。JVM?沒錯,就是我們Java程式設計師喜歡的JVM。Debug版本可以直接用root使用者登入到路由器執行的後臺,基本Linux的命令都是支援的。然後我找到了路由器上的JVM研究了一下,其實就是研究了一下rt.jar的原始碼,這個JVM是極度精簡的版本,很多和路由器執行無關類都去掉了,並且加了很多華為自己寫的類,不過我們編寫程式最常用的類還是沒有精簡的。

  外掛開發分為前端和後端,後端可以基於JVM開發API介面供前端呼叫,前端可以直接使用HTML等任意前端技術進行開發,不過需要呼叫後端的API只能使用特定的函式,最後上傳開發好的應用到路由器即可執行,同時應用也可以在路由器市場直接開啟執行、解除安裝。

家用路由器也能充當Web伺服器?路由器外掛開發心得
                      外掛系統原理圖
  • 跑通Demo

  可以根據官方文件進行操作,在這裡我就不貼出連結了,大家如果有開發的需求,可以直接在華為開發者官網去搜尋路由器開發文件即可,也可以和我討論。首先,需要準備開發環境,JDK1.8、Maven基本就夠了,然後執行官方指令碼向Maven本地庫匯入幾個華為自己的Jar包即可。

  Demo專案是Maven型別的專案,熟悉Java開發的應該很熟悉了,可以用自己喜歡的軟體進行開發,比如我就喜歡使用idea進行開發。執行mvn install,就生成好了對應的Jar包,然後通過官方提供的指令碼打包成Apk檔案,沒錯,就是Apk檔案,不過不是安卓上的Apk,而是華為路由器對應的Apk檔案,然後官方還提供了上傳應用的工具,直接上傳即可。

家用路由器也能充當Web伺服器?路由器外掛開發心得
                      外掛上傳工具

  就這樣,一個Hello Word應用就跑到路由器上了。只不過官方提供的Demo專案沒有前端,只能在後臺控制檯上檢視對應的輸出。如果需要開發前端,需要將對應的前端檔案上傳到公網伺服器上,通過IP進行呼叫。

  • 實現路由器上跑Web伺服器

  Demo應用跑通了,接下來準備做些什麼了。既然路由器執行著JVM,那麼跑Web應用應該是沒什麼問題的,而且我這個路由器還有512M的記憶體,低負載的Web應用應該沒有問題。這個基礎上,我們能夠做我們想做的任何事情,比如做個NAS伺服器,當內部部落格伺服器等等,當然如果你有公網條件,也可當小型部落格伺服器使用,這裡只討論內網應用。JDK1.8本來內建一個簡單的HttpServer類,可惜路由器JVM把這個類精簡了,於是我編寫了以下的類檔案。

package ml.minli.tool.util;

import javax.activation.MimetypesFileTypeMap;
import java.io.*;
import java.net.*;

public class HttpServer extends Thread {

    private final int port;

    private ServerSocket serverSocket;

    private static final MimetypesFileTypeMap mimetypesFileTypeMap = new MimetypesFileTypeMap();

    public HttpServer(int port) {
        this.port = port;
    }

    @Override
    public void run() {
        try {
            serverSocket = new ServerSocket(port);
            while (true) {
                Socket socket = serverSocket.accept();
                HttpRequestHandler httpRequestHandler = new HttpRequestHandler(socket);
                httpRequestHandler.handle();
                socket.close();
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (serverSocket != null && !serverSocket.isClosed()) {
                try {
                    serverSocket.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }

    private static class HttpRequestHandler {

        private final Socket socket;

        public HttpRequestHandler(Socket socket) {
            this.socket = socket;
        }

        public void handle() {
            try {
                StringBuilder stringBuilder = new StringBuilder();
                InputStreamReader inputStreamReader = new InputStreamReader(socket.getInputStream());
                char[] chars = new char[1024];
                int mark;
                while ((mark = inputStreamReader.read(chars)) != -1) {
                    stringBuilder.append(chars, 0, mark);
                    if (mark < chars.length) {
                        break;
                    }
                }
                if (stringBuilder.length() == 0) {
                    return;
                }
                //擷取每行請求
                String[] lines = stringBuilder.toString().split("\r\n");
                if (!lines[0].isEmpty()) {
                    //擷取URL
                    String[] infos = lines[0].split(" ");
                    String info = URLDecoder.decode(infos[1], "UTF-8");
                    File file;
                    if (info.equals("/")) {
                        file = new File(USBInfo.usbPath + "/index.html");
                    } else {
                        file = new File(USBInfo.usbPath + info);
                    }
                    //檔案不存在返回404
                    if (!file.exists()) {
                        socket.getOutputStream().write(("HTTP/1.1 404 Not Found\r\n" +
                                "Content-Type: text/html; charset=utf-8\r\n" +
                                "\r\n").getBytes());
                        return;
                    }
                    String contentType = mimetypesFileTypeMap.getContentType(file);
                    socket.getOutputStream().write(("HTTP/1.1 200 OK\r\n" +
                            "Content-Type: " + contentType + "; charset=utf-8\r\n" +
                            "\r\n").getBytes());
                    FileInputStream fileInputStream = new FileInputStream(file);
                    byte[] bytes = new byte[1024];
                    int length;
                    while ((length = fileInputStream.read(bytes)) != -1) {
                        socket.getOutputStream().write(bytes, 0, length);
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                if (socket != null && !socket.isClosed()) {
                    try {
                        socket.close();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

}

  由於我只準備先實現靜態網頁伺服器的解析,於是準備這樣實現:U盤裡面存放前端檔案,比如HTML、CSS、JS等,然後伺服器解析檔案返回,所以這就簡單多了。只需要拿到請求進行解析就行了,不過返回需要合適的Content-Type,這個就需要對檔案型別進行判斷了,於是用到了javax.activation這個包,本來這個包也是JDK1.8自帶的,可惜,路由器JVM裡面精簡了。不過可以通過Maven外掛將檔案打包進去。

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.felix</groupId>
                <artifactId>maven-bundle-plugin</artifactId>
                <version>5.1.2</version>
                <extensions>true</extensions>
                <configuration>
                    <archive>
                        <addMavenDescriptor>false</addMavenDescriptor>
                    </archive>
                    <instructions>
                        <bundleName>{project.name}</bundleName>
                        <bundleDescription>{project.description}</bundleDescription>
                        <bundleVendor>minli</bundleVendor>
                        <Bundle-Activator>ml.minli.tool.Activator</Bundle-Activator>
                        <Service-Component>OSGI-INF/USBInfo.xml</Service-Component>
                        <Embed-Dependency>*;scope=compile|runtime;inline=false</Embed-Dependency>
                    </instructions>
                </configuration>
            </plugin>
        </plugins>
    </build>

  最後,在啟動的例項類裡面呼叫即可。這裡使用22222埠進行測試,值得注意的是,一些埠被路由器本身佔用了,所以我們只能使用其它埠。

package ml.minli.tool;

import ml.minli.tool.util.HttpServer;
import org.osgi.framework.BundleActivator;
import org.osgi.framework.BundleContext;

public class Activator implements BundleActivator {

    public Activator() {
    }

    @Override
    public void start(BundleContext bundleContext) {
        new HttpServer(22222).start();
    }

    @Override
    public void stop(BundleContext bundleContext) {
    }

}

  同時,我們還要注意許可權的問題,很多地方,沒有許可權程式是會拋異常的。許可權配置如下所示。

(org.osgi.framework.PackagePermission "*" "import")
(java.util.logging.LoggingPermission "control")
(org.osgi.framework.ServicePermission "com.huawei.hilink.rest.RESTResource" "register")
(java.io.FilePermission "/mnt/-" "read")
(com.huawei.hilink.coreapi.perm.USBPermission "*" "list")
(org.osgi.framework.ServicePermission "com.huawei.hilink.openapi.usbstorage.USBStorage" "get")
(java.net.SocketPermission "*" "accept,connect,listen,resolve")
(java.util.PropertyPermission "*" "read")

  最終,成功實現了路由器跑Web應用的功能,可以執行任意的網頁,同時如果是一些普通的檔案,通過URL訪問相當於是下載,所以做個簡單的NAS伺服器好像很容易。

  • 後續

  折騰到這樣,當時就暫時告一段落了,因為那個時候還在準備考研,到現在考研結束才整理寫下了這些內容,不過現在我又可以折騰了,看看有什麼應用可以做的,如果有進展,我會繼續分享的。

 

相關文章