AHKhttp - HTTP Server

Post your working scripts, libraries and tools for AHK v1.1 and older
Skittlez
Posts: 11
Joined: 13 Mar 2014, 12:16

AHKhttp - HTTP Server

16 Oct 2014, 21:02

Basic http server I wrote, requires AHKsock.

Example

Code: Select all

#Persistent
#SingleInstance, force
SetBatchLines, -1

paths := {}
paths["/"] := Func("HelloWorld")
paths["404"] := Func("NotFound")
paths["/logo"] := Func("Logo")

server := new HttpServer()
server.LoadMimes(A_ScriptDir . "/mime.types")
server.SetPaths(paths)
server.Serve(8000)
return

Logo(ByRef req, ByRef res, ByRef server) {
    server.ServeFile(res, A_ScriptDir . "/logo.png")
    res.status := 200
}

NotFound(ByRef req, ByRef res) {
    res.SetBodyText("Page not found")
}

HelloWorld(ByRef req, ByRef res) {
    res.SetBodyText("Hello World")
    res.status := 200
}

#include, %A_ScriptDir%\AHKhttp.ahk
#include <AHKsock>
Source

Code: Select all

class Uri
{
    Decode(str) {
        Loop
            If RegExMatch(str, "i)(?<=%)[\da-f]{1,2}", hex)
                StringReplace, str, str, `%%hex%, % Chr("0x" . hex), All
            Else Break
        Return, str
    }

    Encode(str) {
        f = %A_FormatInteger%
        SetFormat, Integer, Hex
        If RegExMatch(str, "^\w+:/{0,2}", pr)
            StringTrimLeft, str, str, StrLen(pr)
        StringReplace, str, str, `%, `%25, All
        Loop
            If RegExMatch(str, "i)[^\w\.~%]", char)
                StringReplace, str, str, %char%, % "%" . Asc(char), All
            Else Break
        SetFormat, Integer, %f%
        Return, pr . str
    }
}

class HttpServer
{
    static servers := {}

    LoadMimes(file) {
        if (!FileExist(file))
            return false

        FileRead, data, % file
        types := StrSplit(data, "`n")
        this.mimes := {}
        for i, data in types {
            info := StrSplit(data, " ")
            type := info.Remove(1)
            ; Seperates type of content and file types
            info := StrSplit(LTrim(SubStr(data, StrLen(type) + 1)), " ")

            for i, ext in info {
                this.mimes[ext] := type
            }
        }
        return true
    }

    GetMimeType(file) {
        default := "text/plain"
        if (!this.mimes)
            return default

        SplitPath, file,,, ext
        type := this.mimes[ext]
        if (!type)
            return default
        return type
    }

    ServeFile(ByRef response, file) {
        f := FileOpen(file, "r")
        length := f.RawRead(data, f.Length)
        f.Close()

        response.SetBody(data, length)
        res.headers["Content-Type"] := this.GetMimeType(file)
    }

    SetPaths(paths) {
        this.paths := paths
    }

    Handle(ByRef request) {
        response := new HttpResponse()
        if (!this.paths[request.path]) {
            func := this.paths["404"]
            response.status := 404
            if (func)
                func.(request, response, this)
            return response
        } else {
            this.paths[request.path].(request, response, this)
        }
        return response
    }

    Serve(port) {
        this.port := port
        HttpServer.servers[port] := this

        AHKsock_Listen(port, "HttpHandler")
    }
}

