Skip to content

Instantly share code, notes, and snippets.

@thejsa
Created January 28, 2018 21:55
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save thejsa/ae3c11b4427cb204bdce648f4f477324 to your computer and use it in GitHub Desktop.
Save thejsa/ae3c11b4427cb204bdce648f4f477324 to your computer and use it in GitHub Desktop.
Quick and dirty Flipnote Hatena server (with query string sessions + basic authentication)
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule . /index.php [L]
<html>
<head>
<meta name="uppertitle" content="{{ pageTitle|default('kaeru:memo') }}">
{% block pageHeadCode %}{% endblock %}
</head>
<body>
{% block pageContent %}{% endblock %}
</body>
</html>
{
"require": {
"klein/klein": "^2.1",
"twig/twig": "^2.4"
}
}
<?php
/*
Quick and dirty Flipnote Hatena server core
Features authentication via proxy basic authentication, and sessions
with IDs passed through query strings since Hatena shut down their
auth server.
Move .html files to a views/ subdirectory.
Requires class.ugomenu.php by @jaames, <https://github.com/Sudomemo/sudomemo-utils/blob/master/php/class.ugomenu.php>
Database schema:
CREATE TABLE `users` (
`fsid` varchar(16) NOT NULL,
`username` varchar(16) NOT NULL,
`password` varchar(255) NOT NULL,
`bio` text,
`created` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
Copyright (c) 2018, thejsa <https://github.com/thejsa>
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
require './vendor/autoload.php'; // Composer -- modules used: klein/klein ; twig/twig
require 'class.ugomenu.php'; // Sudomemo/sudomemo-utils php/class.ugomenu.php
ini_set('display_errors', '1');
error_reporting(E_ALL & ~E_NOTICE);
$klein = new \Klein\Klein();
$klein->respond(function ($request, $response, $service, $app) use ($klein) {
// Start session
if(isset($_REQUEST['sessionID'])) session_id($_REQUEST['sessionID']);
session_start();
if(isset(apache_request_headers()['Proxy-Authorization'])) {
$service->sharedData()->set('credentials', explode(':', base64_decode(explode(' ', apache_request_headers()['Proxy-Authorization'])[1])));
} else {
$service->sharedData()->set('credentials', null);
}
// Error handler
$klein->onError(function ($klein, $err_msg) {
error_log($err_msg);
$klein->service()->flash($err_msg);
});
// Twig templating engine
$app->register('twig', function () {
$loader = new Twig_Loader_Filesystem('./views/');
$twig = new Twig_Environment($loader);
$twig->addGlobal('session', $_SESSION);
return $twig;
});
// Database
$app->register('db', function () {
return new mysqli('127.0.0.1', 'username', 'password', 'database'); // changeme
});
});
$klein->onHttpError(function ($code, $router) {
switch($code) {
case 404:
$router->response()->body('Not found - URI: ' . $router->request()->uri());
break;
case 405:
$router->response()->body(
'You can\'t do that!'
);
break;
default:
$router->response()->body(
'An HTTP ' . $code . ' error occured :\'('
);
break;
}
});
$klein->with('http://flipnote.hatena.com/ds/v2-[a:region]', function () use ($klein) {
$klein->respond('/', function ($request, $response, $service, $app) {
return 'Hello, world! I don\'t know what you\'re expecting to see here. ~ jsa 2018-01-27';
});
$klein->respond('/auth', function ($request, $response, $service, $app) {
// auth response
$response->code(401); // glitches the DSi or smth?
$response->header('X-DSi-New-Notices', '1');
$response->header('X-DSi-Unread-Notices', '1');
$response->header('X-DSi-SID', 'temporary');
$response->header('X-DSi-Dialog-Type', '0');
// return mb_convert_encoding('Hello! Please press the button again', 'utf-16le');
// return 'bloop';
});
$klein->respond('/[en|fr|es|it|nl|jp:language]/[:confirm]?/[:file].txt', function ($request, $response, $service, $app) {
//
$response->header('Content-Type', 'text/plain; charset=UTF-8');
return mb_convert_encoding('I am '.$request->file.'.txt', 'utf-16le');
});
$klein->respond('/eula_list.tsv', function ($request, $response, $service, $app) {
//
$response->header('Content-Type', 'text/tab-separated-values');
return "RQBuAGcAbABpAHMAaAA=\ten";
});
$klein->respond('/index.ugo', function ($request, $response, $service, $app) {
if(!is_array($service->sharedData()->get('credentials'))) {
// create account flow
$ugomenu = new ugomenu();
$ugomenu->setType('index');
$ugomenu->addItem(array(
'url' => 'http://flipnote.hatena.com/ds/v2-eu/begin.html?sessionID='.session_id(),
'label' => 'Get started',
'icon' => '104'
));
return $ugomenu->getUGO();
}
$stmt = $app->db->prepare('SELECT * FROM `users` WHERE `username` = ?');
$stmt->bind_param('s', $service->sharedData()->get('credentials')[0]);
if(!$stmt):
error_log($app->db->error);
die('database error: '.$app->db->error);
endif;
if(!$stmt->execute()) {
error_log('failed to execute $stmt on login');
die('database error: failed to execute stmt on login');
}
$user = $stmt->get_result()->fetch_all(MYSQLI_ASSOC)[0];
if(!$user) {
// create account flow
$ugomenu = new ugomenu();
$ugomenu->setType('index');
$ugomenu->addItem(array(
'url' => 'http://flipnote.hatena.com/ds/v2-eu/account/link.html?sessionID='.session_id(),
'label' => 'Setup account',
'icon' => 104
));
return $ugomenu->getUGO();
}
$_SESSION['user'] = $user;
$ugomenu = new ugomenu();
$ugomenu->setType('index');
$ugomenu->addItem(array(
'url' => 'http://flipnote.hatena.com/ds/v2-eu/user/'.$user['fsid'].'.html?sessionID='.session_id(),
'label' => 'Hello, ' . $user['username'] . '!',
'icon' => 104
));
return $ugomenu->getUGO();
});
$klein->with('/user.[html]?', function () use ($klein) {
$klein->respond('GET', '/?', function ($request, $response, $service, $app) {
return $app->twig->render('user.html', array (
'user' => $_SESSION['user']
));
});
$klein->respond('GET', '/[:id].[html]?', function ($request, $response, $service, $app) {
$identifier = $request->id;
$stmt = $app->db->prepare('SELECT * FROM `users` WHERE `fsid` = ? OR `username` LIKE ? LIMIT 1');
$stmt->bind_param('ss', $identifier, $identifier);
if(!$stmt):
error_log($app->db->error);
die($app->db->error);
endif;
if(!$stmt->execute()){
error_log('failed to execute $stmt: ' . $app->db->error);
die('failed to execute $stmt');
}
$user = $stmt->get_result()->fetch_all(MYSQLI_ASSOC)[0];
if(!$user) throw Klein\Exceptions\HttpException::createFromCode(404);
return $app->twig->render('user.html', array (
'user' => $user
));
});
});
$klein->respond('/hello.html', function ($request, $response, $service, $app) {
//
$response->header('Content-Type', 'text/html; charset=UTF-8');
$proxyAuth = explode(':', base64_decode(explode(' ', apache_request_headers()['Proxy-Authorization'])[1])); // [0] username [1] password
return $app->twig->render('user.html', array (
'user' => array (
'username' => $proxyAuth[0],
'password' => $proxyAuth[1]
)
));
});
$klein->respond('/store.kbd', function ($request, $response, $service, $app) {
//
$response->header('Content-Type', 'text/html; charset=UTF-8');
$proxyAuth = explode(':', base64_decode(explode(' ', apache_request_headers()['Proxy-Authorization'])[1])); // [0] username [1] password
return '<html><head><title>hello</title></head><body><p>Hello '.$proxyAuth[0].'! The password you entered was: '.$proxyAuth[1].'</p><br><p>Session ID: '.session_id().'</body></html>';
});
});
$klein->dispatch();
{% set pageTitle = user.username ~ '\'s profile' %}
{% extends '_layout.html' %}
{% block pageContent %}
<p>User name: {{ user.username }}</p>
<p>User FSID: {{ user.fsid }}</p>
<p>User Bio: {{ user.bio }}</p>
{% if session.user.fsid == user.fsid %}<p>(This is you!)</p>{% endif %}
{% endblock %}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment