├── .dockerignore
├── .env.example
├── .github
└── workflows
│ ├── ci.yaml
│ └── image-push.yaml
├── .gitignore
├── Dockerfile
├── LICENSE
├── Makefile
├── README.md
├── bin
├── build_nginx_conf.sh
├── compile_templates.sh
├── psqldef
├── setup.sh
└── start.sh
├── compose.yml
├── misc
├── compile_templates.awk
├── default.css
├── nginx.default.conf.template
└── variable_convention_validator.awk
├── schema.sql
├── src
├── controller
│ ├── GET.awk
│ ├── NOTFOUND.awk
│ ├── _.GET.awk
│ ├── _.posts._.GET.awk
│ ├── _.rss..xml.GET.awk
│ ├── _.styles..css.GET.awk
│ ├── api.v1.accounts.GET.awk
│ ├── api.v1.editor.GET.awk
│ ├── api.v1.editor.posts.GET.awk
│ ├── api.v1.editor.posts.POST.awk
│ ├── api.v1.editor.posts._.GET.awk
│ ├── api.v1.images.uploading-sign.POST.awk
│ ├── authed.GET.awk
│ ├── authed.posts.GET.awk
│ ├── authed.posts.delete.GET.awk
│ ├── authed.posts.edit.GET.awk
│ ├── authed.posts.edit.POST.awk
│ ├── authed.posts.new.GET.awk
│ ├── authed.posts.new.POST.awk
│ ├── authed.settings.GET.awk
│ ├── authed.settings.POST.awk
│ ├── authed.styles.GET.awk
│ ├── authed.styles.POST.awk
│ ├── login.GET.awk
│ ├── logout.POST.awk
│ ├── oauth_callback.GET.awk
│ └── test.GET.awk
├── entrypoint.awk
├── lib
│ ├── aes256.awk
│ ├── awss3.awk
│ ├── base64.awk
│ ├── datetime.awk
│ ├── environ.awk
│ ├── error.awk
│ ├── github.awk
│ ├── hmac.awk
│ ├── html.awk
│ ├── http.awk
│ ├── json.awk
│ ├── logger.awk
│ ├── pgsql.awk
│ ├── router.awk
│ ├── shell.awk
│ ├── text.awk
│ ├── url.awk
│ └── uuid.awk
├── middleware
│ ├── auth.awk
│ ├── httpjson.awk
│ └── template.awk
├── model
│ ├── account.awk
│ ├── blog.awk
│ ├── post.awk
│ ├── signin.awk
│ └── stylesheet.awk
├── vendor
│ └── .gitignore
└── version.awk
├── static
├── assets
│ ├── awkblog-icon.png
│ ├── github-mark.svg
│ ├── photo-up.svg
│ └── uploadImage.js
└── favicon.ico
├── test
├── integration
│ ├── healthcheck.yaml
│ └── start-login-flow.yaml
├── integrationtest.sh
├── lib
│ ├── aes256.awk
│ ├── awss3.awk
│ ├── base64.awk
│ ├── hmac.awk
│ ├── http.awk
│ ├── router.awk
│ ├── shell.awk
│ ├── url.awk
│ └── uuid.awk
├── template
│ ├── components
│ │ └── head.html
│ ├── dist
│ │ └── _account_name
│ │ │ ├── get.html
│ │ │ └── posts
│ │ │ └── _id
│ │ │ └── get.html
│ └── pages
│ │ └── _account_name
│ │ ├── get.html
│ │ └── posts
│ │ └── _id
│ │ └── get.html
├── templatetest.sh
├── testutil.awk
└── unittest.sh
└── view
├── components
├── _account_name
│ ├── footer.html
│ ├── head.html
│ └── header.html
└── authed
│ ├── article_editor.html
│ ├── console_head.html
│ └── header.html
├── i18n
├── en.yaml
└── ja.yaml
└── pages
├── 404.html
├── _account_name
├── get.html
├── posts
│ └── _id
│ │ └── get.html
└── rss.xml
│ └── get.xml
├── authed
├── get.html
├── posts
│ ├── edit
│ │ └── get.html
│ ├── get.html
│ └── new
│ │ └── get.html
├── settings
│ └── get.html
└── styles
│ └── get.html
└── get.html
/.dockerignore:
--------------------------------------------------------------------------------
1 | .env*
2 | .git
3 |
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | OAUTH_CLIENT_ID=clientkey
2 | OAUTH_CLIENT_SECRET=clientsecret
3 | OAUTH_CALLBACK_URI=http://localhost:4567/oauth-callback
4 | AWKBLOG_HOSTNAME=http://localhost:4567
5 | GITHUB_LOGIN_SERVER=http://localhost:8080
6 | GITHUB_API_SERVER=http://localhost:8080
7 | NOT_USE_AWS_S3=1
8 | AWKBLOG_WORKERS=40
9 | AWKBLOG_LANG=en
10 | # AWS_ACCESS_KEY_ID=
11 | # AWS_BUCKET=
12 | # AWS_REGION=
13 | # AWS_SECRET_ACCESS_KEY=
14 | # S3_BUCKET_ENDPOINT=https://bucketname.s3.amazonaws.com
15 | # S3_ASSET_HOST=https://bucketname.s3.amazonaws.com
16 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yaml:
--------------------------------------------------------------------------------
1 | on:
2 | push:
3 |
4 | name: Unit Tests
5 |
6 | jobs:
7 | ci:
8 | runs-on: ubuntu-latest
9 | container: ghcr.io/yammerjp/gawk-pgsql
10 |
11 | steps:
12 | - name: Check out repository code
13 | uses: actions/checkout@v3
14 | - name: Install dependencies
15 | run: |
16 | apt-get update -y && apt-get install -y uuid-runtime curl
17 | - name: Run unit tests
18 | run: ./test/unittest.sh
19 | - name: Run template tests
20 | run: ./test/templatetest.sh
21 |
--------------------------------------------------------------------------------
/.github/workflows/image-push.yaml:
--------------------------------------------------------------------------------
1 | on:
2 | push:
3 | branches:
4 | - main
5 |
6 | name: Docker Build and Push
7 |
8 | jobs:
9 | publish_docker_image:
10 | runs-on: ubuntu-latest
11 | permissions:
12 | contents: read
13 | packages: write
14 | steps:
15 | - uses: actions/checkout@v3
16 | - name: Login to GitHub Container Registry
17 | uses: docker/login-action@v2
18 | with:
19 | registry: ghcr.io
20 | username: ${{ github.actor }}
21 | password: ${{ secrets.GITHUB_TOKEN }}
22 | - name: Build Docker Image
23 | run: |
24 | docker build --tag ghcr.io/$GITHUB_ACTOR/awkblog:latest \
25 | --tag ghcr.io/$GITHUB_ACTOR/awkblog:$(echo $GITHUB_SHA | head -c7) \
26 | --build-arg VERSION="0.0.2+$(echo $GITHUB_SHA | head -c7)" \
27 | .
28 | - name: Push Docker Image
29 | run: |
30 | docker push ghcr.io/$GITHUB_ACTOR/awkblog:latest
31 | docker push ghcr.io/$GITHUB_ACTOR/awkblog:$(echo $GITHUB_SHA | head -c7)
32 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .env*
2 | src/_compiled_templates.awk
3 | supervisord.pid
4 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM ghcr.io/yammerjp/gawk-pgsql
2 | # https://github.com/yammerjp/gawk-pgsql-docker
3 | ARG VERSION="0.0.2-dev+with-nginx"
4 |
5 | RUN apt-get update -y && apt-get install -y \
6 | uuid-runtime \
7 | curl \
8 | nginx \
9 | supervisor \
10 | gettext-base\
11 | && apt-get clean \
12 | && rm -rf /var/lib/apt/lists/*
13 |
14 | WORKDIR /app
15 | COPY ./ /app
16 |
17 | RUN rm -r /var/www/html
18 | RUN ln -sf /app/static /var/www/html
19 |
20 | RUN /app/bin/setup.sh
21 |
22 | RUN echo "function getAwkblogVersion() { return \""${VERSION}"\" }" > /app/src/version.awk
23 |
24 | STOPSIGNAL SIGQUIT
25 |
26 | ENTRYPOINT "/app/bin/start.sh"
27 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Keisuke Nakayama
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | up:
2 | docker compose up -d
3 | log:
4 | docker compose logs -f
5 | restart:
6 | docker compose restart gawk
7 | down:
8 | docker compose down
9 | db:
10 | docker compose exec -e PGPASSWORD=passw0rd db /bin/bash -c 'psql -U postgres -d postgres'
11 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # awkblog
2 |
3 | awkblog is an AWK-based blogging platform that demonstrates AWK's capability to build full-fledged web applications. It features GitHub OAuth, Markdown support, and optional S3 image hosting, showcasing that AWK can be used for more than just text processing.
4 |
5 |
6 |

7 |
8 |
9 | ## Getting Started
10 |
11 | ```shell
12 |
13 | ./bin/setup.sh
14 | cp .env.example .env
15 | # By default, login uses a mock server.
16 | # If you want to use a real GitHub account, set up the GitHub OAuth App and configure your confidential information.
17 | # https://github.com/settings/applications/new
18 | # Authorization callback URL: http://localhost:4567/oauth-callback
19 | # Write OAUTH_CLIENT_ID and OAUTH_CLIENT_SECRET
20 | # vim .env
21 | docker compose up -d
22 | # open browser localhost:4567
23 | ```
24 |
25 |
26 | ## Features
27 |
28 | - Blog platform built entirely with AWK
29 | - GitHub OAuth authentication
30 | - Markdown support for posts
31 | - Custom CSS styling for each blog
32 | - RSS feed generation
33 | - Image uploads to S3 (optional)
34 | - Multi-user support
35 |
36 | ## Architecture
37 |
38 | awkblog is a web application written primarily in AWK, with some shell scripting for setup and management. It uses:
39 |
40 | - AWK for the core application logic and request handling
41 | - PostgreSQL for data storage
42 | - Nginx for reverse proxy
43 | - Docker for containerization and easy deployment
44 | - GitHub OAuth for authentication
45 | - Amazon S3 for image storage (optional)
46 |
47 | ## Configuration
48 |
49 | The main configuration is done through environment variables. Copy `.env.example` to `.env` and adjust the values as needed:
50 |
51 | - `OAUTH_CLIENT_ID`, `OAUTH_CLIENT_SECRET`, and `OAUTH_CALLBACK_URL`: GitHub OAuth configuration
52 | - `GITHUB_LOGIN_SERVER`: The URL of the GitHub login server for GitHub OAuth
53 | - `GITHUB_API_SERVER`: The URL of the GitHub API server for GitHub OAuth
54 | - `AWKBLOG_HOSTNAME`: The public URL of your awkblog instance
55 | - `AWKBLOG_WORKERS`: The number of request handler processes
56 | - `NOT_USE_AWS_S3`: If you don't want to use AWS S3 for image uploads, set this to `1`
57 | - `AWS_*` and `S3_*`: Amazon S3 configuration (if using S3 for image uploads)
58 | - `AWKBLOG_LANG`: The language of the blog. Currently, only `en` (English) and `ja` (Japanese) are supported.
59 |
60 | ## Development
61 |
62 | To set up a development environment:
63 |
64 | 1. Clone the repository
65 | 2. Run `./bin/setup.sh` to install dependencies
66 | 3. Copy `.env.example` to `.env` and configure as needed
67 | 4. Run `docker compose up -d` to start the development environment
68 | 5. Access the application at `http://localhost:4567`
69 |
70 | ## Contributing
71 |
72 | Contributions are welcome! Please feel free to submit a Pull Request.
73 |
74 | ## License
75 |
76 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
77 |
78 |
--------------------------------------------------------------------------------
/bin/build_nginx_conf.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -e
4 |
5 | REPOSITORY_ROOT="$(dirname "$0")/.."
6 | cd "$REPOSITORY_ROOT"
7 |
8 | echo "misc/nginx.default.conf.template" | gawk -f misc/compile_templates.awk > /tmp/nginx.default.conf.template.awk
9 | gawk -f /tmp/nginx.default.conf.template.awk -f <(echo 'BEGIN {printf "%s", compiled_templates::render("misc/nginx.default.conf.template")}')
10 |
11 |
12 |
--------------------------------------------------------------------------------
/bin/compile_templates.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -e
3 |
4 | REPOSITORY_ROOT="$(dirname "$0")/.."
5 | cd "$REPOSITORY_ROOT/view"
6 |
7 | if [ "$AWKBLOG_LANG" = "ja" ]; then
8 | export AWKBLOG_LANGFILE="i18n/ja.yaml"
9 | else
10 | export AWKBLOG_LANGFILE="i18n/en.yaml"
11 | fi
12 |
13 | find ./ -type f | grep -e '\.html$' | sed 's#^./##g' | awk -f ../misc/compile_templates.awk > "../src/_compiled_templates.awk"
14 |
--------------------------------------------------------------------------------
/bin/psqldef:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yammerjp/awkblog/8212140fb4f6444dfb54ef3b53ece29adaef49e0/bin/psqldef
--------------------------------------------------------------------------------
/bin/setup.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -e
3 |
4 | REPOSITORY_ROOT="$(dirname "$0")/.."
5 | cd "$REPOSITORY_ROOT"
6 |
7 | curl -sL https://raw.githubusercontent.com/yammerjp/md2html/main/markdown.awk > src/vendor/markdown.awk
8 |
--------------------------------------------------------------------------------
/bin/start.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -e
3 |
4 | REPOSITORY_ROOT="$(dirname "$0")/.."
5 | cd "$REPOSITORY_ROOT"
6 |
7 | if [ "$AWKBLOG_WORKERS" == "" ]; then
8 | AWKBLOG_WORKERS=1
9 | fi
10 |
11 | echo "start.sh: Build templates to src/_compiled_templates.awk"
12 | bin/compile_templates.sh
13 |
14 | echo "start.sh: Migrate Database Schema"
15 | /app/bin/psqldef --user="$POSTGRES_USER" --password="$POSTGRES_PASSWORD" --host="$POSTGRES_HOSTNAME" --file=schema.sql "$POSTGRES_DATABASE"
16 |
17 | echo "start.sh: Start Web Application"
18 |
19 | for AWKBLOG_PORT in $(seq 40001 $((40000 + AWKBLOG_WORKERS))); do
20 | cat /dev/urandom | tr -dc 'a-z0-9' | fold -w 128 | AWKBLOG_PORT=$AWKBLOG_PORT gawk \
21 | $(find src/ -type f | gawk '/\.awk$/{ printf " -f %s", $0 }') \
22 | &
23 | echo "start.sh: started gawk process, port: $AWKBLOG_PORT"
24 | done
25 |
26 | ./bin/build_nginx_conf.sh > /etc/nginx/conf.d/default.conf
27 | echo "start.sh: built /etc/nginx/conf.d/default.conf"
28 |
29 | exec nginx -g "daemon off;"
30 |
--------------------------------------------------------------------------------
/compose.yml:
--------------------------------------------------------------------------------
1 | services:
2 | app:
3 | build:
4 | context: .
5 | depends_on:
6 | - db
7 | - oauth-mock
8 | volumes:
9 | - ./:/app
10 | ports:
11 | - "0.0.0.0:4567:8080"
12 | env_file:
13 | - .env
14 | environment:
15 | - POSTGRES_HOSTNAME=db
16 | - POSTGRES_USER=postgres
17 | - POSTGRES_PASSWORD=passw0rd
18 | - POSTGRES_DATABASE=postgres
19 | - PORT=8080
20 | - ENCRYPTION_KEY=fvrcWCKkyxsmOkvmaxFLsrEXgrWqGrsh
21 | db:
22 | image: postgres:14
23 | volumes:
24 | - db-store:/var/lib/postgresql/data
25 | environment:
26 | - POSTGRES_PASSWORD=passw0rd
27 | - POSTGRES_USER=postgres
28 | oauth-mock:
29 | # https://github.com/yammerjp/github-oauth2-login-mock-server
30 | image: ghcr.io/yammerjp/github-oauth2-login-mock-server:latest
31 | ports:
32 | - "0.0.0.0:8080:8080"
33 | environment:
34 | - PORT=8080
35 | env_file:
36 | - .env
37 | volumes:
38 | db-store:
39 |
--------------------------------------------------------------------------------
/misc/compile_templates.awk:
--------------------------------------------------------------------------------
1 | function codegen_i18n( argname, filename, value, previousFs) {
2 | filename = ENVIRON["AWKBLOG_LANGFILE"]
3 |
4 | if (filename == "") {
5 | return
6 | }
7 |
8 | previousFs = FS
9 | FS=":"
10 | while((getline < filename) > 0) {
11 | argname = $1
12 | value = substr($2, 2)
13 | print " t[\"" argname "\"] = \"" value "\""
14 | }
15 | FS = previousFs
16 | close(filename)
17 | }
18 |
19 | function codegen_header() {
20 | print "@namespace \"compiled_templates\""
21 | print ""
22 | print "function render(path, v , ret, t) {"
23 | codegen_i18n()
24 | print " switch(path) {"
25 | }
26 |
27 | function codegen_by_file(filename) {
28 | printf " case \"%s\":\n", filename
29 |
30 | RS = "<%"
31 |
32 | while((getline < filename) > 0) {
33 | if (RS == "<%") { # out of <% %>
34 | # escape charactors
35 | gsub("\\\\", "\\\\", $0)
36 | gsub("\"", "\\\"", $0)
37 | gsub("\n", "\\n", $0)
38 |
39 | print " ret = ret sprintf(\"%s\", \"" $0 "\")"
40 |
41 | RS = "%>"
42 | } else { # in <% %>
43 | switch ($1) {
44 | case "#include":
45 | if (!($2 in TemplateFileMap)) {
46 | printf "unknown file path, %s\n", $2 > "/dev/stderr"
47 | exit 1
48 | }
49 | print " ret = ret render(\"" $2 "\", v)"
50 | break
51 | case "##":
52 | break
53 | case "=":
54 | $1 = ""
55 | print " ret = ret sprintf(\"%s\", " $0 ")"
56 | break
57 | default:
58 | print $0
59 | break
60 | }
61 | RS = "<%"
62 | }
63 | }
64 | close(filename)
65 |
66 | RS="\n"
67 |
68 | print " break"
69 | }
70 |
71 | function codegen_footer() {
72 | print " default:"
73 | print " print \"unknown path\" > \"/dev/stderr\""
74 | print " exit 1"
75 | print " }"
76 | print " return ret"
77 | print "}"
78 | }
79 |
80 | {
81 | filename = $0
82 | if (filename in TemplateFileMap) {
83 | # detect duplication
84 | next
85 | }
86 | TemplateFileMap[filename] = 1
87 | }
88 |
89 | END {
90 | codegen_header()
91 | for(path in TemplateFileMap) {
92 | codegen_by_file(path)
93 | }
94 | codegen_footer()
95 | }
96 |
--------------------------------------------------------------------------------
/misc/default.css:
--------------------------------------------------------------------------------
1 | img {
2 | max-width: 100%;
3 | }
4 |
5 | .blog-page-top {
6 | margin: 0 auto;
7 | max-width: 800px;
8 | padding: 0 16px;
9 | }
10 |
11 | header {
12 | text-align: center;
13 | }
14 |
15 | main > h1 {
16 | color: #111111;
17 | }
18 |
19 | .blog-header-link, .blog-header-link:visited {
20 | text-decoration: none;
21 | color: #111111;
22 | }
23 |
24 |
25 | .blog-header-link:hover, .blog-header-link:focus {
26 | text-decoration: none;
27 | color: #222222;
28 | }
29 |
30 | main {
31 | margin: 24px 0;
32 | }
33 |
34 | .post {
35 | margin: 24px 0;
36 | }
37 |
38 | .post-link {
39 | text-decoration: none;
40 | color: #333333;
41 | }
42 | .post-link:hover, .post-link:focus {
43 | text-decoration: none;
44 | color: #666666;
45 | }
46 | .post-link:visited {
47 | text-decoration: none;
48 | color: #999999;
49 | }
50 |
51 | .post-link .post-title {
52 | font-size: 2em;
53 | text-decoration: underline;
54 | color: #111111;
55 | }
56 |
57 | footer {
58 | margin-top: 72px;
59 | margin-bottom: 24px;
60 | text-align: center;
61 | }
62 |
63 | footer .account-name {
64 | font-size: 1.5em;
65 | }
66 |
67 | .github-link, .github-link:visited {
68 | text-decoration: none;
69 | color: #111111;
70 | }
71 | .github-link:focus, .github-link:hover {
72 | text-decoration: none;
73 | color: #222222;
74 | }
75 |
76 | footer .account-name::before {
77 | content: "@"
78 | }
79 |
80 | footer .powered-by {
81 | margin: 24px;
82 | }
83 |
84 | footer img {
85 | max-width: 120px;
86 | aspect-ratio: 1;
87 | border-radius: 50%;
88 | }
89 |
90 | a {
91 | color: #FFAC1C;
92 | }
93 | a:hover, a:focus {
94 | color: #FFBF00;
95 | }
96 | a:visited {
97 | color: #CC7722;
98 | }
99 |
--------------------------------------------------------------------------------
/misc/nginx.default.conf.template:
--------------------------------------------------------------------------------
1 | access_log /dev/stdout;
2 | error_log /dev/stderr;
3 |
4 | client_body_temp_path /var/tmp/nginx;
5 | client_body_buffer_size 1M;
6 |
7 | map $request_body $modified_body {
8 | default "$request_body ";
9 | '' $request_body;
10 | }
11 |
12 | root /var/www/html;
13 |
14 | upstream awkblog {
15 | <% for(i=40001; i<= 40000 + ENVIRON["AWKBLOG_WORKERS"]; i++) { %>
16 | server 127.0.0.1:<%= i %> max_conns=1;
17 | <% } %>
18 | }
19 |
20 | server {
21 | listen <%= ENVIRON["PORT"] %>;
22 |
23 | location /assets {
24 | alias /var/www/html/assets;
25 | }
26 |
27 | location /favicon.ico {
28 | alias /var/www/html/favicon.ico;
29 | }
30 |
31 | location / {
32 | proxy_http_version 1.0;
33 | proxy_pass http://awkblog;
34 | proxy_set_header Host $host;
35 | proxy_set_header X-Real-IP $remote_addr;
36 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
37 | proxy_set_header X-Forwarded-Proto $scheme;
38 | proxy_set_header X-Body-Leftover "512";
39 |
40 | proxy_set_body $modified_body;
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/misc/variable_convention_validator.awk:
--------------------------------------------------------------------------------
1 |
2 | # Usage:
3 | # git ls-files \
4 | # | grep -e '\.awk$'
5 | # | xargs -I{} gawk -f misc/variable_convention_validator.awk {}
6 |
7 | function find_in_array(target, source, i) {
8 | for (i in source) {
9 | if (target == source[i]) {
10 | return 1
11 | }
12 | }
13 | return 0
14 | }
15 |
16 | # detect function declaration
17 | /^[[:blank:]]*function[[:blank:]]+[a-zA-Z0-9_]+[[:blank:]]*\([a-zA-Z0-9_, \t]*\)/ {
18 | FuncnameBegin = index($0, "function") + 9
19 | ParenBegin = index($0, "(")
20 | ParenEnd = index($0, ")")
21 | Args = substr($0, ParenBegin + 1, ParenEnd - ParenBegin - 1)
22 |
23 | InFunction = 1
24 | FunctionName = substr($0, FuncnameBegin, ParenBegin - FuncnameBegin)
25 | next
26 | }
27 |
28 | # detect function end
29 | InFunction && /^}$/ {
30 | InFunction = 0
31 |
32 | Args = ""
33 | next
34 | }
35 |
36 | InMultiLineString {
37 | # remove escaped double quote
38 | gsub(/[^\\]\\"/g, " ")
39 |
40 | # remove string token
41 | gsub(/"[^"]*"/g, " ")
42 |
43 | if (match($0, "\"")) {
44 | # finish multi-line string
45 | InMultiLineString = 0
46 | gsub(/^[^"]*"/, " ")
47 | }
48 | }
49 |
50 | !InMultiLineString && InFunction {
51 | gsub("@namespace", " ")
52 |
53 | # remove escaped characters
54 | gsub(/\\./, " ")
55 |
56 | StringIsLeftOfRegularExpression = match($0, "\"") < match($0, "/")
57 | if (StringIsLeftOfRegularExpression) {
58 | # remove string token
59 | gsub(/"[^"]*"/, " ")
60 | }
61 |
62 | # remove regular expression
63 | gsub(/\/[^\/\\]*\[[^\]]*\]/, "/ ")
64 | gsub(/\/[^\/\\]*\//, " ")
65 |
66 | if (!StringIsLeftOfRegularExpression) {
67 | # remove string token
68 | gsub(/"[^"]*"/, " ")
69 | }
70 |
71 |
72 | if (match($0, "\"")) {
73 | # start multi-line string
74 | InMultiLineString = 1
75 | gsub(/"[^"]+$/, " ")
76 | }
77 |
78 | # remove comment
79 | gsub(/#.*$/, " ")
80 |
81 | # remove namespace
82 | gsub(/[a-zA-Z0-9_]*::/, "")
83 |
84 | # remove function call
85 | gsub(/[a-zA-Z0-9_]*\(/, "")
86 |
87 | # remove operator
88 | gsub(/[^a-zA-Z0-9_]/, " ")
89 |
90 | for (i = 1; i <= NF; i++) {
91 | switch ($i) {
92 | # reserved words
93 | case "break":
94 | case "case":
95 | case "close":
96 | case "continue":
97 | case "default":
98 | case "delete":
99 | case "else":
100 | case "exit":
101 | case "for":
102 | case "function":
103 | case "getline":
104 | case "if":
105 | case "in":
106 | case "next":
107 | case "print":
108 | case "printf":
109 | case "return":
110 | case "switch":
111 | case "while":
112 | break
113 |
114 | # not reserved words
115 | default:
116 | if (match($i, /^[0-9]+$/)) {
117 | # decimal number
118 | break
119 | }
120 | if (match($i, /^0x[0-9A-Fa-f]+$/)) {
121 | # hexadecimal number
122 | break
123 | }
124 | VariableBeginUpperCase = match($i, /^[A-Z]/)
125 | VariableIncludesFunctionArguments = match(Args, $i)
126 | if (VariableBeginUpperCase && VariableIncludesFunctionArguments) {
127 | # global variable should be started with upper case
128 | print "global variable should start with upper case: \"" $i "\" at " FILENAME ":" NR " in " FunctionName "()"
129 | }
130 | if (!VariableBeginUpperCase && !VariableIncludesFunctionArguments) {
131 | # local variable should be defined in function arguments
132 | print "local variable should be defined in function arguments: \"" $i "\" at " FILENAME ":" NR " in " FunctionName "()"
133 | }
134 | }
135 | }
136 | }
137 |
--------------------------------------------------------------------------------
/schema.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE accounts (
2 | id bigint PRIMARY KEY NOT NULL,
3 | name text,
4 | created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
5 | );
6 |
7 | CREATE TABLE posts (
8 | id SERIAL PRIMARY KEY NOT NULL,
9 | account_id bigint NOT NULL REFERENCES accounts(id),
10 | title text,
11 | content text,
12 | created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
13 | );
14 |
15 | CREATE TABLE blogs (
16 | id SERIAL PRIMARY KEY NOT NULL,
17 | account_id bigint NOT NULL REFERENCES accounts(id),
18 | title text,
19 | description text,
20 | author_profile text,
21 | coverimage VARCHAR(255),
22 | created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
23 | );
24 |
25 | CREATE UNIQUE INDEX uk_blogs_account_id ON blogs(account_id);
26 |
27 | CREATE TABLE sessions (
28 | id CHAR(36) PRIMARY KEY,
29 | account_id bigint NOT NULL REFERENCES accounts(id),
30 | created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
31 | last_accessed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
32 | );
33 |
34 | CREATE TABLE stylesheets (
35 | id SERIAL PRIMARY KEY NOT NULL,
36 | account_id bigint NOT NULL REFERENCES accounts(id),
37 | content text,
38 | created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
39 | );
40 |
41 | CREATE UNIQUE INDEX uk_stylesheets_account_id ON stylesheets(account_id);
42 |
--------------------------------------------------------------------------------
/src/controller/GET.awk:
--------------------------------------------------------------------------------
1 | @namespace "controller"
2 |
3 | function get() {
4 | auth::redirectIfSuccessToVerify()
5 |
6 | template::render("get.html")
7 | }
8 |
--------------------------------------------------------------------------------
/src/controller/NOTFOUND.awk:
--------------------------------------------------------------------------------
1 | @namespace "controller"
2 |
3 | function notfound( v) {
4 | template::render("404.html", v, 404);
5 | }
6 |
--------------------------------------------------------------------------------
/src/controller/_.GET.awk:
--------------------------------------------------------------------------------
1 | @namespace "controller"
2 |
3 | function _account_name__get( splitted, params, query, rows, id, html, blog, posts, templateVars, description, accountName, accountId) {
4 | split(http::getPath(), splitted, "@")
5 | accountName = splitted[2]
6 |
7 | accountId = model::getAccountId(accountName)
8 | if (accountId == "") {
9 | notfound()
10 | return
11 | }
12 | model::getBlog(blog, accountId)
13 | model::getPosts(posts, accountId)
14 |
15 | templateVars["account_name"] = html::escape(accountName)
16 | templateVars["blog_title"] = html::escape(blog["title"])
17 | templateVars["blog_description"] = markdown::parseMultipleLines(html::escape(blog["description"]))
18 | templateVars["blog_author_profile"] = markdown::parseMultipleLines(html::escape(blog["author_profile"]))
19 |
20 | for(i = 1; i <= length(posts); i++) {
21 | templateVars["posts"][i]["id"] = html::escape(posts[i]["id"])
22 | templateVars["posts"][i]["title"] = html::escape(posts[i]["title"])
23 | templateVars["posts"][i]["description"] = text::headAbout500(html::toText(markdown::parseMultipleLines(html::escape(posts[i]["content"])))) "..."
24 | }
25 |
26 | template::render("_account_name/get.html", templateVars);
27 | }
28 |
--------------------------------------------------------------------------------
/src/controller/_.posts._.GET.awk:
--------------------------------------------------------------------------------
1 | @namespace "controller"
2 |
3 | function _account_name__posts___id__get( splitted, params, query, rows, id, html, blog, post, templateVars, atAccountName, accountName, accountId, at, postId) {
4 | split(http::getPath(), splitted, "/")
5 | atAccountName = splitted[2]
6 | postId = splitted[4]
7 |
8 | at = substr(atAccountName, 1, 1)
9 | if (at != "@") {
10 | notfound()
11 | return
12 | }
13 | accountName = substr(atAccountName, 2)
14 |
15 | accountId = model::getAccountId(accountName)
16 | if (accountId == "") {
17 | notfound()
18 | return
19 | }
20 |
21 | templateVars["account_name"] = html::escape(accountName)
22 | model::getBlog(blog, accountId)
23 | templateVars["blog_title"] = html::escape(blog["title"])
24 | templateVars["blog_description"] = markdown::parseMultipleLines(html::escape(blog["description"]))
25 | templateVars["blog_author_profile"] = markdown::parseMultipleLines(html::escape(blog["author_profile"]))
26 |
27 | model::getPost(post, postId)
28 | if ("error" in post) {
29 | notfound()
30 | return
31 | }
32 | templateVars["id"] = html::escape(post["id"])
33 | templateVars["title"] = html::escape(post["title"])
34 | templateVars["content"] = markdown::parseMultipleLines(html::escape(post["content"]))
35 | templateVars["created_at"] = html::escape(post["created_at"])
36 |
37 | template::render("_account_name/posts/_id/get.html", templateVars);
38 | }
39 |
--------------------------------------------------------------------------------
/src/controller/_.rss..xml.GET.awk:
--------------------------------------------------------------------------------
1 | @namespace "controller"
2 |
3 | function _account_name__rss_xml__get( path_parts, splitted, params, query, rows, id, html, result, templateVars, blog, accountName, accountId) {
4 | split(http::getPath(), path_parts, "/")
5 | split(path_parts[2], splitted, "@")
6 | accountName = splitted[2]
7 |
8 |
9 | accountId = model::getAccountId(accountName)
10 | if (accountId == "") {
11 | notfound()
12 | return
13 | }
14 |
15 | model::getBlog(blog, accountId)
16 | templateVars["author_account_name"] = html::escape(accountName)
17 | templateVars["blog_title"] = html::escape(blog["title"])
18 | templateVars["blog_description"] = html::escape(blog["description"])
19 | templateVars["copyright"] = html::escape("Copyright 2024 " accountName)
20 |
21 | templateVars["account_url"] = http::getHostName() "/@" accountName
22 |
23 | model::getPosts(result, accountId)
24 |
25 | for(i = 1; i <= length(result); i++) {
26 | templateVars["posts"][i]["id"] = html::escape(result[i]["id"])
27 | templateVars["posts"][i]["title"] = html::escape(result[i]["title"])
28 | templateVars["posts"][i]["content"] = html::escape(markdown::parseMultipleLines(result[i]["content"]))
29 | templateVars["posts"][i]["created_at"] = html::escape(result[i]["created_at"]) # TODO: fix format
30 | }
31 |
32 | template::render("_account_name/rss.xml/get.xml", templateVars, 200, "xml");
33 | }
34 |
--------------------------------------------------------------------------------
/src/controller/_.styles..css.GET.awk:
--------------------------------------------------------------------------------
1 | @namespace "controller"
2 |
3 | function _account_name__style_css__get( path_parts, splitted, params, query, rows, id, html, result, templateVars, accountName, accountId) {
4 | split(http::getPath(), path_parts, "/")
5 | split(path_parts[2], splitted, "@")
6 | accountName = splitted[2]
7 |
8 |
9 | accountId = model::getAccountId(accountName)
10 | if (accountId == "") {
11 | notfound()
12 | return
13 | }
14 |
15 | http::setHeader("content-type", "text/css")
16 | http::send(200, model::getStylesheet(accountId))
17 | }
18 |
--------------------------------------------------------------------------------
/src/controller/api.v1.accounts.GET.awk:
--------------------------------------------------------------------------------
1 | @load "json"
2 | @namespace "controller"
3 |
4 | function api__v1__accounts__get( result) {
5 | model::getAccounts(result)
6 | if ("error" in result) {
7 | http::send(403)
8 | }
9 | httpjson::render(result)
10 | }
11 |
--------------------------------------------------------------------------------
/src/controller/api.v1.editor.GET.awk:
--------------------------------------------------------------------------------
1 | @load "json"
2 | @namespace "controller"
3 |
4 | function api__v1__editor__get( result, accountId) {
5 | auth::forbiddenIfFailedToVerify()
6 | accountId = auth::getAccountId()
7 |
8 | model::getAccount(result, accountId)
9 | if (result["error"] != "" ) {
10 | http::send(403)
11 | }
12 | httpjson::render(result)
13 | }
14 |
--------------------------------------------------------------------------------
/src/controller/api.v1.editor.posts.GET.awk:
--------------------------------------------------------------------------------
1 | @load "json"
2 | @namespace "controller"
3 |
4 | function api__v1__editor__posts__get( result, accountId) {
5 | auth::forbiddenIfFailedToVerify()
6 | accountId = auth::getAccountId()
7 |
8 |
9 |
10 | model::getPosts(result, accountId)
11 | httpjson::render(result)
12 | }
13 |
--------------------------------------------------------------------------------
/src/controller/api.v1.editor.posts.POST.awk:
--------------------------------------------------------------------------------
1 | @load "json"
2 | @namespace "controller"
3 |
4 | function api__v1__editor__posts__post( req, accountId, title, content) {
5 | auth::forbiddenIfFailedToVerify()
6 | accountId = auth::getAccountId()
7 |
8 | json::from_json(http::HTTP_REQUEST["body"], req)
9 |
10 | title = req["title"]
11 | content = req["content"]
12 |
13 | if (title == "") {
14 | http::send(400)
15 | return
16 | }
17 | if (content == "") {
18 | http::send(400)
19 | return
20 | }
21 |
22 | model::createPost(title, content, accountId)
23 | http::send(201)
24 | return
25 | }
26 |
--------------------------------------------------------------------------------
/src/controller/api.v1.editor.posts._.GET.awk:
--------------------------------------------------------------------------------
1 | @load "json"
2 | @namespace "controller"
3 |
4 | function api__v1__editor__posts___id__get( result, accountId, postId, splitted) {
5 |
6 | auth::forbiddenIfFailedToVerify()
7 | accountId = auth::getAccountId()
8 |
9 | split(http::getPath(), splitted, "/")
10 | # ex) /api/v1/editor/posts/33
11 | postId = splitted[6]
12 |
13 | model::getPost(result, postId)
14 |
15 | if (result["account_id"] != accountId) {
16 | http::send(403)
17 | }
18 |
19 | httpjson::render(result)
20 | }
21 |
--------------------------------------------------------------------------------
/src/controller/api.v1.images.uploading-sign.POST.awk:
--------------------------------------------------------------------------------
1 | @load "json"
2 | @namespace "controller"
3 |
4 | function api__v1__images__uploading_sign__post( req, accountId, type, extension, now, key, sizeMin, sizeMax) {
5 | auth::forbiddenIfFailedToVerify()
6 | accountId = auth::getAccountId()
7 |
8 | json::from_json(http::HTTP_REQUEST["body"], req)
9 |
10 | type = req["type"]
11 | switch(type) {
12 | case "image/png":
13 | extension = "png"
14 | break
15 | case "image/jpeg":
16 | extension = "jpg"
17 | break
18 | case "image/gif":
19 | extension = "gif"
20 | break
21 | case "image/webp":
22 | extension = "webp"
23 | break
24 | default:
25 | http::send(400, "unknown file type")
26 | return
27 | }
28 |
29 | now = awk::systime()
30 |
31 | key = "img/" accountId "/" now substr(http::getRequestId(), 1, 8) "." extension
32 |
33 | sizeMin = 32
34 | sizeMax = 1024 * 1024 * 3
35 |
36 | http::setHeader("content-type", "application/json")
37 | http::send(200, awss3::buildPreSignedUploadParams(now, key, type, sizeMin, sizeMax ))
38 | }
39 |
--------------------------------------------------------------------------------
/src/controller/authed.GET.awk:
--------------------------------------------------------------------------------
1 | @namespace "controller"
2 |
3 | function authed__get(variables) {
4 | auth::redirectIfFailedToVerify()
5 |
6 | variables["account_name"] = html::escape(auth::getUsername())
7 | template::render("authed/get.html", variables)
8 | }
9 |
--------------------------------------------------------------------------------
/src/controller/authed.posts.GET.awk:
--------------------------------------------------------------------------------
1 | @namespace "controller"
2 |
3 | function authed__posts__get( query, params, html, result, templateVars, accountId, i) {
4 | auth::redirectIfFailedToVerify()
5 |
6 | templateVars["account_name"] = html::escape(auth::getUsername())
7 |
8 | accountId = auth::getAccountId()
9 | model::getPosts(result, accountId)
10 |
11 | for(i = 1; i <= length(result); i++) {
12 | templateVars["posts"][i]["id"] = html::escape(result[i]["id"])
13 | templateVars["posts"][i]["title"] = html::escape(result[i]["title"])
14 | templateVars["posts"][i]["content"] = markdown::parseMultipleLines(html::escape(result[i]["content"]))
15 | templateVars["posts"][i]["created_at"] = html::escape(result[i]["created_at"])
16 | }
17 |
18 | template::render("authed/posts/get.html", templateVars)
19 | }
20 |
--------------------------------------------------------------------------------
/src/controller/authed.posts.delete.GET.awk:
--------------------------------------------------------------------------------
1 | @namespace "controller"
2 |
3 | function authed__posts__delete__post( id, title, content, account_id, query, params, result, accountId) {
4 | http::guardCSRF()
5 | auth::redirectIfFailedToVerify()
6 |
7 | url::decodeWwwForm(result, http::HTTP_REQUEST["body"])
8 |
9 | id = result["post_id"] + 0
10 | accountId = auth::getAccountId()
11 |
12 | model::deletePost(id, accountId)
13 |
14 | # TODO: flash message
15 |
16 | http::sendRedirect("/authed/posts")
17 | }
18 |
--------------------------------------------------------------------------------
/src/controller/authed.posts.edit.GET.awk:
--------------------------------------------------------------------------------
1 | @namespace "controller"
2 |
3 | function authed__posts__edit__get( postId, result, accountId, variables) {
4 | auth::redirectIfFailedToVerify()
5 |
6 | variables["account_name"] = html::escape(auth::getUsername())
7 |
8 | postId = http::getParameter("post_id") + 0
9 | accountId = auth::getAccountId() + 0
10 |
11 | model::getPostWithAccountId(result, postId, accountId)
12 | if ("error" in result) {
13 | http::send(403)
14 | return
15 | }
16 | variables["post_id"] = html::escape(result["id"])
17 | variables["title"] = html::escape(result["title"])
18 | variables["content"] = html::escape(result["content"])
19 |
20 | template::render("authed/posts/edit/get.html", variables)
21 | }
22 |
--------------------------------------------------------------------------------
/src/controller/authed.posts.edit.POST.awk:
--------------------------------------------------------------------------------
1 | @namespace "controller"
2 |
3 | function authed__posts__edit__post( id, title, content, account_id, query, params, result, accountId) {
4 | http::guardCSRF()
5 | auth::redirectIfFailedToVerify()
6 |
7 | url::decodeWwwForm(result, http::HTTP_REQUEST["body"])
8 |
9 | id = result["post_id"] + 0
10 | title = result["title"]
11 | content = result["content"]
12 | accountId = auth::getAccountId()
13 |
14 | model::updatePost(title, content, id, accountId)
15 |
16 | http::sendRedirect("/authed/posts")
17 | }
18 |
--------------------------------------------------------------------------------
/src/controller/authed.posts.new.GET.awk:
--------------------------------------------------------------------------------
1 | @namespace "controller"
2 |
3 | function authed__posts__new__get(variables) {
4 | auth::redirectIfFailedToVerify()
5 |
6 | variables["account_name"] = html::escape(auth::getUsername())
7 |
8 | variables["title"] = "" # default title
9 | variables["content"] = "" # default content
10 |
11 | template::render("authed/posts/new/get.html", variables)
12 | }
13 |
--------------------------------------------------------------------------------
/src/controller/authed.posts.new.POST.awk:
--------------------------------------------------------------------------------
1 | @namespace "controller"
2 |
3 | function authed__posts__new__post( title, content, accountId, query, params, result) {
4 | http::guardCSRF()
5 | auth::redirectIfFailedToVerify()
6 |
7 | url::decodeWwwForm(result, http::HTTP_REQUEST["body"])
8 |
9 | title = result["title"]
10 | content = result["content"]
11 | accountId = auth::getAccountId()
12 |
13 | model::createPost(title, content, accountId)
14 |
15 | http::sendRedirect("/authed/posts")
16 | }
17 |
--------------------------------------------------------------------------------
/src/controller/authed.settings.GET.awk:
--------------------------------------------------------------------------------
1 | @namespace "controller"
2 |
3 | function authed__settings__get(accountId, blog, variables) {
4 | auth::redirectIfFailedToVerify()
5 |
6 | variables["account_name"] = html::escape(auth::getUsername())
7 |
8 | accountId = auth::getAccountId()
9 | model::getBlog(blog, accountId)
10 | variables["title"] = html::escape(blog["title"])
11 | variables["description"] = html::escape(blog["description"])
12 | variables["author_profile"] = html::escape(blog["author_profile"])
13 |
14 | template::render("authed/settings/get.html", variables)
15 | }
16 |
--------------------------------------------------------------------------------
/src/controller/authed.settings.POST.awk:
--------------------------------------------------------------------------------
1 | @namespace "controller"
2 |
3 | function authed__settings__post( accountId, blog, result, title, description, authorProfile) {
4 | auth::redirectIfFailedToVerify()
5 | accountId = auth::getAccountId() + 0
6 |
7 | model::getBlog(blog, accountId)
8 |
9 | url::decodeWwwForm(result, http::HTTP_REQUEST["body"])
10 |
11 | title = result["title"]
12 | description = result["description"]
13 | authorProfile = result["author_profile"]
14 |
15 | model::updateBlog(title, description, authorProfile, accountId)
16 |
17 | http::sendRedirect("/authed/settings")
18 | }
19 |
--------------------------------------------------------------------------------
/src/controller/authed.styles.GET.awk:
--------------------------------------------------------------------------------
1 | @namespace "controller"
2 |
3 | function authed__styles__get(accountId, blog, variables) {
4 | auth::redirectIfFailedToVerify()
5 |
6 | variables["account_name"] = auth::getUsername()
7 |
8 | accountId = auth::getAccountId()
9 | variables["style"] = html::escape(model::getStylesheet(accountId))
10 |
11 | template::render("authed/styles/get.html", variables)
12 | }
13 |
--------------------------------------------------------------------------------
/src/controller/authed.styles.POST.awk:
--------------------------------------------------------------------------------
1 | @namespace "controller"
2 |
3 | function authed__styles__post( accountId, blog, result, title, description, authorProfile) {
4 | auth::redirectIfFailedToVerify()
5 | accountId = auth::getAccountId() + 0
6 |
7 | model::getBlog(blog, accountId)
8 |
9 | url::decodeWwwForm(result, http::HTTP_REQUEST["body"])
10 | model::updateStylesheet(result["style"], accountId)
11 |
12 | http::sendRedirect("/authed/styles")
13 | }
14 |
--------------------------------------------------------------------------------
/src/controller/login.GET.awk:
--------------------------------------------------------------------------------
1 | @namespace "controller"
2 |
3 | function login__get( state, url) {
4 | state = uuid::gen()
5 | url = github::redirectUrl(state)
6 | http::setCookie("state", state)
7 | http::sendRedirect(url)
8 | }
9 |
--------------------------------------------------------------------------------
/src/controller/logout.POST.awk:
--------------------------------------------------------------------------------
1 | @namespace "controller"
2 |
3 | function logout__post() {
4 | auth::logout()
5 | http::sendRedirect("/")
6 | }
7 |
--------------------------------------------------------------------------------
/src/controller/oauth_callback.GET.awk:
--------------------------------------------------------------------------------
1 | @namespace "controller"
2 |
3 | function oauth_callback__get( code, error, stateOnQuery, stateOnCookie, ret) {
4 | code = http::getParameter("code")
5 | error = http::getParameter("error")
6 | if (code == "" || error != "") {
7 | http::send(400, "failed to login")
8 | print "faild to login. code:" code " error:" error
9 | return
10 | }
11 | stateOnQuery = http::getParameter("state")
12 | stateOnCookie = http::getCookie("state")
13 | if (stateOnQuery == "" || stateOnQuery != stateOnCookie) {
14 | http::send(400, "invalid state")
15 | return
16 | }
17 |
18 | delete ret
19 | github::verify(ret, code)
20 |
21 | if (ret["error"] != "") {
22 | http::send(400, ret["error"])
23 | return
24 | }
25 |
26 | auth::login(ret["id"], ret["loginname"])
27 | http::sendRedirect("/authed")
28 | }
29 |
--------------------------------------------------------------------------------
/src/controller/test.GET.awk:
--------------------------------------------------------------------------------
1 | @namespace "controller"
2 |
3 | function test__get() {
4 | http::send(200, "Hello, test!")
5 | }
6 |
--------------------------------------------------------------------------------
/src/entrypoint.awk:
--------------------------------------------------------------------------------
1 | BEGIN {
2 | logger::setProcessIdentifier(http::getPort())
3 | pgsql::createConnection()
4 |
5 | query = "SELECT count(id) as ids FROM posts;"
6 | pgsql::exec(query)
7 | rows = pgsql::fetchRows()
8 | logger::info("Database Healthcheck: count(posts.id) (rows:" rows ") ... " pgsql::fetchResult(0, "ids"))
9 |
10 | router::register("GET", "/", "controller::get")
11 | router::register("GET", "/test", "controller::test__get")
12 | router::register("GET", "/login", "controller::login__get")
13 | router::register("POST", "/logout", "controller::logout__post")
14 | router::register("GET", "/oauth-callback", "controller::oauth_callback__get")
15 | router::register("GET", "/authed", "controller::authed__get")
16 | router::register("GET", "/authed/posts/new", "controller::authed__posts__new__get")
17 | router::register("POST", "/authed/posts/new", "controller::authed__posts__new__post")
18 | router::register("GET", "/authed/posts/edit", "controller::authed__posts__edit__get")
19 | router::register("POST", "/authed/posts/edit", "controller::authed__posts__edit__post")
20 | router::register("POST", "/authed/posts/delete", "controller::authed__posts__delete__post")
21 | router::register("GET", "/authed/posts", "controller::authed__posts__get")
22 | router::register("GET", "/authed/settings", "controller::authed__settings__get")
23 | router::register("POST", "/authed/settings", "controller::authed__settings__post")
24 | router::register("GET", "/authed/styles", "controller::authed__styles__get")
25 | router::register("POST", "/authed/styles", "controller::authed__styles__post")
26 | router::register("GET", "/api/v1/editor/posts", "controller::api__v1__editor__posts__get")
27 | router::register("POST", "/api/v1/editor/posts", "controller::api__v1__editor__posts__post")
28 | router::register("GET", "/api/v1/editor/posts/*", "controller::api__v1__editor__posts___id__get")
29 | router::register("GET", "/api/v1/editor", "controller::api__v1__editor__get")
30 | router::register("POST", "/api/v1/images/uploading-sign", "controller::api__v1__images__uploading_sign__post")
31 | router::register("GET", "/api/v1/accounts", "controller::api__v1__accounts__get")
32 | router::register("GET", "/*", "controller::_account_name__get")
33 | router::register("GET", "/*/rss.xml", "controller::_account_name__rss_xml__get")
34 | router::register("GET", "/*/style.css", "controller::_account_name__style_css__get")
35 | router::register("GET", "/*/posts/*", "controller::_account_name__posts___id__get")
36 | router::register_notfound("controller::notfound")
37 |
38 | router::debug_print()
39 |
40 | http::initialize();
41 | }
42 |
43 | {
44 | pgsql::renewConnection()
45 | http::receiveRequest()
46 | router::call(http::getMethod(), http::getPath())
47 | }
48 |
--------------------------------------------------------------------------------
/src/lib/aes256.awk:
--------------------------------------------------------------------------------
1 | @namespace "aes256"
2 |
3 | function encrypt(str , cmd, ret) {
4 | if (!(str ~ /^[a-zA-Z0-9_ -]+$/)) {
5 | error::raise("invalid charactor is included", "aes256")
6 | }
7 | cmd = "echo '" str "' | openssl enc -A -base64 -aes-256-cbc -salt -pbkdf2 -pass env:ENCRYPTION_KEY"
8 | cmd |& getline ret
9 | close(cmd)
10 | return base64::urlsafe(ret)
11 | }
12 |
13 | function decrypt(str , cmd, ret) {
14 | str = base64::urlunsafe(str)
15 | if (!(str ~ /^[a-zA-Z0-9+/]+=*$/)) {
16 | error::raise("aes256::decrypt", "aes256")
17 | }
18 | cmd = "echo '" str "' | openssl enc -d -base64 -aes-256-cbc -salt -pbkdf2 -pass env:ENCRYPTION_KEY"
19 | cmd |& getline ret
20 | close(cmd)
21 | return ret
22 | }
23 |
--------------------------------------------------------------------------------
/src/lib/awss3.awk:
--------------------------------------------------------------------------------
1 | @namespace "awss3"
2 |
3 | BEGIN {
4 | loadEnviron()
5 | }
6 |
7 | function loadEnviron() {
8 | NOT_USE_AWS_S3 = environ::get("NOT_USE_AWS_S3")
9 | if (NOT_USE_AWS_S3) {
10 | logger::info("The environment variable NOT_USE_AWS_S3 is set; remove this environment variable if you want to use S3.")
11 | } else {
12 | logger::info("S3 will be used. If you do not use it, set the environment variable NOT_USE_AWS_S3")
13 | ACCESS_KEY_ID = environ::getOrPanic("AWS_ACCESS_KEY_ID")
14 | BUCKET = environ::getOrPanic("AWS_BUCKET")
15 | REGION = environ::getOrPanic("AWS_REGION")
16 | SECRET_ACCESS_KEY = environ::getOrPanic("AWS_SECRET_ACCESS_KEY")
17 | ENDPOINT = environ::getOrPanic("S3_BUCKET_ENDPOINT")
18 | ASSET_HOST = environ::getOrPanic("S3_ASSET_HOST")
19 | }
20 | }
21 |
22 | function needToUseAwsS3() {
23 | if (NOT_USE_AWS_S3) {
24 | error::raise("need to use aws s3", "awss3")
25 | }
26 | }
27 |
28 | function buildPolicyToUpload(now, key, type, sizeMin, sizeMax , policy) {
29 | policy["expiration"] = datetime::gmdate("%Y-%m-%dT%H:%M:%S.000Z", now + 60)
30 | policy["conditions"][1]["bucket"] = BUCKET
31 | policy["conditions"][2]["key"] = key
32 | policy["conditions"][3]["Content-Type"] = type
33 | policy["conditions"][4][1] = "content-length-range"
34 | policy["conditions"][4][2] = sizeMin
35 | policy["conditions"][4][3] = sizeMax
36 | policy["conditions"][5]["acl"] = "public-read"
37 | policy["conditions"][6]["success_action_status"] = "201"
38 | policy["conditions"][7]["x-amz-algorithm"] = "AWS4-HMAC-SHA256"
39 | policy["conditions"][8]["x-amz-credential"] = ACCESS_KEY_ID "/" datetime::gmdate("%Y%m%d", now) "/" REGION "/s3/aws4_request"
40 | policy["conditions"][9]["x-amz-date"] = datetime::gmdate("%Y%m%dT%H%M%SZ", now)
41 |
42 | return json::to_json(policy, 1)
43 | }
44 |
45 | function buildEncodedPolicyToUpload(now, key, type, sizeMin, sizeMax , policy) {
46 | return base64::encode(buildPolicyToUpload(now, key, type, sizeMin, sizeMax))
47 | }
48 |
49 | function buildDateRegionKey(now) {
50 | return hmac::sha256(REGION, "hexkey:" hmac::sha256(datetime::gmdate("%Y%m%d", now), "key:AWS4" SECRET_ACCESS_KEY))
51 | }
52 |
53 | function sign(signee, now, dateRegionKey, dateRegionServiceKey, signingKey) {
54 | dateRegionKey = buildDateRegionKey(now)
55 | dateRegionServiceKey = hmac::sha256("s3", "hexkey:" dateRegionKey)
56 | signingKey = hmac::sha256("aws4_request", "hexkey:" dateRegionServiceKey)
57 |
58 | return hmac::sha256(signee, "hexkey:" signingKey)
59 | }
60 |
61 | function buildPreSignedUploadParams(now, key, type, sizeMin, sizeMax , ret, stringToSign) {
62 | needToUseAwsS3()
63 |
64 | stringToSign = base64::encode(buildPolicyToUpload(now, key, type, sizeMin, sizeMax))
65 | gsub("\n", "", stringToSign)
66 |
67 | ret["upload_url"] = ENDPOINT # "https://" BUCKET ".s3.amazonaws.com"
68 | ret["public_url"] = ASSET_HOST "/" key # "https://" BUCKET ".s3.amazonaws.com/" key
69 | ret["data"]["bucket"] = BUCKET
70 | ret["data"]["key"] = key
71 | ret["data"]["acl"] = "public-read"
72 | ret["data"]["success_action_status"] = "201"
73 | ret["data"]["policy"] = stringToSign
74 | ret["data"]["x-amz-credential"] = ACCESS_KEY_ID "/" datetime::gmdate("%Y%m%d", now) "/" REGION "/s3/aws4_request"
75 | ret["data"]["x-amz-signature"] = sign(stringToSign, now)
76 | ret["data"]["x-amz-algorithm"] = "AWS4-HMAC-SHA256"
77 | ret["data"]["x-amz-date"] = datetime::gmdate("%Y%m%dT%H%M%SZ", now)
78 | ret["data"]["Content-Type"] = type
79 | return json::to_json(ret)
80 | }
81 |
--------------------------------------------------------------------------------
/src/lib/base64.awk:
--------------------------------------------------------------------------------
1 | @namespace "base64"
2 |
3 | function urlsafe(str) {
4 | gsub("+","-", str)
5 | gsub("/","_", str)
6 | gsub("=", "", str)
7 | return str
8 | }
9 |
10 | function urlunsafe(str) {
11 | gsub("-","+", str)
12 | gsub("_","/", str)
13 | switch (length(str) % 4) {
14 | case 0:
15 | return str
16 | case 1:
17 | return str "==="
18 | case 2:
19 | return str "=="
20 | case 3:
21 | return str "="
22 | }
23 | }
24 |
25 | function encode(str) {
26 | return shell::exec("openssl enc -e -base64 -A", str)
27 | }
28 |
29 | function decode(str) {
30 | return shell::exec("openssl enc -d -base64", str "\n")
31 | }
32 |
--------------------------------------------------------------------------------
/src/lib/datetime.awk:
--------------------------------------------------------------------------------
1 | @namespace "datetime"
2 |
3 | function gmdate(format, timestamp , tzstr, operator, diffH, diffM, diffS, offset) {
4 | tzstr = awk::strftime("%z")
5 | if (tzstr !~ /^[-+][0-9]{4}$/) {
6 | error::raise("failure. strftime(\"%z\") is invalid format:" tzstr, "datetime")
7 | }
8 | operator = substr(tzstr,1,1)
9 | diffH = substr(tzstr, 2,2)
10 | diffM = substr(tzstr, 4,2)
11 | diffS = (diffH + 0) * 3600 + (diffS + 0) * 60
12 |
13 | if (operator == "+") {
14 | offset = - diffS
15 | } else {
16 | offset = + diffS
17 | }
18 | if (timestamp == 0) {
19 | timestamp = awk::systime()
20 | }
21 | return awk::strftime(format, timestamp + offset)
22 | }
23 |
--------------------------------------------------------------------------------
/src/lib/environ.awk:
--------------------------------------------------------------------------------
1 | @namespace "environ"
2 |
3 | function getOrRaise(name) {
4 | if (has(name)) {
5 | return get(name)
6 | }
7 | error::raise("ENVIRON[\"" name "\"] is not found")
8 | }
9 |
10 |
11 | function getOrPanic(name) {
12 | if (has(name)) {
13 | return get(name)
14 | }
15 | error::panic("ENVIRON[\"" name "\"] is not found")
16 | }
17 |
18 | function get(name) {
19 | return ENVIRON[name]
20 | }
21 |
22 | function has(name) {
23 | return name in ENVIRON
24 | }
25 |
26 | function is(name) {
27 | return has(name) && get(name)
28 | }
29 |
--------------------------------------------------------------------------------
/src/lib/error.awk:
--------------------------------------------------------------------------------
1 | @namespace "error"
2 |
3 | function defaultErrorHandler(message) {
4 | print message > "/dev/stderr"
5 | }
6 |
7 | function defaultPanicHandler(message) {
8 | print message > "/dev/stderr"
9 | exit 1
10 | }
11 |
12 | function registerErrorHandler(funcname) {
13 | logger::debug("error::registerErrorHandler(): " funcname)
14 | ERROR_HANDLER = funcname
15 | }
16 |
17 | function currentErrorHandler() {
18 | return ERROR_HANDLER
19 | }
20 |
21 | function currentPanicHandler() {
22 | return PANIC_HANDLER
23 | }
24 |
25 |
26 | function registerPanicHandler(funcname) {
27 | PANIC_HANDLER = funcname
28 | }
29 |
30 | function raise(message, extraContext) {
31 | if (ERROR_HANDLER == "") {
32 | defaultErrorHandler(message)
33 | } else {
34 | @ERROR_HANDLER(message, extraContext)
35 | }
36 | }
37 |
38 | function panic(message, extraContext) {
39 | if (PANIC_HANDLER == "") {
40 | defaultPanicHandler(message)
41 | } else {
42 | @PANIC_HANDLER(message, extraContext)
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/lib/github.awk:
--------------------------------------------------------------------------------
1 | @load "json"
2 | @namespace "github"
3 |
4 | BEGIN {
5 | GITHUB_LOGIN_SERVER = environ::getOrPanic("GITHUB_LOGIN_SERVER")
6 | OAUTH_CLIENT_ID = environ::getOrPanic("OAUTH_CLIENT_ID")
7 | OAUTH_CLIENT_SECRET = environ::getOrPanic("OAUTH_CLIENT_SECRET")
8 | OAUTH_CALLBACK_URI = environ::getOrPanic("OAUTH_CALLBACK_URI")
9 | GITHUB_LOGIN_SERVER = environ::getOrPanic("GITHUB_LOGIN_SERVER")
10 | GITHUB_API_SERVER = environ::getOrPanic("GITHUB_API_SERVER")
11 | }
12 |
13 | function redirectUrl(state) {
14 | return GITHUB_LOGIN_SERVER "/login/oauth/authorize?client_id=" OAUTH_CLIENT_ID "&redirect_uri=" OAUTH_CALLBACK_URI "&state=" state
15 | }
16 |
17 | function verify(ret, code , response, res_json, access_token) {
18 | if (!(code ~ /^[a-zA-Z0-9_ -]+$/)) {
19 | error::raise("github::verify", "invalid code")
20 | }
21 |
22 | response = shell::exec("curl -X POST -H 'Accept: application/json' '" GITHUB_LOGIN_SERVER "/login/oauth/access_token?client_id=" OAUTH_CLIENT_ID "&client_secret=" OAUTH_CLIENT_SECRET "&code=" code "'")
23 | json::from_json(response, res_json)
24 | access_token = res_json["access_token"]
25 | if (access_token == "") {
26 | ret["error"] = "access token is not found"
27 | return
28 | }
29 |
30 | if (!(access_token ~ /^[a-zA-Z0-9_ -]+$/)) {
31 | error::raise("github::verify", "invalid access token")
32 | }
33 |
34 | response = shell::exec("curl -H 'Authorization: Bearer " access_token "' -H 'Accept: application/vnd.github+json' -H 'X-GitHub-Api-Version: 2022-11-28' " GITHUB_API_SERVER "/user")
35 | json::from_json(response, res_json)
36 | ret["loginname"] = res_json["login"]
37 | ret["id"] = res_json["id"]
38 | }
39 |
--------------------------------------------------------------------------------
/src/lib/hmac.awk:
--------------------------------------------------------------------------------
1 | @namespace "hmac"
2 |
3 | function sha256(value, secret , splitted, ret) {
4 | if (secret !~ /^key:[a-zA-Z0-9+/]+$/ && secret !~/^hexkey:[a-fA-F0-9]+$/) {
5 | error::panic("secret is invalid")
6 | }
7 | ret = shell::exec("openssl dgst -sha256 -mac HMAC -macopt \"" secret "\"", value)
8 | split(ret, splitted, " ")
9 | return splitted[2]
10 | }
11 |
--------------------------------------------------------------------------------
/src/lib/html.awk:
--------------------------------------------------------------------------------
1 | @namespace "html"
2 |
3 | function escape(str) {
4 | gsub(/&/, "\\&", str)
5 | gsub(/'/, "\\'", str)
6 | gsub(/"/, "\\"", str)
7 | gsub(/, "\\<", str)
8 | gsub(/>/, "\\>", str)
9 | return str
10 | }
11 |
12 | function toText(content , chars, inTag, i ,ret) {
13 | ret = ""
14 | inTag = 0
15 | split(content, chars, "")
16 | for (i in chars) {
17 | if (chars[i] == "<") {
18 | inTag = 1
19 | continue
20 | }
21 | if (chars[i] == ">") {
22 | inTag = 0
23 | continue
24 | }
25 | if (inTag) {
26 | continue
27 | }
28 | ret = ret chars[i]
29 | }
30 | return ret
31 | }
32 |
33 |
--------------------------------------------------------------------------------
/src/lib/http.awk:
--------------------------------------------------------------------------------
1 | @namespace "http"
2 |
3 | function receiveRequest() {
4 | delete HTTP_REQUEST
5 | delete HTTP_REQUEST_PARAMETERS
6 | delete REQUEST_COOKIES
7 | delete RESPONSE_COOKIES
8 | delete HTTP_RESPONSE_HEADERS
9 | delete HTTP_REQUEST_HEADERS
10 | HTTP_REQUEST["body"] = ""
11 | HTTP_REQUEST["id"] = $0
12 | $0 = "";
13 |
14 | readFirstLine()
15 | readHeader()
16 | readBody()
17 |
18 | parseCookie()
19 | logRequest()
20 | }
21 |
22 | function readFirstLine( line, splitted, parameters, result) {
23 | # read first line
24 | awk::RS="\n"
25 | INET |& getline line;
26 | logger::debug("readFirstLine: " line, "http")
27 | if (line !~ /^\(HEAD|GET|POST|PUT|DELETE|OPTIONS|PATCH\) \/.* HTTP\/1\.[01]$/) {
28 | send(400)
29 | return
30 | }
31 | split(line, splitted,"[ ?]");
32 | HTTP_REQUEST["method"] = splitted[1];
33 | HTTP_REQUEST["path"] = splitted[2];
34 |
35 | if (splitted[4] == "") {
36 | HTTP_REQUEST["version"] = splitted[3];
37 | return
38 | }
39 |
40 | parameters = splitted[3];
41 | HTTP_REQUEST["version"] = splitted[4];
42 |
43 | if (length(parameters) > 0) {
44 | url::decodeWwwForm(result, parameters)
45 | for (i in result) {
46 | HTTP_REQUEST_PARAMETERS[i] = result[i]
47 | }
48 | }
49 | }
50 |
51 | function readHeader( line, colonSpace, key, value) {
52 | for(i = 1; INET |& getline line > 0; i++) {
53 | logger::debug("readHeader(): " line, "http")
54 | if (line == "" || line == "\r") {
55 | break;
56 | }
57 | gsub(/\r/, "" , line)
58 | colonSpace = index(line, ": ")
59 | key = tolower(substr(line, 1, colonSpace-1))
60 | value = substr(line, colonSpace+2)
61 | HTTP_REQUEST_HEADERS[key] = value
62 | }
63 | }
64 |
65 | function readBody( contentLength, unread, leftover, reading) {
66 | contentLength = getHeader("content-length")
67 | if (getMethod() == "GET" || getMethod() == "HEAD") {
68 | return
69 | }
70 | if (contentLength !~ /^[0-9]+$/) {
71 | send(411)
72 | }
73 | if (contentLength == 0) {
74 | # body is nothing
75 | return
76 | }
77 |
78 | if (environ::is("AWKBLOG_NO_PROXY") && !hasHeader("x-body-leftover")) {
79 | leftover = length(defaultLeftOverPadding())
80 | } else {
81 | # The end of the body is not read;\if the entire body is tried to be read, the operation is stalled due to waiting for the next input after the last character.
82 | leftover = getHeader("x-body-leftover") + 0
83 | }
84 | if (leftover < 1) {
85 | setHeader("content-type", "text/plain")
86 | send(400, "the HTTP Header 'X-Body-Leftover' must be greater than 0")
87 | }
88 | unread = contentLength - leftover
89 | while(unread > 0) {
90 | if (unread > 1000) {
91 | reading = 1000
92 | } else {
93 | reading = unread
94 | }
95 | awk::RS = sprintf(".{%d}", reading)
96 | INET |& getline
97 | HTTP_REQUEST["body"] = HTTP_REQUEST["body"] awk::RT
98 | unread -= reading
99 | logger::debug("readBody(): " awk::RT, "http")
100 | }
101 | awk::RS = "\n"
102 | }
103 |
104 | function parseCookie( splitted, i, idx, key, value) {
105 | split(getHeader("cookie"), splitted, "; ")
106 | for(i in splitted) {
107 | idx = index(splitted[i], "=")
108 | key = substr(splitted[i], 1, idx-1)
109 | value = substr(splitted[i], idx+1)
110 | if (value ~ "^\".*\"$") {
111 | value = substr(value, 2, length(value) - 2)
112 | }
113 |
114 | REQUEST_COOKIES[key]["value"] = value;
115 | }
116 | }
117 |
118 | function buildCookieHeader( headerStr, maxAge, secure, i) {
119 | headerStr = ""
120 | for (i in RESPONSE_COOKIES) {
121 | if ("Max-Age" in RESPONSE_COOKIES[i]) {
122 | maxAge = sprintf("; Max-Age=%s;", RESPONSE_COOKIES[i]["Max-Age"])
123 | } else {
124 | maxAge = ""
125 | }
126 | if (getHostName() ~ /^https:\/\//) {
127 | secure = sprintf("; Secure")
128 | } else {
129 | secure = ""
130 | }
131 | headerStr = sprintf("%sSet-Cookie: %s=%s; SameSite=Lax; HttpOnly%s%s\n", headerStr, i, RESPONSE_COOKIES[i]["value"], maxAge, secure)
132 | }
133 | return headerStr
134 | }
135 |
136 | function logRequest( params, headers, i) {
137 | if (getPath() == "/test") {
138 | logger::debug("request: /test", "http")
139 | return
140 | }
141 | for (i in HTTP_REQUEST_PARAMETERS) {
142 | params = params "\n " i ": " HTTP_REQUEST_PARAMETERS[i]
143 | }
144 | for (i in HTTP_REQUEST_HEADERS) {
145 | headers = headers "\n " i ": " HTTP_REQUEST_HEADERS[i]
146 | }
147 |
148 | logger::debug(sprintf("\
149 | request:\n\
150 | method:\n %s\n\
151 | path:\n %s\n\
152 | parameter:%s\n\
153 | header:%s\n\
154 | body:\n %s\n\
155 | ", getMethod(), getPath(), params, headers, getBody()), "http")
156 | }
157 |
158 | function getCookie(key) {
159 | if (key in REQUEST_COOKIES) {
160 | return REQUEST_COOKIES[key]["value"]
161 | }
162 | return ""
163 | }
164 |
165 | function setCookie(key, value) {
166 | RESPONSE_COOKIES[key]["value"] = value
167 | }
168 |
169 | function setCookieMaxAge(key, maxAge) {
170 | RESPONSE_COOKIES[key]["Max-Age"] = maxAge
171 | }
172 |
173 | function getPort( port) {
174 | return environ::get("AWKBLOG_PORT")
175 | }
176 |
177 | function initialize( port) {
178 | error::registerErrorHandler("http::internalServerError")
179 |
180 | port = getPort()
181 |
182 | INET = "/inet/tcp/" port "/0/0";
183 | FS=""
184 | RS = "\n"
185 |
186 | logger::info("Start awkblog. listen port " port " ...")
187 | }
188 |
189 | function internalServerError(message, kind) {
190 | if (kind == "") {
191 | kind = "http_internal_server_error"
192 | logger::error("internal server error has occured", "http_internal_server_error")
193 | }
194 | logger::error(message, kind)
195 |
196 | http::setHeader("content-type", "text/html; charset=UTF-8")
197 | http::send(500, "An error has occured. Please return to the previous page.")
198 | }
199 |
200 | function buildResponse(statusNum, content, headerStr, status, i) {
201 | switch(statusNum) {
202 | case 200: status = "200 OK"; break;
203 | case 201: status = "201 No Content"; break;
204 | case 204: status = "204 OK"; break;
205 | case 302: status = "302 Found"; break;
206 | case 400: status = "400 Bad Request"; break;
207 | case 401: status = "401 Unauthorized"; break;
208 | case 403: status = "403 Forbidden"; break;
209 | case 404: status = "404 Not Found"; break;
210 | case 411: status = "411 Length Required"; break;
211 | default: status = "500 Not Handled";break;
212 | }
213 |
214 | for(i in HTTP_RESPONSE_HEADERS) {
215 | headerStr = headerStr i ": " HTTP_RESPONSE_HEADERS[i] "\n";
216 | }
217 | headerStr = headerStr buildCookieHeader();
218 |
219 | return sprintf("HTTP/1.1 %s\n%s\n%s", status, headerStr, content);
220 | }
221 |
222 | function send(statusNum, content) {
223 | setHeader("x-awkblog-version", awk::getAwkblogVersion())
224 | logger::info(statusNum " flyip:" getHeader("fly-client-ip") " x44ip:" getHeader("x-forwarded-for") " " getMethod() " " getPath() " rf:" getHeader("referer") " ua:" getHeader("user-agent") , "http")
225 | printf "%s", buildResponse(statusNum, content) |& INET;
226 | close(INET);
227 |
228 | RS = "\n"
229 | next
230 | }
231 |
232 | function sendRedirect(url) {
233 | HTTP_RESPONSE_HEADERS["Location"] = url
234 | send(302, "")
235 | }
236 |
237 | function setHeader(key, value) {
238 | HTTP_RESPONSE_HEADERS[key] = value
239 | }
240 |
241 | function getPath() {
242 | return HTTP_REQUEST["path"]
243 | }
244 |
245 | function getMethod() {
246 | return HTTP_REQUEST["method"]
247 | }
248 |
249 | function getBody() {
250 | return HTTP_REQUEST["body"]
251 | }
252 |
253 | function hasHeader(key) {
254 | return key in HTTP_REQUEST_HEADERS
255 | }
256 |
257 | function getHeader(key) {
258 | if (hasHeader(key)) {
259 | return HTTP_REQUEST_HEADERS[key]
260 | }
261 | return ""
262 | }
263 |
264 | function hasParameter(key) {
265 | return key in HTTP_REQUEST_PARAMETERS
266 | }
267 |
268 | function getParameter(key) {
269 | if (hasParameter(key)) {
270 | return HTTP_REQUEST_PARAMETERS[key]
271 | }
272 | return ""
273 | }
274 |
275 | function getRequestId() {
276 | return HTTP_REQUEST["id"]
277 | }
278 |
279 | function guardCSRF() {
280 | if (isCrossSiteRequest()) {
281 | send(400)
282 | return
283 | }
284 | }
285 |
286 | function isCrossSiteRequest() {
287 | return getHeader("origin") != getHostName()
288 | }
289 |
290 | function getHostName() {
291 | return AWKBLOG_HOSTNAME
292 | }
293 |
294 | function defaultLeftOverPadding() {
295 | return "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
296 | }
297 |
298 | BEGIN {
299 | AWKBLOG_HOSTNAME = environ::getOrPanic("AWKBLOG_HOSTNAME")
300 | }
301 |
--------------------------------------------------------------------------------
/src/lib/json.awk:
--------------------------------------------------------------------------------
1 | @load "json"
2 |
--------------------------------------------------------------------------------
/src/lib/logger.awk:
--------------------------------------------------------------------------------
1 | @namespace "logger"
2 |
3 | function debug(message, tag) {
4 | output("DEBUG", message, tag)
5 | }
6 |
7 | function info(message, tag) {
8 | output("INFO", message, tag)
9 | }
10 |
11 | function warning(message, tag) {
12 | output("WARNING", message, tag)
13 | }
14 |
15 | function error(message, tag) {
16 | output("ERROR", message, tag)
17 | }
18 |
19 | function output(level, message, tag) {
20 | if (level == "DEBUG" && !environ::get("DEBUG")) {
21 | return
22 | }
23 | if (tag == "") {
24 | tag = "default"
25 | }
26 |
27 | printf "%s %s %s [%s] %s\n", nowISO8601(), level, ProcessIdentifier, tag, message
28 | fflush()
29 | }
30 |
31 | function setProcessIdentifier(identifier) {
32 | ProcessIdentifier = identifier
33 | }
34 |
35 | function nowISO8601() {
36 | return awk::strftime("%Y-%m-%dT%H:%M:%S%z", awk::systime())
37 | }
38 |
--------------------------------------------------------------------------------
/src/lib/pgsql.awk:
--------------------------------------------------------------------------------
1 | @load "pgsql"
2 |
3 | @namespace "pgsql"
4 |
5 | function exec(query, params, response, numberOfFields, col, numberOfRow, row, logstr, extraMessage, columns, numberOfRows, columnName) {
6 | logger::debug("pgsql::exec() query: " query, "pgsql")
7 | response = awk::pg_execparams(Connection, query, length(params), params)
8 | logger::debug("pgsql::exec() response: " response, "pgsql")
9 | delete RESULT
10 | RESULT[0] = ""
11 | delete RESULT[0]
12 | if (response ~ /^OK/) {
13 | awk::pg_clear(response)
14 | return 1
15 | }
16 | if (response ~ /^TUPLES /) {
17 | numberOfFields = awk::pg_nfields(response)
18 | for (col = 0; col < numberOfFields; col++) {
19 | columns[col] = awk::pg_fname(response, col)
20 | o }
21 | numberOfRows = awk::pg_ntuples(response)
22 | for (row = 0; row < numberOfRows; row++) {
23 | for (col = 0; col < numberOfFields; col++) {
24 | RESULT[row][columns[col]] = (awk::pg_getisnull(response,row,col) ? "" : awk::pg_getvalue(response,row,col))
25 | }
26 | }
27 |
28 | logstr = "\n"
29 | for(i in RESULT) {
30 | logstr = logstr " " i ":\n"
31 | for (columnName in RESULT[i]) {
32 | logstr = logstr " " columnName ": " RESULT[i][columnName] "\n"
33 | }
34 | }
35 | logger::debug(logstr, "pgsql")
36 |
37 | awk::pg_clear(response)
38 | return 1
39 | }
40 | extraMessage = awk::pg_errormessage(Connection)
41 | awk::pg_clear(response)
42 | error::raise("failed to pgsql::exec(): " response ", extraMessage: " extraMessage, "pgsql")
43 | return 0
44 | }
45 |
46 | function createConnection( param) {
47 | POSTGRES_HOSTNAME = environ::getOrPanic("POSTGRES_HOSTNAME")
48 | POSTGRES_DATABASE = environ::getOrPanic("POSTGRES_DATABASE")
49 | POSTGRES_USER = environ::getOrPanic("POSTGRES_USER")
50 | POSTGRES_PASSWORD = environ::getOrPanic("POSTGRES_PASSWORD")
51 |
52 | param = "host=" POSTGRES_HOSTNAME " dbname=" POSTGRES_DATABASE " user=" POSTGRES_USER " password=" POSTGRES_PASSWORD
53 | if (environ::has("POSTGRES_SSLMODE")) {
54 | param = param " sslmode=" environ::get("POSTGRES_SSLMODE")
55 | }
56 | if (environ::has("POSTGRES_OPTIONS")) {
57 | param = param " options=" environ::get("POSTGRES_OPTIONS")
58 | }
59 | if ((Connection = awk::pg_connect(param)) == "" ) {
60 | error::panic("pgsql::createConnection(): pg_connectionect failed: " ERRNO)
61 | }
62 | logger::info("created a postgres connection", "pgsql")
63 | logger::info("pgsql::createConnection(): Connection: " Connection, "pgsql")
64 | }
65 |
66 | function destroyConnection() {
67 | awk::pg_disconnect(Connection)
68 | }
69 |
70 | function renewConnection() {
71 | if (Connection) {
72 | destroyConnection()
73 | }
74 | createConnection()
75 | }
76 |
77 | function fetchRows() {
78 | return length(RESULT)
79 | }
80 |
81 | function fetchResult(row, columnName) {
82 | return RESULT[row][columnName]
83 | }
84 |
--------------------------------------------------------------------------------
/src/lib/router.awk:
--------------------------------------------------------------------------------
1 | @namespace "router"
2 |
3 | RoutingNotFoundCallback = "router::default_404"
4 |
5 | function default_404() {
6 | http::send(404, "");
7 | }
8 |
9 | function register(method, path, callback, key) {
10 | key = method " " path
11 | RoutingTable[key] = callback
12 |
13 | register_wildcard_position(path)
14 | }
15 |
16 | function register_notfound(callback) {
17 | RoutingNotFoundCallback = callback
18 | }
19 |
20 | function find(method, path, key, pos) {
21 | key = method " " path
22 | if (key in RoutingTable) {
23 | return RoutingTable[key]
24 | }
25 |
26 | for (pos in WildcardPositions) {
27 | key = method " " wildcard_compress(path, pos)
28 | if (key in RoutingTable) {
29 | return RoutingTable[key]
30 | }
31 | }
32 | return RoutingNotFoundCallback
33 | }
34 |
35 | function debug_print( i) {
36 | for(i in RoutingTable) {
37 | logger::debug("RoutingTable[\"" i "\"] = " RoutingTable[i], "router")
38 | }
39 | }
40 |
41 | function call(method, path, controller) {
42 | controller = find(method, path)
43 | @controller();
44 | }
45 |
46 | function register_wildcard_position(path, pos, path_parts, i) {
47 | path = substr(path, 2) # ignore leading a slash
48 | split(path, path_parts, "/")
49 | for (i in path_parts) {
50 | if (path_parts[i] == "*") {
51 | pos = pos " " i
52 | }
53 | }
54 | if (pos) {
55 | WildcardPositions[pos] = 1
56 | }
57 | }
58 |
59 | function wildcard_compress(path, pos, path_parts, masked_path, pos_parts, i) {
60 | path = substr(path, 2) # ignore leading a slash
61 | split(path, path_parts, "/")
62 | split_index(pos, pos_parts, " ")
63 | for(i in path_parts) {
64 | if (i in pos_parts) {
65 | masked_path = masked_path "/*"
66 | } else {
67 | masked_path = masked_path "/" path_parts[i]
68 | }
69 | }
70 | return masked_path
71 | }
72 |
73 | function split_index(str, arr, sep, arr_val, i) {
74 | split(str, arr_val, sep)
75 | for (i in arr_val)
76 | arr[arr_val[i]] = 1
77 | }
78 |
--------------------------------------------------------------------------------
/src/lib/shell.awk:
--------------------------------------------------------------------------------
1 | @namespace "shell"
2 |
3 | function exec(cmd ,stdinStr , ret, isFirstLine) {
4 | isFirstLine = 1
5 | logger::debug("shell::exec(\"" cmd "\", \"" stdinStr "\")")
6 | if (stdinStr == "") {
7 | while((cmd | getline) > 0) {
8 | if (isFirstLine) {
9 | isFirstLine = 0
10 | ret = $0
11 | } else {
12 | ret = ret "\n" $0
13 | }
14 | }
15 | } else {
16 | printf "%s", stdinStr |& cmd
17 | close(cmd, "to")
18 | while((cmd |& getline) > 0) {
19 | if (isFirstLine) {
20 | isFirstLine = 0
21 | ret = $0
22 | } else {
23 | ret = ret "\n" $0
24 | }
25 | }
26 | }
27 | if (close(cmd) != 0) {
28 | error::raise("shell::exec() failed", "shell")
29 | }
30 | logger::debug("shell::exec ret: " ret)
31 | return ret
32 | }
33 |
34 |
--------------------------------------------------------------------------------
/src/lib/text.awk:
--------------------------------------------------------------------------------
1 | @namespace "text"
2 |
3 | function headAbout500(content , splitted, i, description) {
4 | split(content, splitted, " ")
5 |
6 | for(i in splitted) {
7 | if (length(description) > 500) {
8 | break
9 | }
10 | description = description " " splitted[i]
11 | }
12 | return description
13 | }
14 |
--------------------------------------------------------------------------------
/src/lib/url.awk:
--------------------------------------------------------------------------------
1 | @namespace "url"
2 |
3 | function decodeWwwForm(result, encodedStr, encodedParts, key, value, equalIndex, i) {
4 | split(encodedStr, encodedParts, "&");
5 |
6 | for (i in encodedParts) {
7 | if (encodedParts[i] == "") {
8 | continue
9 | }
10 | key = encodedParts[i]
11 | value = ""
12 |
13 | # split =
14 | equalIndex = index(key, "=")
15 | if (equalIndex > 0) {
16 | value = substr(key, equalIndex + 1)
17 | key = substr(key, 1, equalIndex - 1)
18 | }
19 |
20 | # replace +
21 | gsub("+", " ", key)
22 | gsub("+", " ", value)
23 |
24 | # utf-8 percent decode
25 | key = decodeUtf8ParcentEncoding(key)
26 | value = decodeUtf8ParcentEncoding(value)
27 | result[key] = value
28 | }
29 | }
30 |
31 | function decodeUtf8ParcentEncoding(encodedStr, chars, decodedStr, l, n, num, utf32num, p1, p2, p3, p4, b1, b2, b3, b4) {
32 | l = length(encodedStr)
33 | n = 1
34 | decodedStr = ""
35 | split(encodedStr, chars, "")
36 | while (n<=l) {
37 | if (chars[n] != "%" || n+2 > l) {
38 | decodedStr = decodedStr chars[n]
39 | n++
40 | continue
41 | }
42 |
43 | # ref:
44 | # http://www.unicode.org/versions/Unicode6.0.0/ch03.pdf
45 | # Table 3-7. Well-Formed UTF-8 Byte Sequences
46 |
47 | p1 = chars[n] == "%"
48 | b1 = awk::strtonum("0x" chars[n+1] chars[n+2])
49 | p2 = chars[n+3] == "%"
50 | b2 = awk::strtonum("0x" chars[n+4] chars[n+5])
51 | p3 = chars[n+6] == "%"
52 | b3 = awk::strtonum("0x" chars[n+7] chars[n+8])
53 | p4 = chars[n+9] == "%"
54 | b4 = awk::strtonum("0x" chars[n+10] chars[n+11])
55 |
56 | if (p1 && 0x00 <= b1 && b1 <= 0x7F) {
57 | # 0xxxxxxx
58 | decodedStr = sprintf("%s%c", decodedStr, b1)
59 | n+=3
60 | continue
61 | }
62 | if (p1 && p2 && \
63 | 0xC2 <= b1 && b1 <= 0xDF && 0x80 <= b2 && b2 <= 0xBF \
64 | ) {
65 | # 110xxxxx 10xxxxxx
66 | decodedStr = sprintf("%s%c%c", decodedStr, b1, b2)
67 | n+=6
68 | continue
69 | }
70 | if (p1 && p2 && p3 && (\
71 | ( b1 == 0xE0 && 0xA0 <= b2 && b2 <= 0xBF && 0x80 <= b3 && b3 <= 0xBF ) || \
72 | ( 0xE1 <= b1 && b1 <= 0xEC && 0x80 <= b2 && b2 <= 0xBF && 0x80 <= b3 && b3 <= 0xBF ) || \
73 | ( b1 == 0xED && 0x80 <= b2 && b2 <= 0x9F && 0x80 <= b3 && b3 <= 0xBF ) || \
74 | ( 0xEE <= b1 && b1 <= 0xEF && 0x80 <= b2 && b2 <= 0xBF && 0x80 <= b3 && b3 <= 0xBF ) \
75 | )) {
76 | # 1110xxxx 10xxxxxx 10xxxxxx
77 | decodedStr = sprintf("%s%c%c%c", decodedStr, b1, b2, b3)
78 | n+=9
79 | continue
80 | }
81 | if (p1 && p2 && p3 && p4 && ( \
82 | ( b1 == 0xF0 && 0x90 <= b2 && b2 <= 0xBF && 0x80 <= b3 && b3 <= 0xBF && 0x80 <= b4 && b4 <= 0x80 ) || \
83 | ( 0xF1 <= b1 && b1 <= 0xF3 && 0x80 <= b2 && b2 <= 0xBF && 0x80 <= b3 && b3 <= 0xBF && 0x80 <= b4 && b4 <= 0x80 ) || \
84 | ( b1 == 0xF4 && 0x80 <= b2 && b2 <= 0x8F && 0x80 <= b3 && b3 <= 0xBF && 0x80 <= b4 && b4 <= 0x80 ) \
85 | )) {
86 | # 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
87 | decodedStr = sprintf("%s%c%c%c%c", decodedStr, b1, b2, b3, b4)
88 | n+=12
89 | continue
90 | }
91 | error::raise("failed to decode UTF-8 parcent encoded string", "url")
92 | exit 1
93 | }
94 | return decodedStr
95 | }
96 |
--------------------------------------------------------------------------------
/src/lib/uuid.awk:
--------------------------------------------------------------------------------
1 | @namespace "uuid"
2 |
3 | function gen( ret, cmd) {
4 | return tolower(shell::exec("uuidgen"))
5 | }
6 |
--------------------------------------------------------------------------------
/src/middleware/auth.awk:
--------------------------------------------------------------------------------
1 | @namespace "auth"
2 |
3 | function verify( encrypted, decrypted, splitted) {
4 | encrypted = http::getCookie("login_session")
5 | if (encrypted == "empty" || encrypted == "") {
6 | logger::info("login_session is empty", "auth")
7 | return 0
8 | }
9 | decrypted = aes256::decrypt(encrypted)
10 | split(decrypted, splitted, " ")
11 | if (splitted[1] != "AWKBLOG_LOGIN_SESSION") {
12 | return 0
13 | }
14 | MIDDLEWARE_AUTH["userid"] = splitted[2]
15 | MIDDLEWARE_AUTH["account_name"] = splitted[3]
16 | return length(MIDDLEWARE_AUTH["userid"]) > 0 && length(MIDDLEWARE_AUTH["account_name"]) > 0
17 | }
18 |
19 | function redirectIfFailedToVerify() {
20 | if (!verify()) {
21 | http::sendRedirect("/")
22 | }
23 | }
24 |
25 | function redirectIfSuccessToVerify() {
26 | if (verify()) {
27 | http::sendRedirect("/authed")
28 | }
29 | }
30 |
31 | function forbiddenIfFailedToVerify() {
32 | if (!verify()) {
33 | http::send(403)
34 | }
35 | }
36 |
37 |
38 | function getUsername() {
39 | return MIDDLEWARE_AUTH["account_name"]
40 | }
41 |
42 | function getAccountId() {
43 | return MIDDLEWARE_AUTH["userid"]
44 | }
45 |
46 | function login(accountId, accountName , loginSessionStr) {
47 | model::signin(accountId, accountName)
48 | loginSessionStr = sprintf("AWKBLOG_LOGIN_SESSION %d %s", accountId, accountName)
49 | http::setCookie("login_session", aes256::encrypt(loginSessionStr))
50 | }
51 |
52 | function logout() {
53 | http::setCookie("login_session", "empty")
54 | }
55 |
--------------------------------------------------------------------------------
/src/middleware/httpjson.awk:
--------------------------------------------------------------------------------
1 | @namespace "httpjson"
2 |
3 | function render(arr, statusNum) {
4 | http::setHeader("content-type", "application/json")
5 | if (statusNum == "") {
6 | statusNum = 200
7 | }
8 | return http::send(statusNum, json::to_json(arr, 1))
9 | }
10 |
11 | function badRequest(message , arr) {
12 | if (message == "") {
13 | message = "bad request"
14 | }
15 | arr["message"] = message
16 | return render(arr, 400)
17 | }
18 |
19 | function notFound(message , arr) {
20 | if (message == "") {
21 | message = "not found"
22 | }
23 | arr["message"] = message
24 | return render(arr, 404)
25 | }
26 |
27 | function internalError(message , arr) {
28 | if (message == "") {
29 | message = "internal server error"
30 | }
31 | arr["message"] = message
32 | return render(arr, 500)
33 | }
34 |
--------------------------------------------------------------------------------
/src/middleware/template.awk:
--------------------------------------------------------------------------------
1 | @namespace "template"
2 |
3 | function render(path, v, statusCode, contentType) {
4 | if (statusCode == "") {
5 | statusCode = 200
6 | }
7 | switch (contentType) {
8 | case "":
9 | case "html":
10 | http::setHeader("content-type", "text/html; charset=UTF-8")
11 | break
12 | case "xml":
13 | http::setHeader("content-type", "application/xml; charset=UTF-8")
14 | break
15 | default:
16 | error::raise("unknown content type", "middleware/template")
17 | }
18 | http::send(statusCode, compiled_templates::render("pages/" path, v));
19 | }
20 |
21 |
--------------------------------------------------------------------------------
/src/model/account.awk:
--------------------------------------------------------------------------------
1 | @namespace "model"
2 |
3 | function getAccountId(account_name , params, query, rows) {
4 | params[1] = account_name
5 | query = "SELECT id, name FROM accounts WHERE name = $1;"
6 | pgsql::exec(query, params)
7 |
8 | rows = pgsql::fetchRows()
9 | if (rows == 0 || rows > 1) {
10 | return ""
11 | }
12 |
13 | return pgsql::fetchResult(0, "id")
14 | }
15 |
16 | function getAccount(ret, id , params, query, rows) {
17 | params[1] = id
18 | query = "SELECT id, name FROM accounts WHERE id = $1"
19 | pgsql::exec(query, params)
20 | rows = pgsql::fetchRows()
21 | if (rows != 1) {
22 | ret["error"] = "account not found"
23 | return
24 | }
25 | ret["id"] = pgsql::fetchResult(0, "id")
26 | ret["name"] = pgsql::fetchResult(0, "name")
27 | return
28 | }
29 |
30 | function getAccounts(result , i, query, rows) {
31 | query = "SELECT id, name FROM accounts"
32 | pgsql::exec(query)
33 | rows = pgsql::fetchRows()
34 | for(i = 1; i <= rows; i++) {
35 | result[i]["id"] = pgsql::fetchResult(i-1, "id")
36 | result[i]["name"] = pgsql::fetchResult(i-1, "name")
37 | # TODO: add description
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/model/blog.awk:
--------------------------------------------------------------------------------
1 | @namespace "model"
2 |
3 | function getBlog(ret, accountId , params, query, rows) {
4 | params[1] = accountId
5 | query = "SELECT title, description, author_profile, coverimage FROM blogs WHERE account_id = $1;"
6 | pgsql::exec(query, params)
7 |
8 | rows = pgsql::fetchRows()
9 | if (rows == 0 || rows > 1) {
10 | error::raise("blog record is not found, or found multiple record")
11 | return
12 | }
13 |
14 | ret["title"] = pgsql::fetchResult(0, "title")
15 | ret["description"] = pgsql::fetchResult(0, "description")
16 | ret["author_profile"] = pgsql::fetchResult(0, "author_profile")
17 | logger::debug(ret["author_profile"], "authorprofile")
18 | ret["coverimage"] = pgsql::fetchResult(0, "coverimage")
19 | }
20 |
21 | function updateBlog(title, description, authorProfile, accountId , params, query) {
22 | params[1] = title
23 | params[2] = description
24 | params[3] = authorProfile
25 | params[4] = accountId
26 | query = "UPDATE blogs SET title = $1, description = $2, author_profile = $3 WHERE account_id = $4;"
27 | pgsql::exec(query, params)
28 | }
29 |
--------------------------------------------------------------------------------
/src/model/post.awk:
--------------------------------------------------------------------------------
1 | @namespace "model"
2 |
3 | function getPosts(result, id , params, query, html, rows, i) {
4 | params[1] = id
5 | query = "SELECT id, title, content, created_at FROM posts WHERE account_id = $1 ORDER BY created_at DESC;"
6 | pgsql::exec(query, params)
7 |
8 | rows = pgsql::fetchRows()
9 | for(i = 1; i <= rows; i++) {
10 | result[i]["id"] = pgsql::fetchResult(i-1, "id")
11 | result[i]["title"] = pgsql::fetchResult(i-1, "title")
12 | result[i]["content"] = pgsql::fetchResult(i-1, "content")
13 | result[i]["created_at"] = pgsql::fetchResult(i-1, "created_at")
14 | }
15 | }
16 |
17 | function getPostWithAccountId(result, id, accountId , params, query, rows) {
18 | params[1] = id
19 | params[2] = accountId
20 | query = "SELECT id, title, content, created_at FROM posts WHERE id = $1 AND account_id = $2;"
21 | pgsql::exec(query, params)
22 | rows = pgsql::fetchRows()
23 | if (rows != 1) {
24 | result["error"] = "not 1 record"
25 | return
26 | }
27 | result["id"] = pgsql::fetchResult(0, "id")
28 | result["title"] = pgsql::fetchResult(0, "title")
29 | result["content"] = pgsql::fetchResult(0, "content")
30 | result["created_at"] = pgsql::fetchResult(0, "created_at")
31 | }
32 |
33 |
34 | function getPost(result, id , params, query, rows) {
35 | params[1] = id
36 | query = "SELECT id, title, content, account_id, created_at FROM posts WHERE id = $1"
37 | pgsql::exec(query, params)
38 | rows = pgsql::fetchRows()
39 | if (rows != 1) {
40 | result["error"] = "not 1 record"
41 | return
42 | }
43 | result["id"] = pgsql::fetchResult(0, "id")
44 | result["title"] = pgsql::fetchResult(0, "title")
45 | result["content"] = pgsql::fetchResult(0, "content")
46 | result["account_id"] = pgsql::fetchResult(0, "account_id")
47 | result["created_at"] = pgsql::fetchResult(0, "created_at")
48 | # TODO: add coverimage
49 | }
50 |
51 | function createPost(title, content, accountId , params, query) {
52 | query = "INSERT INTO posts ( account_id, title, content ) VALUES ($1, $2, $3);"
53 | params[1] = accountId
54 | params[2] = title
55 | params[3] = content
56 | pgsql::exec(query, params)
57 | }
58 |
59 | function updatePost(title, content, id, accountId , params, query) {
60 | logger::info("updatePost(" title ", " content ", " id ", " accountId)
61 | query = "UPDATE posts SET title = $1, content = $2 WHERE id = $3 AND account_id = $4;"
62 | params[1] = title
63 | params[2] = content
64 | params[3] = id
65 | params[4] = accountId
66 | pgsql::exec(query, params)
67 | }
68 |
69 | function deletePost(id, accountId , params, query) {
70 | logger::info("deletePost(" id ", " accountId ")")
71 | query = "DELETE FROM posts WHERE id = $1 AND account_id = $2;"
72 | params[1] = id
73 | params[2] = accountId
74 | pgsql::exec(query, params)
75 | }
76 |
--------------------------------------------------------------------------------
/src/model/signin.awk:
--------------------------------------------------------------------------------
1 | @namespace "model"
2 |
3 | function signin(accountId, accountName , query, params) {
4 | query = "INSERT INTO accounts( id, name ) VALUES ($1, $2) ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name;"
5 | params[1] = accountId
6 | params[2] = accountName
7 | pgsql::exec(query, params)
8 |
9 | delete params
10 | query = "INSERT INTO stylesheets (account_id, content) SELECT $1, $2 WHERE NOT EXISTS (SELECT account_id FROM stylesheets WHERE account_id = $3);"
11 | params[1] = accountId
12 | params[2] = shell::exec("cat misc/default.css")
13 | params[3] = accountId
14 | pgsql::exec(query, params)
15 |
16 | delete params
17 | query = "INSERT INTO blogs (account_id, title, description, author_profile) SELECT $1, $2, $3, $4 WHERE NOT EXISTS (SELECT account_id FROM blogs WHERE account_id = $5);"
18 | params[1] = accountId
19 | params[2] = accountName "'s Blog"
20 | params[3] = ""
21 | params[4] = ""
22 | params[5] = accountId
23 | pgsql::exec(query, params)
24 |
25 | delete params
26 | params[1] = accountId
27 | params[2] = accountName
28 | query = "SELECT id, name FROM accounts WHERE id = $1 AND name = $2;"
29 | pgsql::exec(query, params)
30 | if (pgsql::fetchRows() != 1) {
31 | error::raise("failed to find account")
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/model/stylesheet.awk:
--------------------------------------------------------------------------------
1 | @namespace "model"
2 |
3 | function getStylesheet(accountId , params, query, rows) {
4 | params[1] = accountId + 0
5 | query = "SELECT content FROM stylesheets WHERE account_id = $1;"
6 | pgsql::exec(query, params)
7 |
8 | rows = pgsql::fetchRows()
9 | if (rows == 0 || rows > 1) {
10 | return ""
11 | }
12 |
13 | return pgsql::fetchResult(0, "content")
14 | }
15 |
16 | function insertStylesheet(accountId , params, query, rows, content) {
17 | content = ""
18 | while (("misc/default.css" | getline) > 0) {
19 | content = content $0 "\n"
20 | }
21 | params[1] = accountId
22 | params[2] = content
23 | query = "INSERT INTO stylesheets (account_id, content) VALUES($1, $2)"
24 | pgsql::exec(query, params)
25 | }
26 |
27 | function updateStylesheet(content, accountId , params, query) {
28 | params[1] = content
29 | params[2] = accountId
30 | query = "UPDATE stylesheets SET content = $1 WHERE account_id = $2"
31 | pgsql::exec(query, params)
32 | }
33 |
--------------------------------------------------------------------------------
/src/vendor/.gitignore:
--------------------------------------------------------------------------------
1 | *.awk
2 |
--------------------------------------------------------------------------------
/src/version.awk:
--------------------------------------------------------------------------------
1 | function getAwkblogVersion() { return "unknown" }
2 |
--------------------------------------------------------------------------------
/static/assets/awkblog-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yammerjp/awkblog/8212140fb4f6444dfb54ef3b53ece29adaef49e0/static/assets/awkblog-icon.png
--------------------------------------------------------------------------------
/static/assets/github-mark.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/static/assets/photo-up.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/static/assets/uploadImage.js:
--------------------------------------------------------------------------------
1 | // Need to load `browser-image-compression`
2 |
3 | async function uploadImage(originalFile, printLog) {
4 | const file = await imageCompression(originalFile, {maxSizeMB: 1, maxWidthOrHeight: 1920})
5 | printLog(`Form Submitted! Timestamp: ${event.timeStamp} (file name:${file.name}, size: ${file.size}, type: ${file.type})`);
6 | printLog("start to fetch presigned upload url")
7 | const res = await fetch("/api/v1/images/uploading-sign", {
8 | headers: { "Content-Type": "application/json;" },
9 | body: JSON.stringify({ type: file.type }),
10 | method: 'POST',
11 | }).then(r => {
12 | if (!r.ok) {
13 | throw new Error("failed to communicate with presign endpoint")
14 | }
15 | return r.json()
16 | })
17 | printLog("success to fetch presigned upload url")
18 | printLog(`response: ${JSON.stringify(res)}`);
19 |
20 | const postData = new FormData()
21 | for(const key in res.data) {
22 | postData.append(key, res.data[key]);
23 | }
24 | postData.append('file', file);
25 |
26 | printLog("start to post to S3")
27 | const resAws = await fetch(res.upload_url, {
28 | method: 'POST',
29 | body: postData,
30 | })
31 |
32 | printLog("success to post to S3")
33 | printLog(`response: ${await resAws.text()}`)
34 | printLog(`uploaded_url: ${res.public_url}`)
35 | if (!resAws.ok) {
36 | throw new Error("failed to communicate with aws")
37 | }
38 | return res.public_url
39 | }
40 |
--------------------------------------------------------------------------------
/static/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yammerjp/awkblog/8212140fb4f6444dfb54ef3b53ece29adaef49e0/static/favicon.ico
--------------------------------------------------------------------------------
/test/integration/healthcheck.yaml:
--------------------------------------------------------------------------------
1 | desc: Render top page
2 | runners:
3 | req: http://localhost:4567
4 | steps:
5 | login:
6 | req:
7 | /:
8 | get:
9 | body: null
10 | test: |
11 | current.res.status == 200
12 |
--------------------------------------------------------------------------------
/test/integration/start-login-flow.yaml:
--------------------------------------------------------------------------------
1 | desc: Redirect to host.docker.internal:8080 for login
2 | runners:
3 | req:
4 | endpoint: http://localhost:4567
5 | notFollowRedirect: true
6 | steps:
7 | login:
8 | req:
9 | /login:
10 | get:
11 | body: null
12 | test: |
13 | current.res.body == null && current.res.status == 302 && current.res.headers["Location"][0] matches "^http://host.docker.internal:8080/login/oauth/authorize\\?client_id=clientkey&redirect_uri=http://localhost:4567/oauth-callback&state=[0-9a-f-]+$"
14 |
--------------------------------------------------------------------------------
/test/integrationtest.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | REPOSITORY_ROOT="$(dirname "$0")/.."
4 | cd "$REPOSITORY_ROOT"
5 |
6 | export ENCRYPTION_KEY="passw0rd"
7 |
8 | find test/integration -type f | grep -e '\.yaml$' | xargs runn run
9 |
--------------------------------------------------------------------------------
/test/lib/aes256.awk:
--------------------------------------------------------------------------------
1 | @include "src/lib/aes256.awk"
2 | @include "src/lib/base64.awk"
3 | @include "test/testutil.awk"
4 |
5 | "aes256Encrypt" {
6 | encrypted = aes256::encrypt("secret information")
7 | decrypted = aes256::decrypt(encrypted)
8 | assertEqual("secret information", decrypted)
9 | }
10 |
11 | "aes256Decrypt" {
12 | encrypted = "U2FsdGVkX1-GoTdd5307fxvzI5Yqu7wFMFXv6-lwDAW8E8AYSWs__bGzsVlL4nQG"
13 | # U2FsdGVkX1+GoTdd5307fxvzI5Yqu7wFMFXv6+lwDAW8E8AYSWs//bGzsVlL4nQG
14 | decrypted = aes256::decrypt(encrypted)
15 | assertEqual("secret information", decrypted)
16 | }
17 |
--------------------------------------------------------------------------------
/test/lib/awss3.awk:
--------------------------------------------------------------------------------
1 | @include "test/testutil.awk"
2 | @include "src/lib/datetime.awk"
3 | @include "src/lib/awss3.awk"
4 | @include "src/lib/shell.awk"
5 | @include "src/lib/logger.awk"
6 | @include "src/lib/json.awk"
7 | @include "src/lib/base64.awk"
8 | @include "src/lib/hmac.awk"
9 | @include "src/lib/environ.awk"
10 | @include "src/lib/error.awk"
11 |
12 | "buildPolicyJson" {
13 | ENVIRON["AWS_ACCESS_KEY_ID"] = "accesskeyid"
14 | ENVIRON["AWS_BUCKET"] = "bucketname"
15 | ENVIRON["AWS_REGION"] = "ap-northeast-1"
16 | awss3::loadEnviron()
17 |
18 | result = awss3::buildPolicyToUpload(1704377552, "path/to/image.jpg", "image/jpeg", 1234, 1234)
19 | assertEqual("{\"conditions\":[{\"bucket\":\"bucketname\"},{\"key\":\"path/to/image.jpg\"},{\"Content-Type\":\"image/jpeg\"},[\"content-length-range\",1234,1234],{\"acl\":\"public-read\"},{\"success_action_status\":\"201\"},{\"x-amz-algorithm\":\"AWS4-HMAC-SHA256\"},{\"x-amz-credential\":\"accesskeyid/20240104/ap-northeast-1/s3/aws4_request\"},{\"x-amz-date\":\"20240104T141232Z\"}],\"expiration\":\"2024-01-04T14:13:32.000Z\"}", result)
20 | }
21 |
22 | "buildPolicyJsonEncoded" {
23 | ENVIRON["AWS_ACCESS_KEY_ID"] = "accesskeyid"
24 | ENVIRON["AWS_BUCKET"] = "bucketname"
25 | ENVIRON["AWS_REGION"] = "ap-northeast-1"
26 | awss3::loadEnviron()
27 |
28 | result = base64::encode(awss3::buildPolicyToUpload(1704377552, "path/to/image.jpg", "image/jpeg", 1234, 1234))
29 |
30 | assertEqual("eyJjb25kaXRpb25zIjpbeyJidWNrZXQiOiJidWNrZXRuYW1lIn0seyJrZXkiOiJwYXRoL3RvL2ltYWdlLmpwZyJ9LHsiQ29udGVudC1UeXBlIjoiaW1hZ2UvanBlZyJ9LFsiY29udGVudC1sZW5ndGgtcmFuZ2UiLDEyMzQsMTIzNF0seyJhY2wiOiJwdWJsaWMtcmVhZCJ9LHsic3VjY2Vzc19hY3Rpb25fc3RhdHVzIjoiMjAxIn0seyJ4LWFtei1hbGdvcml0aG0iOiJBV1M0LUhNQUMtU0hBMjU2In0seyJ4LWFtei1jcmVkZW50aWFsIjoiYWNjZXNza2V5aWQvMjAyNDAxMDQvYXAtbm9ydGhlYXN0LTEvczMvYXdzNF9yZXF1ZXN0In0seyJ4LWFtei1kYXRlIjoiMjAyNDAxMDRUMTQxMjMyWiJ9XSwiZXhwaXJhdGlvbiI6IjIwMjQtMDEtMDRUMTQ6MTM6MzIuMDAwWiJ9", result)
31 |
32 | }
33 |
34 | "buildDateRegionKey" {
35 | ENVIRON["AWS_REGION"] = "ap-northeast-1"
36 | ENVIRON["AWS_SECRET_ACCESS_KEY"] = "secretkey"
37 | awss3::loadEnviron()
38 |
39 | assertEqual("835ae2b2d0f4fbecaf27c58d608b4ffa7d1c248bf0d908255c27f9882926c731", awss3::buildDateRegionKey(1704377552))
40 | }
41 |
42 | "sign" {
43 | ENVIRON["AWS_ACCESS_KEY_ID"] = "accesskeyid"
44 | ENVIRON["AWS_BUCKET"] = "bucketname"
45 | ENVIRON["AWS_REGION"] = "ap-northeast-1"
46 | ENVIRON["AWS_SECRET_ACCESS_KEY"] = "secretkey"
47 | awss3::loadEnviron()
48 |
49 | result = awss3::sign("eyJleHBpcmF0aW9uIjoiMjAyNC0wMS0wNFQxNDoxMzozMi4wMDBaIiwiY29uZGl0aW9ucyI6W3siYnVja2V0IjoiYnVja2V0bmFtZSJ9LHsia2V5IjoicGF0aFwvdG9cL2ltYWdlLmpwZyJ9LHsiQ29udGVudC1UeXBlIjoiaW1hZ2VcL2pwZWcifSxbImNvbnRlbnQtbGVuZ3RoLXJhbmdlIiwxMjM0LDEyMzRdLHsiYWNsIjoicHVibGljLXJlYWQifSx7InN1Y2Nlc3NfYWN0aW9uX3N0YXR1cyI6IjIwMSJ9LHsieC1hbXotYWxnb3JpdGhtIjoiQVdTNC1ITUFDLVNIQTI1NiJ9LHsieC1hbXotY3JlZGVudGlhbCI6ImFjY2Vzc2tleWlkXC8yMDI0MDEwNFwvYXAtbm9ydGhlYXN0LTFcL3MzXC9hd3M0X3JlcXVlc3QifSx7IngtYW16LWRhdGUiOiIyMDI0MDEwNFQxNDEyMzJaIn1dfQ==", 1704377552)
50 | assertEqual("9025496f853e2eadf981e9a3a86a013919e553f1859340b4ce9e50c711f8dc83", result)
51 |
52 | }
53 |
54 | "sign" {
55 | ENVIRON["AWS_ACCESS_KEY_ID"] = "accesskeyid"
56 | ENVIRON["AWS_BUCKET"] = "bucketname"
57 | ENVIRON["AWS_REGION"] = "ap-northeast-1"
58 | ENVIRON["AWS_SECRET_ACCESS_KEY"] = "secretkey"
59 | awss3::loadEnviron()
60 |
61 | result = awss3::sign("eyJleHBpcmF0aW9uIjoiMjAyNC0wMS0wNFQxNDoxMzozMi4wMDBaIiwiY29uZGl0aW9ucyI6W3siYnVja2V0IjoiYnVja2V0bmFtZSJ9LHsia2V5IjoicGF0aFwvdG9cL2ltYWdlLmpwZyJ9LHsiQ29udGVudC1UeXBlIjoiaW1hZ2VcL2pwZWcifSxbImNvbnRlbnQtbGVuZ3RoLXJhbmdlIiwxMjM0LDEyMzRdLHsiYWNsIjoicHVibGljLXJlYWQifSx7InN1Y2Nlc3NfYWN0aW9uX3N0YXR1cyI6IjIwMSJ9LHsieC1hbXotYWxnb3JpdGhtIjoiQVdTNC1ITUFDLVNIQTI1NiJ9LHsieC1hbXotY3JlZGVudGlhbCI6ImFjY2Vzc2tleWlkXC8yMDI0MDEwNFwvYXAtbm9ydGhlYXN0LTFcL3MzXC9hd3M0X3JlcXVlc3QifSx7IngtYW16LWRhdGUiOiIyMDI0MDEwNFQxNDEyMzJaIn1dfQ", 1704377552)
62 | assertEqual("8066001e912f98d4b2cff80ce945eff014fb0535a21326a2858fbc3ac2ff484d", result)
63 |
64 | }
65 |
66 | "buildPreSignedUploadParams" {
67 | ENVIRON["AWS_ACCESS_KEY_ID"] = "accesskeyid"
68 | ENVIRON["AWS_BUCKET"] = "bucketname"
69 | ENVIRON["AWS_REGION"] = "ap-northeast-1"
70 | ENVIRON["AWS_SECRET_ACCESS_KEY"] = "secretkey"
71 | ENVIRON["S3_BUCKET_ENDPOINT"] = "https://bucketname.s3.amazonaws.com"
72 | ENVIRON["S3_ASSET_HOST"] = "https://bucketname.s3.amazonaws.com"
73 | awss3::loadEnviron()
74 |
75 | result = awss3::buildPreSignedUploadParams(1704377552, "path/to/image.jpg", "image/jpeg", 1234, 1234)
76 | assertEqual("{\"public_url\":\"https://bucketname.s3.amazonaws.com/path/to/image.jpg\",\"data\":{\"x-amz-algorithm\":\"AWS4-HMAC-SHA256\",\"acl\":\"public-read\",\"key\":\"path/to/image.jpg\",\"Content-Type\":\"image/jpeg\",\"x-amz-credential\":\"accesskeyid/20240104/ap-northeast-1/s3/aws4_request\",\"bucket\":\"bucketname\",\"policy\":\"eyJjb25kaXRpb25zIjpbeyJidWNrZXQiOiJidWNrZXRuYW1lIn0seyJrZXkiOiJwYXRoL3RvL2ltYWdlLmpwZyJ9LHsiQ29udGVudC1UeXBlIjoiaW1hZ2UvanBlZyJ9LFsiY29udGVudC1sZW5ndGgtcmFuZ2UiLDEyMzQsMTIzNF0seyJhY2wiOiJwdWJsaWMtcmVhZCJ9LHsic3VjY2Vzc19hY3Rpb25fc3RhdHVzIjoiMjAxIn0seyJ4LWFtei1hbGdvcml0aG0iOiJBV1M0LUhNQUMtU0hBMjU2In0seyJ4LWFtei1jcmVkZW50aWFsIjoiYWNjZXNza2V5aWQvMjAyNDAxMDQvYXAtbm9ydGhlYXN0LTEvczMvYXdzNF9yZXF1ZXN0In0seyJ4LWFtei1kYXRlIjoiMjAyNDAxMDRUMTQxMjMyWiJ9XSwiZXhwaXJhdGlvbiI6IjIwMjQtMDEtMDRUMTQ6MTM6MzIuMDAwWiJ9\",\"x-amz-signature\":\"025c2c3800b438b4e8633b0a925f260b5c4b1a3d48fd8c6b18965a902a32d414\",\"x-amz-date\":\"20240104T141232Z\",\"success_action_status\":\"201\"},\"upload_url\":\"https://bucketname.s3.amazonaws.com\"}", result)
77 | }
78 |
--------------------------------------------------------------------------------
/test/lib/base64.awk:
--------------------------------------------------------------------------------
1 | @include "src/lib/logger.awk"
2 | @include "src/lib/shell.awk"
3 | @include "src/lib/base64.awk"
4 | @include "test/testutil.awk"
5 | @include "src/lib/environ.awk"
6 |
7 | "base64Encode" {
8 | encoded = base64::encode("hello, world!")
9 | assertEqual("aGVsbG8sIHdvcmxkIQ==", encoded)
10 | }
11 |
12 | "base64Decode" {
13 | decoded = base64::decode("aGVsbG8sIHdvcmxkIQ==")
14 | assertEqual("hello, world!", decoded)
15 | }
16 |
17 | "base64EncodeAndDecode" {
18 | encoded = base64::encode("secret information")
19 | decoded = base64::decode(encoded)
20 | assertEqual("secret information", decoded)
21 | }
22 |
23 | "base64ToUrlsafe" {
24 | base64 = "U2FsdGVkX1+T/ZtrrVXETUKzKs0DjeeKSeAEF6G+rdY="
25 | base64url = "U2FsdGVkX1-T_ZtrrVXETUKzKs0DjeeKSeAEF6G-rdY"
26 | assertEqual(base64url, base64::urlsafe(base64))
27 | }
28 |
29 | "base64ToUrlunsafe" {
30 | base64 = "U2FsdGVkX1+T/ZtrrVXETUKzKs0DjeeKSeAEF6G+rdY="
31 | base64url = "U2FsdGVkX1-T_ZtrrVXETUKzKs0DjeeKSeAEF6G-rdY"
32 | assertEqual(base64, base64::urlunsafe(base64url))
33 | }
34 |
--------------------------------------------------------------------------------
/test/lib/hmac.awk:
--------------------------------------------------------------------------------
1 | @include "test/testutil.awk"
2 | @include "src/lib/shell.awk"
3 | @include "src/lib/logger.awk"
4 | @include "src/lib/json.awk"
5 | @include "src/lib/base64.awk"
6 | @include "src/lib/hmac.awk"
7 | @include "src/lib/environ.awk"
8 |
9 | "hashHex" {
10 | assertEqual("a0f43328cd36e3d02970fafc2649f4b43edb81b399f615e334be2b4c3ec14302", hmac::sha256("20240104", "key:AWS4secretKey"))
11 | }
12 |
13 | "hmacTwice" {
14 | assertEqual("195f75652ec679550c3eea897a42ccb812d76991c5e5803aab1f2c01d99e6412", hmac::sha256("ap-northeast-1", "hexkey:a0f43328cd36e3d02970fafc2649f4b43edb81b399f615e334be2b4c3ec14302"))
15 |
16 | }
17 |
18 |
19 |
--------------------------------------------------------------------------------
/test/lib/http.awk:
--------------------------------------------------------------------------------
1 | @include "src/lib/http.awk"
2 | @include "src/lib/environ.awk"
3 | @include "test/testutil.awk"
4 |
5 | "buildResponse" {
6 | assertEqual("HTTP/1.1 200 OK\n\nok", http::buildResponse(200, "ok"))
7 | }
8 |
--------------------------------------------------------------------------------
/test/lib/router.awk:
--------------------------------------------------------------------------------
1 | @include "src/lib/router.awk"
2 | @include "test/testutil.awk"
3 |
4 | "wildcard_compress" {
5 | replaced = router::wildcard_compress("/hello/world", 1)
6 | assertEqual("/*/world", replaced)
7 |
8 | replaced = router::wildcard_compress("/hello/world", 2)
9 | assertEqual("/hello/*", replaced)
10 |
11 | replaced = router::wildcard_compress("/@yammerjp", 1)
12 | assertEqual("/*", replaced)
13 |
14 | }
15 |
16 | "routing" {
17 | router::register("GET", "/", "controller::get")
18 | router::register("GET", "/test", "controller::test__get")
19 | router::register("GET", "/login", "controller::login__get")
20 | router::register("GET", "/oauth-callback", "controller::oauth_callback__get")
21 | router::register("GET", "/authed", "controller::authed__get")
22 | router::register("GET", "/authed/posts/new", "controller::authed__posts__new__get")
23 | router::register("GET", "/authed/posts", "controller::authed__posts__get")
24 | router::register("POST", "/authed/posts", "controller::authed__posts__post")
25 | router::register("GET", "/*", "controller::_account_id__get")
26 | router::register("GET", "/accounts/*/posts/*", "controller::_accounts__account_id__posts__post_id__get")
27 | router::register_notfound("controller::notfound")
28 |
29 | assertEqual("controller::authed__posts__post", router::find("POST", "/authed/posts"))
30 | assertEqual("controller::_account_id__get", router::find("GET", "/@yammerjp"))
31 | assertEqual("controller::notfound", router::find("POST", "/path/to/notfound"))
32 | assertEqual("controller::_accounts__account_id__posts__post_id__get", router::find("GET", "/accounts/13/posts/15"))
33 | }
34 |
--------------------------------------------------------------------------------
/test/lib/shell.awk:
--------------------------------------------------------------------------------
1 | @include "src/lib/shell.awk"
2 | @include "src/lib/logger.awk"
3 | @include "test/testutil.awk"
4 | @include "src/lib/environ.awk"
5 |
6 | {
7 | assertEqual("hoge\nfuga", shell::exec("echo \"hoge\nfuga\""))
8 | }
9 |
--------------------------------------------------------------------------------
/test/lib/url.awk:
--------------------------------------------------------------------------------
1 | @include "src/lib/url.awk"
2 | @include "src/lib/logger.awk"
3 | @include "src/lib/environ.awk"
4 | @include "src/lib/error.awk"
5 | @include "test/testutil.awk"
6 |
7 | "decodeUtf8ParcentEncoding" {
8 | assertEqual("hello", url::decodeUtf8ParcentEncoding("hello"))
9 | assertEqual(" ", url::decodeUtf8ParcentEncoding("%20"))
10 | assertEqual("あ", url::decodeUtf8ParcentEncoding("%E3%81%82"))
11 | assertEqual("😀", url::decodeUtf8ParcentEncoding("%F0%9F%98%80"))
12 | }
13 |
14 | "decodewwwform" {
15 | url::decodeWwwForm(result, "foo=bar&hello=world&jp=%e3%81%82")
16 | assertEqual(3, length(result))
17 | assertEqual("bar", result["foo"])
18 | assertEqual("world", result["hello"])
19 | assertEqual("あ", result["jp"])
20 | }
21 |
--------------------------------------------------------------------------------
/test/lib/uuid.awk:
--------------------------------------------------------------------------------
1 | @include "src/lib/uuid.awk"
2 | @include "src/lib/shell.awk"
3 | @include "src/lib/logger.awk"
4 | @include "test/testutil.awk"
5 | @include "src/lib/environ.awk"
6 |
7 | "uuid" {
8 | generated1 = uuid::gen()
9 | assertEqual(1, generated1 ~ /^[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}$/)
10 | }
11 |
--------------------------------------------------------------------------------
/test/template/components/head.html:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/test/template/dist/_account_name/get.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
10 |
11 |
12 | posts of
13 |
14 |
17 |
18 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/test/template/dist/_account_name/posts/_id/get.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
20 |
21 |
22 | タイトル 空白が 入っていても 大丈夫
23 | posted: 2023-12-23 11:25:00
24 |
25 | hello, world!
26 |
27 | foo
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/test/template/pages/_account_name/get.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | <%#include components/head.html %>
4 |
9 |
10 |
11 | posts of <%= v["account_name"] %>
12 | <% for(i in v["posts"]) { %>
13 |
16 | <% } %>
17 |
18 |
19 |
--------------------------------------------------------------------------------
/test/template/pages/_account_name/posts/_id/get.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
20 |
21 |
22 | <%= v["title"] %>
23 | posted: <%= v["created_at"] %>
24 |
25 | <%= v["content"] %>
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/test/templatetest.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -e
4 | set pipefail
5 |
6 | TEMPLATE_DIR="$(cd $(dirname $0); pwd)"
7 | cd "$TEMPLATE_DIR/template"
8 |
9 | COMPILED_TEMPLATE="/tmp/compiled_templates.awk"
10 |
11 | find ./ -type f | sed 's#^./##g'| awk -f ../../misc/compile_templates.awk > "$COMPILED_TEMPLATE"
12 |
13 | diff <(echo '' | awk -f "$COMPILED_TEMPLATE" -f <(echo '{
14 | v["posts"][1]["title"] = "タイトル 空白が 入っていても 大丈夫";
15 | v["posts"][1]["account_name"] = "yammerjp";
16 | v["posts"][1]["id"] = 33;
17 |
18 | v["posts"][2]["title"] = "次の記事";
19 | v["posts"][2]["account_name"] = "yammerjp";
20 | v["posts"][2]["id"] = 33;
21 |
22 | print compiled_templates::render("pages/_account_name/get.html", v)
23 | }')) dist/_account_name/get.html
24 |
25 |
26 | cmp <(echo '' | awk -f "$COMPILED_TEMPLATE" -f <(echo '{
27 | v["title"] = "タイトル 空白が 入っていても 大丈夫";
28 | v["content"] = "hello, world!\n\nfoo\n
";
29 | v["created_at"] = "2023-12-23 11:25:00";
30 |
31 | print compiled_templates::render("pages/_account_name/posts/_id/get.html", v)
32 | }')) dist/_account_name/posts/_id/get.html
33 |
--------------------------------------------------------------------------------
/test/testutil.awk:
--------------------------------------------------------------------------------
1 | function assertEqual(expected, needle) {
2 | if (expected == needle) {
3 | printf(".")
4 | } else {
5 | printf("expected: %s\nactual: %s\n", expected, needle)
6 | ExitCode = 1
7 | }
8 | }
9 |
10 | END {
11 | exit ExitCode
12 | }
13 |
--------------------------------------------------------------------------------
/test/unittest.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | REPOSITORY_ROOT="$(dirname "$0")/.."
4 | cd "$REPOSITORY_ROOT"
5 |
6 | export ENCRYPTION_KEY="passw0rd"
7 | export OAUTH_CLIENT_ID=xxxx
8 | export OAUTH_CLIENT_SECRET=xxxx
9 | export AWKBLOG_HOSTNAME="http://localhost:4567"
10 | export POSTGRES_HOSTNAME=db
11 | export POSTGRES_USER=postgres
12 | export POSTGRES_PASSWORD=passw0rd
13 | export POSTGRES_DATABASE=postgres
14 | export OAUTH_CALLBACK_URI="http://localhost:4567/oauth-callback"
15 | export GITHUB_API_SERVER="https://api.github.com"
16 | export GITHUB_LOGIN_SERVER="https://github.com"
17 | export AWS_REGION=ap-northeast-1
18 | export AWS_BUCKET=bucketname
19 | export AWS_ACCESS_KEY_ID=xxxx
20 | export AWS_SECRET_ACCESS_KEY=xxxx
21 | export S3_BUCKET_ENDPOINT="https://bucketname.s3.amazonaws.com"
22 | export S3_ASSET_HOST="https://bucketname.s3.amazonaws.com"
23 | export PORT="8080"
24 | export AWKBLOG_INTERNAL_HOSTNAME="127.0.0.1"
25 | unset NOT_USE_AWS_S3
26 |
27 |
28 | find test/lib -type f | grep -e '\.awk$' | while read awkfile
29 | do
30 | echo '' | gawk -f $awkfile
31 | done
32 |
--------------------------------------------------------------------------------
/view/components/_account_name/footer.html:
--------------------------------------------------------------------------------
1 |
8 |
9 |
--------------------------------------------------------------------------------
/view/components/_account_name/head.html:
--------------------------------------------------------------------------------
1 |
2 | /style.css">
3 | " href="/@<%= v["account_name"] %>/rss.xml" />
4 |
5 |
--------------------------------------------------------------------------------
/view/components/_account_name/header.html:
--------------------------------------------------------------------------------
1 |
11 |
12 |
--------------------------------------------------------------------------------
/view/components/authed/article_editor.html:
--------------------------------------------------------------------------------
1 | " class="title-input">
2 |
3 |
4 |
7 |
8 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
116 |
117 |
118 |
119 |
188 |
--------------------------------------------------------------------------------
/view/components/authed/console_head.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
55 |
56 |
--------------------------------------------------------------------------------
/view/components/authed/header.html:
--------------------------------------------------------------------------------
1 |
38 |
39 |
134 |
135 |
--------------------------------------------------------------------------------
/view/i18n/en.yaml:
--------------------------------------------------------------------------------
1 | logout: Logout
2 | view_blog: View Blog
3 | blog_settings: Blog Settings
4 | new_post: New Post
5 | posts: Posts
6 | style_settings: Style Settings
7 | confirm_delete: Are you sure you want to delete this post? This action cannot be undone.
8 | blog_settings: Blog Settings
9 | blog_settings_description: You can change the title, description, and author profile that are displayed at the top of the public page.
10 | style_settings_title: Style Settings
11 | style_settings_description: You can change the CSS that is loaded on all pages of the public blog.
12 | console_title: hello, awkblog!
13 | descriptive_list_posts: View Posts
14 | descriptive_new_post: New Post
15 | descriptive_blog_settings: Blog Settings
16 | descriptive_style_settings: Style Settings
17 | not_found: 404 Not Found
18 | welcome: hello, awkblog!
19 | welcome_message_1: A blog system implemented with awk.
20 | welcome_message_2: Sign in with your GitHub account and start using it!
21 | placeholder_blog_title: Blog Title
22 | placeholder_blog_description: Blog Description
23 | placeholder_author_profile: Author Profile
24 |
--------------------------------------------------------------------------------
/view/i18n/ja.yaml:
--------------------------------------------------------------------------------
1 | logout: ログアウト
2 | view_blog: ブログを確認
3 | blog_settings: ブログ設定
4 | new_post: 新規投稿
5 | posts: 記事一覧
6 | style_settings: スタイル設定
7 | confirm_delete: 記事を削除しても良いですか?この操作は取り消せません
8 | blog_settings: ブログ設定
9 | blog_settings_description: 公開ページの上部に表示されるタイトルや説明、公開ページの下部に紹介される著者の自己紹介の内容を変更できます。
10 | style_settings_title: スタイル設定
11 | style_settings_description: 公開されるブログの全てのページで読み込まれるCSSを変更することができます。
12 | console_title: hello, awkblog!
13 | descriptive_list_posts: 記事の一覧をみる
14 | descriptive_new_post: 記事を投稿する
15 | descriptive_blog_settings: ブログのタイトルや著者欄を変更する
16 | descriptive_style_settings: 公開ページのスタイルを変更する
17 | not_found: 404 Not Found
18 | welcome: hello, awkblog!
19 | welcome_message_1: gawkで実装されたブログシステムです。
20 | welcome_message_2: GitHubアカウントで早速ログインし、使ってみましょう!
21 | placeholder_blog_title: ブログのタイトル
22 | placeholder_blog_description: ブログの説明
23 | placeholder_author_profile: 著者の自己紹介
24 |
--------------------------------------------------------------------------------
/view/pages/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | <%= t["not_found"] %>
5 |
6 |
7 |
--------------------------------------------------------------------------------
/view/pages/_account_name/get.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | <%#include components/_account_name/head.html %>
4 |
5 |
6 | <%#include components/_account_name/header.html %>
7 |
8 | <% for(i in v["posts"]) { %>
9 |
18 | <% } %>
19 |
20 | <%#include components/_account_name/footer.html %>
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/view/pages/_account_name/posts/_id/get.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | <%#include components/_account_name/head.html %>
4 |
5 |
6 | <%#include components/_account_name/header.html %>
7 |
8 | <%= v["title"] %>
9 | <%= v["created_at"] %>
10 |
11 | <%= v["content"] %>
12 |
13 |
14 | <%#include components/_account_name/footer.html %>
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/view/pages/_account_name/rss.xml/get.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | <%= v["account_url"] %>
4 | <%= v["blog_title"] %>
5 | <%= v["blog_description"] %>
6 | <%= v["copyright"] %>
7 | https://www.rssboard.org/rss-specification
8 | awkblog <%= awk::getAwkblogVersion() %>
9 | ja
10 | <%= awk::strftime() %>
11 | <% for(key in v["posts"]) { %>
12 | -
13 | <%= v["posts"][key]["title"] %>
14 | <%= v["account_url"] %>/posts/<%= v["posts"][key]["id"] %>
15 | <%= v["posts"][key]["content"] %>
16 | <%= v["account_url"] %>/posts/<%= v["posts"][key]["id"] %>
17 | <%= v["posts"][key]["created_at"] %>
18 |
19 | <% } %>
20 |
21 |
22 |
--------------------------------------------------------------------------------
/view/pages/authed/get.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | <%#include components/authed/console_head.html %>
5 |
6 |
7 | <%#include components/authed/header.html %>
8 |
9 | <%= t["console_title"] %>
10 | <%= t["descriptive_list_posts"] %>: /authed/posts
11 | <%= t["descriptive_new_post"] %>: /authed/posts/new
12 | <%= t["descriptive_blog_settings"] %>: /authed/settings
13 | <%= t["descriptive_style_settings"] %>: /authed/styles
14 |
15 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/view/pages/authed/posts/edit/get.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | <%#include components/authed/console_head.html %>
5 |
6 |
7 | <%#include components/authed/header.html %>
8 |
9 |
13 |
14 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/view/pages/authed/posts/get.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | <%#include components/authed/console_head.html %>
5 |
6 |
7 | <%#include components/authed/header.html %>
8 |
9 | posts
10 | <% for(i in v["posts"]) { %>
11 |
12 | <%= v["posts"][i]["title"] %>
13 | <%= v["posts"][i]["created_at"] %>
14 | <%= v["posts"][i]["content"] %>
15 |
16 |
20 |
24 |
25 |
26 |
27 | <% } %>
28 |
29 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/view/pages/authed/posts/new/get.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | <%#include components/authed/console_head.html %>
5 |
6 |
7 | <%#include components/authed/header.html %>
8 |
9 |
12 |
13 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/view/pages/authed/settings/get.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | <%#include components/authed/console_head.html %>
5 |
6 |
7 | <%#include components/authed/header.html %>
8 |
9 | <%= t["blog_settings"] %>
10 |
11 | <%= t["blog_settings_description"] %>
12 |
13 |
19 |
20 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/view/pages/authed/styles/get.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | <%#include components/authed/console_head.html %>
5 |
11 |
12 |
13 | <%#include components/authed/header.html %>
14 |
15 | <%= t["style_settings"] %>
16 |
17 | <%= t["style_settings_description"] %>
18 |
19 |
23 |
24 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/view/pages/get.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | <%#include components/authed/console_head.html %>
5 |
6 |
7 | <%#include components/authed/header.html %>
8 |
9 | <%= t["welcome"] %>
10 |
11 | <%= t["welcome_message_1"] %>
12 |
13 |
14 | <%= t["welcome_message_2"] %>
15 |
16 |
17 |
19 |
20 |
21 |
--------------------------------------------------------------------------------