HttpHandler(sEvent, iSocket = 0, sName = 0, sAddr = 0, sPort = 0, ByRef bData = 0, bDataLength = 0) {
    static sockets := {}

    if (!sockets[iSocket]) {
        sockets[iSocket] := new Socket(iSocket)
        AHKsock_SockOpt(iSocket, "SO_KEEPALIVE", true)
    }
    socket := sockets[iSocket]

    if (sEvent == "DISCONNECTED") {
        socket.request := false
        sockets[iSocket] := false
    } else if (sEvent == "SEND") {
        if (socket.TrySend()) {
            socket.Close()
        }

    } else if (sEvent == "RECEIVED") {
        server := HttpServer.servers[sPort]

        text := StrGet(&bData, "UTF-8")
        request := new HttpRequest(text)

        ; Multipart request
        if (request.IsMultipart()) {
            length := request.headers["Content-Length"]
            request.bytesLeft := length + 0

            if (request.body) {
                request.bytesLeft -= StrLen(request.body)
            }
            socket.request := request
        } else if (socket.request) {
            ; Get data and append it to the request body
            socket.request.bytesLeft -= StrLen(text)
            socket.request.body := socket.request.body . text
        }

        if (socket.request) {
            request := socket.request
            if (request.bytesLeft <= 0) {
                request.done := true
            }
        }

        response := server.Handle(request)
        if (response.status) {
            socket.SetData(response.Generate())

            if (socket.TrySend()) {
                if (!request.IsMultipart() || (request.IsMultipart() && request.done)) {
                    socket.Close()
                }
            }
        }
    }
}

class HttpRequest
{
    __New(data = "") {
        if (data)
            this.Parse(data)
    }

    GetPathInfo(top) {
        results := []
        while (pos := InStr(top, " ")) {
            results.Insert(SubStr(top, 1, pos - 1))
            top := SubStr(top, pos + 1)
        }
        this.method := results[1]
        this.path := Uri.Decode(results[2])
        this.protocol := top
    }

    GetQuery() {
        pos := InStr(this.path, "?")
        query := StrSplit(SubStr(this.path, pos + 1), "&")
        if (pos)
            this.path := SubStr(this.path, 1, pos - 1)

        this.queries := {}
        for i, value in query {
            pos := InStr(value, "=")
            key := SubStr(value, 1, pos - 1)
            val := SubStr(value, pos + 1)
            this.queries[key] := val
        }
    }

    Parse(data) {
        this.raw := data
        data := StrSplit(data, "`n`r")
        headers := StrSplit(data[1], "`n")
        this.body := LTrim(data[2], "`n")

        this.GetPathInfo(headers.Remove(1))
        this.GetQuery()
        this.headers := {}

        for i, line in headers {
            pos := InStr(line, ":")
            key := SubStr(line, 1, pos - 1)
            val := Trim(SubStr(line, pos + 1), "`n`r ")

            this.headers[key] := val
        }
    }

    IsMultipart() {
        length := this.headers["Content-Length"]
        expect := this.headers["Expect"]

        if (expect = "100-continue" && length > 0)
            return true
        return false
    }
}

class HttpResponse
{
    __New() {
        this.headers := {}
        this.status := 0
        this.protocol := "HTTP/1.1"

        this.SetBodyText("")
    }

    Generate() {
        FormatTime, date,, ddd, d MMM yyyy HH:mm:ss
        this.headers["Date"] := date

        headers := this.protocol . " " . this.status . "`n"
        for key, value in this.headers {
            headers := headers . key . ": " . value . "`n"
        }
        headers := headers . "`n"
        length := this.headers["Content-Length"]

        buffer := new Buffer((StrLen(headers) * 2) + length)
        buffer.WriteStr(headers)

        buffer.Append(this.body)
        buffer.Done()

        return buffer
    }

    SetBody(ByRef body, length) {
        this.body := new Buffer(length)
        this.body.Write(&body, length)
        this.headers["Content-Length"] := length
    }

    SetBodyText(text) {
        this.body := Buffer.FromString(text)
        this.headers["Content-Length"] := this.body.length
    }


}

class Socket
{
    __New(socket) {
        this.socket := socket
    }

    Close(timeout = 5000) {
        AHKsock_Close(this.socket, timeout)
    }

    SetData(data) {
        this.data := data
    }

    TrySend() {
        if (!this.data || this.data == "")
            return false

        p := this.data.GetPointer()
        length := this.data.length

        this.dataSent := 0
        loop {
            if ((i := AHKsock_Send(this.socket, p, length - this.dataSent)) < 0) {
                if (i == -2) {
                    return
                } else {
                    ; Failed to send
                    return
                }
            }

            if (i < length - this.dataSent) {
                this.dataSent += i
            } else {
                break
            }
        }
        this.dataSent := 0
        this.data := ""

        return true
    }
}

class Buffer
{
    __New(len) {
        this.SetCapacity("buffer", len)
        this.length := 0
    }

    FromString(str, encoding = "UTF-8") {
        length := Buffer.GetStrSize(str, encoding)
        buffer := new Buffer(length)
        buffer.WriteStr(str)
        return buffer
    }

    GetStrSize(str, encoding = "UTF-8") {
        encodingSize := ((encoding="utf-16" || encoding="cp1200") ? 2 : 1)
        ; length of string, minus null char
        return StrPut(str, encoding) * encodingSize - encodingSize
    }

    WriteStr(str, encoding = "UTF-8") {
        length := this.GetStrSize(str, encoding)
        VarSetCapacity(text, length)
        StrPut(str, &text, encoding)

        this.Write(&text, length)
        return length
    }

    ; data is a pointer to the data
    Write(data, length) {
        p := this.GetPointer()
        DllCall("RtlMoveMemory", "uint", p + this.length, "uint", data, "uint", length)
        this.length += length
    }

    Append(ByRef buffer) {
        destP := this.GetPointer()
        sourceP := buffer.GetPointer()

        DllCall("RtlMoveMemory", "uint", destP + this.length, "uint", sourceP, "uint", buffer.length)
        this.length += buffer.length
    }

    GetPointer() {
        return this.GetAddress("buffer")
    }

    Done() {
        this.SetCapacity("buffer", this.length)
    }
}
GitHub
Documentation
Last edited by Skittlez on 08 Nov 2014, 18:53, edited 2 times in total.
User avatar
joedf
Posts: 8937
Joined: 29 Sep 2013, 17:08
Location: Canada
Contact:

Re: AHKhttp - HTTP Server

16 Oct 2014, 23:27

Interesting
Image Image Image Image Image
Windows 10 x64 Professional, Intel i5-8500, NVIDIA GTX 1060 6GB, 2x16GB Kingston FURY Beast - DDR4 3200 MHz | [About Me] | [About the AHK Foundation] | [Courses on AutoHotkey]
[ASPDM - StdLib Distribution] | [Qonsole - Quake-like console emulator] | [LibCon - Autohotkey Console Library]
User avatar
fincs
Posts: 527
Joined: 30 Sep 2013, 14:17
Location: Seville, Spain
Contact:

Re: AHKhttp - HTTP Server

17 Oct 2014, 03:24

AutoHotkey on Rails anybody? This little script even supports route-based serving instead of using a folder in disk.
fincs
Windows 11 Pro (Version 22H2) | AMD Ryzen 7 3700X with 32 GB of RAM | AutoHotkey v2.0.0 + v1.1.36.02
Get SciTE4AutoHotkey v3.1.0 - [My project list]
User avatar
joedf
Posts: 8937
Joined: 29 Sep 2013, 17:08
Location: Canada
Contact:

Re: AHKhttp - HTTP Server

17 Oct 2014, 13:57

Haha true, along with AHK-webkit ;)
Image Image Image Image Image
Windows 10 x64 Professional, Intel i5-8500, NVIDIA GTX 1060 6GB, 2x16GB Kingston FURY Beast - DDR4 3200 MHz | [About Me] | [About the AHK Foundation] | [Courses on AutoHotkey]
[ASPDM - StdLib Distribution] | [Qonsole - Quake-like console emulator] | [LibCon - Autohotkey Console Library]
User avatar
Relayer
Posts: 160
Joined: 30 Sep 2013, 13:09
Location: Delaware, USA

Re: AHKhttp - HTTP Server

18 Oct 2014, 08:47

This looks very interesting but I would like to ask someone to explain a little more how one would use this. It would be very helpful.

Relayer
ahk7
Posts: 572
Joined: 06 Nov 2013, 16:35

Re: AHKhttp - HTTP Server

18 Oct 2014, 10:42

@Skittlez: do you plan to work on this some more and extend it?

Some documentation would indeed be useful, I've been playing around with a bit - see below.

You can find AHKsock here https://github.com/jleb/AHKsock

Here is a small example displaying parameters and using them in a form. Enter two numbers and press submit will show you the answer on the next page.

Code: Select all

#Persistent
#SingleInstance, force
SetBatchLines, -1

ahp=
(
<html>
<title>AHKhttp-server 1.0</title>
<body>
<b>[var]</b>
</body>
</html>
)

paths := {}
paths["/"] := Func("PlayList")
paths["/page"] := Func("Page")
paths["/calc"] := Func("Calc")
paths["404"] := Func("NotFound")

server := new HttpServer()
server.SetPaths(paths)
server.Serve(8000)
Run http://localhost:8000/page?para1=123&para2=456
return

NotFound(ByRef req, ByRef res) {
    res.SetBody("Page not found")
}

Page(ByRef req, ByRef res) {
	global ahp
	form=
	(
<form action="/calc" method="get">
<input type=text name=para1> 
+ 
<input type=text name=para2>
<input type=submit>
</form>
	)
	stringreplace, serve, ahp, [var], % "para1: " req.queries["para1"] " - para2: " req.queries["para2"] "<br><br>" form, All
    res.SetBody(serve)
    res.statusCode := 200
}

Calc(ByRef req, ByRef res) {
	global ahp
	answer:=req.queries["para1"] + req.queries["para2"]
	stringreplace, serve, ahp, [var], % req.queries["para1"] "+" req.queries["para2"] "=" answer, All
    res.SetBody(serve)
    res.statusCode := 200
}

HelloWorld(ByRef req, ByRef res) {
    res.SetBody("Hello World")
    res.statusCode := 200
}

f12::Reload
	
#include, %A_ScriptDir%\AHKhttp.ahk
#include AHKsock.ahk
Skittlez
Posts: 11
Joined: 13 Mar 2014, 12:16

Re: AHKhttp - HTTP Server

18 Oct 2014, 11:30

I'll write some documentation later today.
@ahk7 If you have any features you'd like to see, just let me know and I'll see what I can do.
guest3456
Posts: 3453
Joined: 09 Oct 2013, 10:31

Re: AHKhttp - HTTP Server

18 Oct 2014, 14:40

awesome

User avatar
Relayer
Posts: 160
Joined: 30 Sep 2013, 13:09
Location: Delaware, USA

Re: AHKhttp - HTTP Server

19 Oct 2014, 08:51

ahk7,

I tried your script and get "unable to connect". I'm using Firefox and AutoHotKey v1.1.16.05

I'm a complete noob when it comes to http server stuff.

Relayer
ahk7
Posts: 572
Joined: 06 Nov 2013, 16:35

Re: AHKhttp - HTTP Server

19 Oct 2014, 12:13

if you google 'unable to connect localhost firefox' you will see that it might be a firefox setting so try it with another browser to see if that works - if it does you can try to figure out how to let firefox allow connections to localhost.
You can test if the server is up and running by opening a command window (cmd.exe) and use ping localhost if it gives valid pings the script (server) works - so you will have to work on getting it to work in your browser. (Edit see comments by Lexikos below)

I'd like to be able to display images in pages and stream mp3 over http - I recall sparrow did that by reading/parsing a list of mimetypes.

Edit: ping but note the test script does a run Run http://localhost:8000/page?para1=123&para2=456 so the default browser should try to open http://localhost:8000 - it may be a firefox problem so worth trying in another browser just to check. Otherwise I don't know why it doesn't work or how to fix it.
Last edited by ahk7 on 21 Oct 2014, 11:20, edited 1 time in total.
guest3456
Posts: 3453
Joined: 09 Oct 2013, 10:31

Re: AHKhttp - HTTP Server

20 Oct 2014, 17:17

Relayer wrote:ahk7,

I tried your script and get "unable to connect". I'm using Firefox and AutoHotKey v1.1.16.05

I'm a complete noob when it comes to http server stuff.

Relayer
Well why don't you tell us what url you are trying to connect to in firefox

The example in the OP uses port 8000 instead of the http default which is 80

lexikos
Posts: 9494
Joined: 30 Sep 2013, 04:07
Contact:

Re: AHKhttp - HTTP Server

20 Oct 2014, 21:49

You can test if the server is up and running by opening a command window (cmd.exe) and use ping localhost if it gives valid pings the script (server) works
That will only prove that the computer responds to ICMP (specifically, ping) requests on its loopback interface. It will not prove that the HTTP server is running or that the system will accept TCP/IP connections on any particular port. Some systems don't respond to ping (from outside) but still accept connections on specific TCP ports. I think you'd be hard-pressed to find a system that doesn't respond to ping localhost.

What you can do is attempt to connect to localhost:8000 using telnet or PuTTY, or see if port 8000 is listed by netstat -a.

Of course, that won't be necessary if the problem is as guest3456 guessed.
http://localhost:8000
not http://localhost
ahk7
Posts: 572
Joined: 06 Nov 2013, 16:35

Re: AHKhttp - HTTP Server

21 Oct 2014, 11:23

I can indeed see it is listening using netstat -a so that is a good tip (edited post above)
kidbit
Posts: 168
Joined: 02 Oct 2013, 16:05

Re: AHKhttp - HTTP Server

27 Oct 2014, 13:28

There's some bug: last 5 symbols of the page get eaten.
ahk7's example results into a page with such a code:

Code: Select all

<html>
<title>AHKhttp-server 1.0</title>
<body>
<b>para1: 123 - para2: 456<br><br><form action="/calc" method="get">
<input type=text name=para1>
+
<input type=text name=para2>
<input type=submit>
</form></b>
</body>
</
question := (2b) || !(2b) © Shakespeare.
Skittlez
Posts: 11
Joined: 13 Mar 2014, 12:16

Re: AHKhttp - HTTP Server

07 Nov 2014, 22:22

@ahk7 I've added an example of how to serve images.
@kidbit Try updating to the latest version, I can't reproduce that bug.

I'll be adding support for mime types and writing some docs, for real this time, sometime this weekend.
ahk7
Posts: 572
Joined: 06 Nov 2013, 16:35

Re: AHKhttp - HTTP Server

08 Nov 2014, 09:16

Thanks!

I've already been playing around with it and now have a rudimentary mp3 server. I can access the server from other PCs in the network and "stream" MP3 via a downloadable m3u playlist, very nice.

For others who already want to play around with it, you can use the following mimetypes for mp3/m3u

for m3u: (playlists)
res.headers["Content-Type"] := "audio/x-mpequrl"

for mp3:
res.headers["Content-Type"] := "audio/mpeg"

This will play an mp3 in your browser:

Code: Select all

mp3(ByRef req, ByRef res) {
    f := FileOpen(A_ScriptDir . "/groove.mp3", "r") ; example mp3 file
    length := f.RawRead(data, f.Length)
    f.Close()
    res.status := 200
    res.headers["Content-Type"] := "audio/mpeg"
    res.SetBody(data, length)
}
Look forward to the documentation
geek
Posts: 1051
Joined: 02 Oct 2013, 22:13
Location: GeekDude
Contact:

Re: AHKhttp - HTTP Server

08 Nov 2014, 09:55

Binary files over HTTP. Wonderful! Now we need to dump WAVs from the Speech API over http
Skittlez
Posts: 11
Joined: 13 Mar 2014, 12:16

Re: AHKhttp - HTTP Server

08 Nov 2014, 18:54

Added support for mime types, updated example using ServeFile.
Documentation is on github.
tmplinshi
Posts: 1604
Joined: 01 Oct 2013, 14:57

Re: AHKhttp - HTTP Server

15 Mar 2015, 11:31

Great! Thank you very much!!
tmplinshi
Posts: 1604
Joined: 01 Oct 2013, 14:57

Re: AHKhttp - HTTP Server

17 Mar 2015, 04:44

For anyone who want to use this wonderful lib:
  1. Please use AHK Unicode version in order to work.
  2. ahk7's example code not working? Yes, it's outdated (I tried his code several times, and finally noticed that today...).
    You need make these changes in his code:
    • Replace SetBody to SetBodyText
    • Replace statusCode to status
Thanks for your example code ahk7!

Return to “Scripts and Functions (v1)”

Who is online

Users browsing this forum: furqan, Spawnova, TheNaviator and 87 guests