OpenVPNサーバにMacOSXで接続(Tunnelblick)

はじめに

前回投稿したさくらのVPS512(月々税抜635円)で固定IPアドレスをゲットする(OpenVPNサーバを作る)ではWindowsで接続する例を記載しました。 今回は追記として、MacOSXから接続する手順をまとめました。

なお、上記の投稿で設定したOpenVPNサーバとの接続を想定しており、他のサーバとの接続においては設定変更が必要となる可能性があります。

作業手順

設定ファイルの新規作成

各証明書・秘密鍵と同じディレクトリに設定ファイルを新規作成します。設定ファイルの内容は後述しますが、配置のイメージは下記の通りです。 スクリーンショット 2016-02-08 2.44.47.png

ではテキストエディタ等で設定ファイルを作りましょう。今回は"myvpn.ovpn"とします。拡張子は必ず.ovpnとしてください。

client

dev tun
proto udp

remote ホスト名もしくはIPアドレス 1194

resolv-retry infinite
nobind

persist-key
persist-tun

ca ca.crt
cert user.crt
key user.key

fragment 1280
mssfix 1280
link-mtu 1400

comp-lzo

verb 3

Tunnelblickのインストール

Tunnelblickをインストールしてください。 https://tunnelblick.net/downloads.html

設定ファイルの登録

先ほど作成した"myvpn.ovpn"をダブルクリックします。 Tunnelblickが設定のインストール方法を尋ねてきます。都合のよい範囲を指定してください。 スクリーンショット 2016-02-08 2.45.18.png 設定変更時の確認のダイアログが出たら認証してください。 スクリーンショット 2016-02-08 2.45.31.png

接続の確認

Tunnelblickを起動します。myvpnという設定が取り込まれていることを確認して「接続」します。 スクリーンショット 2016-02-08 2.47.19.png

初回はパスワードを尋ねられますので、ユーザ個別のパスワードを入力してください。 スクリーンショット 2016-02-08 2.49.11.png

接続が完了したら、IPアドレス確認用のサイトに行って確認してみましょう。 さくらのVPSで与えられたサーバのIPアドレスになっていたら成功です。 IPアドレス確認|Web便利ツール@ツールタロウ dcffa2af-664e-06bd-b7da-7b09917c0fab.png

Webツールも公開しています。 Web便利ツール@ツールタロウ

Java SSHでコマンド実行(known_hosts不要)

はじめに

事前に以下のライブラリを用意します。

実装例

サンプルでは、動作確認しやすいようにmainメソッドで実行できるようにしてあります。

import com.jcraft.jsch.ChannelExec;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;
import com.jcraft.jsch.SftpException;
import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.UnsupportedEncodingException;

/**
 *
 * @author tool-taro.com
 */
public class SSHTest {

    public static void main(String[] args) throws JSchException, SftpException, UnsupportedEncodingException, FileNotFoundException, IOException {

        //サーバ
        String host = "ホスト名";
        //ポート
        int port = 22;
        //ユーザ
        String user = "ユーザ";
        //パスワード
        String password = "パスワード";
        //コマンド
        String command = "date";

        JSch jsch;
        Session session = null;
        ChannelExec channel = null;
        BufferedInputStream bin = null;

        try {
            //接続
            jsch = new JSch();
            session = jsch.getSession(user, host, port);
            //known_hostsのチェックをスキップ
            session.setConfig("StrictHostKeyChecking", "no");
            session.setPassword(password);
            session.connect();

            channel = (ChannelExec) session.openChannel("exec");
            channel.setCommand(command);
            channel.connect();

            //コマンド実行
            bin = new BufferedInputStream(channel.getInputStream());
            ByteArrayOutputStream bout = new ByteArrayOutputStream();
            byte[] buf = new byte[1024];
            int length;
            while (true) {
                length = bin.read(buf);
                if (length == -1) {
                    break;
                }
                bout.write(buf, 0, length);
            }
            //標準出力
            System.out.format("実行結果=%1$s", new String(bout.toByteArray(), "UTF-8"));
        }
        finally {
            if (bin != null) {
                try {
                    bin.close();
                }
                catch (Exception e) {
                }
            }
            if (channel != null) {
                try {
                    channel.disconnect();
                }
                catch (Exception e) {
                }
            }
            if (session != null) {
                try {
                    session.disconnect();
                }
                catch (Exception e) {
                }
            }
        }
    }
}

動作確認

$ javac SSHTest.java
$ java SSHTest
$ 実行結果=Mon Feb  8 01:26:47 JST 2016

環境

Webツールも公開しています。 Web便利ツール@ツールタロウ

Java SFTPでアップロード・ダウンロード(known_hosts不要)

はじめに

事前に以下のライブラリを用意します。

アップロードする以下のファイルを使います。

テストファイルです

実装例

今回は、アップロードしたファイルをダウンロードして内容確認することとします。 サンプルでは、動作確認しやすいようにmainメソッドで実行できるようにしてあります。

import com.jcraft.jsch.ChannelSftp;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;
import com.jcraft.jsch.SftpException;
import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.UnsupportedEncodingException;

/**
 *
 * @author tool-taro.com
 */
public class SFTPTest {

    public static void main(String[] args) throws JSchException, SftpException, UnsupportedEncodingException, FileNotFoundException, IOException {

        //サーバ
        String host = "ホスト名";
        //ポート
        int port = 22;
        //ユーザ
        String user = "ユーザ";
        //パスワード
        String password = "パスワード";
        //ディレクトリ
        String dir = "/ディレクトリ";
        //アップロード・ダウンロードするファイル名
        String fileName = "put.txt";

        JSch jsch;
        Session session = null;
        ChannelSftp channel = null;
        FileInputStream fin = null;
        BufferedInputStream bin = null;

        try {
            //接続
            jsch = new JSch();
            session = jsch.getSession(user, host, port);
            //known_hostsのチェックをスキップ
            session.setConfig("StrictHostKeyChecking", "no");
            session.setPassword(password);
            session.connect();

            channel = (ChannelSftp) session.openChannel("sftp");
            channel.connect();
            //ディレクトリ移動
            channel.cd(dir);

            //アップロード
            fin = new FileInputStream(fileName);
            channel.put(fin, fileName);

            //ダウンロード
            bin = new BufferedInputStream(channel.get(fileName));
            ByteArrayOutputStream bout = new ByteArrayOutputStream();
            byte[] buf = new byte[1024];
            int length;
            while (true) {
                length = bin.read(buf);
                if (length == -1) {
                    break;
                }
                bout.write(buf, 0, length);
            }
            //標準出力
            System.out.format("ダウンロードしたファイル=%1$s", new String(bout.toByteArray(), "UTF-8"));
        }
        finally {
            if (fin != null) {
                try {
                    fin.close();
                }
                catch (Exception e) {
                }
            }
            if (bin != null) {
                try {
                    bin.close();
                }
                catch (Exception e) {
                }
            }
            if (channel != null) {
                try {
                    channel.disconnect();
                }
                catch (Exception e) {
                }
            }
            if (session != null) {
                try {
                    session.disconnect();
                }
                catch (Exception e) {
                }
            }
        }
    }
}

動作確認

$ javac SFTPTest.java
$ java SFTPTest
$ 取得したファイル=テストファイルです

環境

Webツールも公開しています。 Web便利ツール@ツールタロウ

さくらのVPS512(月々税抜635円)で固定IPアドレスをゲットする(OpenVPNサーバを作る)

はじめに

固定IPサービスといえばマイIP(株式会社インターリンク)が有名でしょうか。 1アカウント1,000円(税抜)/月ですので、リーズナブルです。

しかし、もっと安価に使いたい&複数アカウントでも使いたい、というニーズもあると思います。 自分でサーバを管理していくという手間は発生しますが、 安価かつ柔軟に(≒複数人で)固定IPアドレスを使えるよう、 VPNサーバを立ち上げる手順をまとめます。

手順を作るにあたり詰まってしまった部分は、以下のWebページで解消できました(順不同)。 感謝申し上げます。 - VPNサーバー構築(OpenVPN) (サイト:CentOSで自宅サーバー構築) - OpenVPNで通信が遅い場合 (サイト:パソコン鳥のブログ)

作業前提(自己責任でご判断ください)

  • 今回は「さくらのVPS 512プラン(月々税抜635円 2016/02/06現在)」を使う
  • CentOS7.1
  • OpenVPN
  • ほかの機能(Webサーバ等)は一切設けない
  • 複数人で使えるようにする
  • クレジットカード決済にすると2週間お試しが可能(2016/02/06現在)
  • お試し期間中は回線速度制限などがあり充分には評価できない可能性がある
  • 説明の簡略化のため、ほぼすべてrootで作業
  • コマンドの入力、viによるファイルの編集などがあり、漏れなく作業が必要
  • 契約・請求・キャンセル方法等については、さくらインターネット株式会社からの案内に従ってください

問題なければ始めましょう。

作業手順

申込の開始

さくらのVPSの「2週間お試し無料!お申込みはこちら>」から申込を開始します。 VPS(仮想専用サーバ)|さくらインターネット - 無料お試し実施中.png

さくらの会員IDがない場合には新規登録してください。 SAKURA.AD.JP -- お申込み.png

リージョンとスペックの選択では、512プランを選択してください。 SAKURA.AD.JP -- お申込み.png

支払方法は、毎月払い+クレジットカードがおすすめです。 SAKURA.AD.JP -- お申込み.png

確認画面にも記載がありますが、無料期間中に解約すれば料金はかかりません(2016/02/06現在)。 SAKURA.AD.JP -- お申込み.png

仮登録完了通知の受信・コントロールパネルへのログイン

申込が確定してしばらく経過すると、「仮登録完了のお知らせ」という件名のメールが届きます。 VPSコントロールパネルにログインして状態を確認してみましょう。

 ▼サーバ情報

  [サーバ基本情報]
   IPアドレス:XXX.XXX.XXX.XXX

  [管理用ユーザ]
   ユーザ名      :XXXXXXXXXX
   初期パスワード:XXXXXXXXXX

  [VPSコントロールパネル ログイン情報]
     URL       :https://XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
     IPアドレス:XXX.XXX.XXX.XXX
     パスワード:XXXXXXXXXX

  ※ご提供時の初期状態ではサーバは停止状態になっていますので、利用を開始
    する際は「VPSコントロールパネル」にログインしサーバを起動して下さい。

さくらのVPSコントロールパネル.png

会員IDとパスワードを入力してログインします。 会員メニュー|さくらインターネット.png

サーバ一覧の確認

サーバ一覧に、作られたばかりのサーバが表示されています。初期状態は「停止中」です。 サーバ一覧 - さくらのVPSコントロールパネル.png

カスタムOSインストールの開始

サーバをクリックすると詳細な情報を表示する画面に遷移しますので、 「OSインストール」のプルダウンから「カスタムOSインストール」を選択してください。 名称未設定 - さくらのVPSコントロールパネル.png

インストールOSに「CentOS7 x86_64」を選択して次に進みます。 カスタムOSインストール(名称未設定) - さくらのVPSコントロールパネル.png

インストールが開始されると、コンソールを開いて作業を進めるように指示されます。 「VNCコンソール(HTML5版)を起動」で次に進みます。 名称未設定3 - さくらのVPSコントロールパネル.png

インストール初期画面が表示されていますので、エンターキーで次に進みます。 d7e6ec51-98f7-e144-f01e-3ce27670ca45.png

ここから先の画面は少し広くなりますので、ウィンドウを適宜広げてください。 まずは「LANGUAGE SUPPORT」で言語選択画面に入り、日本語を追加して確定(Done)します。 ff9289af-73d4-fac3-1b5c-a3d017b2391e.png 8dddfffe-99a8-eaca-012d-64a7f3ce0eb1.png

次に「INSTALLATION DESTINATION」でインストール先のディスク選択画面に入ります。 3f202718-b315-c200-a97b-e2da878aab8d.png

ここで、追加できるディスクはありませんのでいったん確定(Done)してください。 次のような画面が表示されます。 84de3fec-dcf2-7cff-f9cb-4e6ad411754b.png

もともとインストールされていた別のバージョンのCentOSがディスクを消費していて、 このままではインストールできません。「Reclaim space」で次に進みます。

今回は細かい設定をせず、「Delete all」を実行します。 a1bbe664-9a12-ecd8-6c33-4b26ace4eb0c.png

すべてのディスクのActionが「Delete」に変わったのを確認したら、「Reclaim space」を実行します。 d18ad7fb-2599-0e1e-c225-2628b26a063d.png

元の画面に戻ったら、「Begin Installation」で次に進みます。 d03b7a57-bd44-9d1c-8115-e99a240c2a33.png

インストールが始まります。待っている間に、「ROOT PASSWORD」を設定しておきましょう。 5b217d6f-a841-03ae-bd75-eb4def2bb1ef.png 5f614bf8-6e10-cbc8-7fc3-46024ad3de78.png

インストールが終わると、自動的にシャットダウンしますので、サーバの一覧に戻り、 サーバの左側にあるチェックボックスにチェックを入れて「起動」しましょう。 状態が「稼働中」に変化します。 サーバ一覧 - さくらのVPSコントロールパネル.png

SSHログイン

ここから先はSSH経由で作業します。 WindowsならTeraTermなど、Macならターミナルなどでログインしてください。 ログイン先のサーバはメールに記載されているIPアドレス、ユーザはroot、パスワードはインストール中に設定したパスワードです。

rootパスワードの設定

ここから先はすべてrootで作業します。 先ほど設定したrootのパスワードが気に入らない場合は修正しておきましょう。

$ passwd root
Changing password for user root.
New password:パスワード入力
Retype new password:パスワード再入力
passwd: all authentication tokens updated successfully.

SELinuxの無効化

"SELINUX=disabled"となっている場合は変更の必要がありません。先に進みましょう。

$ vi /etc/selinux/config
#SELINUX=enforcing
SELINUX=disabled

SSHログイン用ユーザの追加

SSHでログインする際、最初からrootでログインするのはセキュアではないため、 別のユーザを作り、パスワードを設定します。 今後は、別ユーザでログインしてからrootになる、という流れを作るわけです。

$ useradd sshuser
$ passwd sshuser
Changing password for user sshuser.
New password:パスワード入力
Retype new password:パスワード再入力
passwd: all authentication tokens updated successfully.

rootでのSSHログインの無効化

SSHでログインする際、rootでのログインを許可しないように設定します。

$ vi /etc/ssh/sshd_config
#PermitRootLogin yes
PermitRootLogin no

変更を反映します。

$ systemctl restart sshd.service

次回以降のログインの流れについて触れておきます。 先ほど作ったsshuserでログインした後、rootになるには以下のように入力します。

$ su -
Password:パスワード入力

sshuserでログインする流れを確認したところで、 SELinuxの設定変更の反映もしておきたいので、サーバを再起動します。

$ reboot

再度SSHでアクセスできるようになったら、sshuserでログインし、rootになります。

IPフォワーディングの有効化

$ vi /etc/sysctl.d/10-ipv4.conf
#追記
net.ipv4.ip_forward = 1

変更を反映します。

$ sysctl -p /etc/sysctl.d/10-ipv4.conf
net.ipv4.ip_forward = 1

openvpnのインストール

$ yum -y install epel-release
$ yum -y install openvpn
$ yum -y install wget

この後、設定をしていくわけですが、先にパスワードを1つ決めておいてください。 各種証明書の作成時などに使います。 パスワード(A)とします。

CA証明書・秘密鍵の作成

$ cd /usr/local/src/
$ wget https://github.com/OpenVPN/easy-rsa/archive/master.zip
$ unzip master.zip
$ cp -r easy-rsa-master/easyrsa3/ /etc/openvpn/
$ rm -fr easy-rsa-master/
$ cd /etc/openvpn/easyrsa3/
$ ./easyrsa init-pki

init-pki complete; you may now create a CA or requests.
Your newly created PKI dir is: /etc/openvpn/easyrsa3/pki

$ ./easyrsa build-ca
Generating a 2048 bit RSA private key
............................................................................................+++
.................................................+++
writing new private key to '/etc/openvpn/easyrsa3/pki/private/ca.key.XXXXXXXXXX'
Enter PEM pass phrase:パスワード(A)入力
Verifying - Enter PEM pass phrase:パスワード(A)再入力
-----
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Common Name (eg: your user, host, or server name) [Easy-RSA CA]:サーバ名入力(例: vpnserver)

CA creation complete and you may now import and sign cert requests.
Your new CA certificate file for publishing is at:
/etc/openvpn/easyrsa3/pki/ca.crt

$ cp pki/ca.crt /etc/openvpn/

サーバ証明書秘密鍵の作成

$ ./easyrsa build-server-full server nopass
Generating a 2048 bit RSA private key
...................................................+++
...............................................................................+++
writing new private key to '/etc/openvpn/easyrsa3/pki/private/server.key.e7omRJmwXu'
-----
Using configuration from /etc/openvpn/easyrsa3/openssl-1.0.cnf
Enter pass phrase for /etc/openvpn/easyrsa3/pki/private/ca.key:パスワード(A)入力
Check that the request matches the signature
Signature ok
The Subject's Distinguished Name is as follows
commonName            :PRINTABLE:'server'
Certificate is to be certified until Feb  3 04:11:41 2026 GMT (3650 days)

Write out database with 1 new entries
Data Base Updated

$ cp pki/issued/server.crt /etc/openvpn/
$ cp pki/private/server.key /etc/openvpn/

DHパラメータの作成

$ ./easyrsa gen-dh
Generating DH parameters, 2048 bit long safe prime, generator 2
This is going to take a long time
(省略)

DH parameters of size 2048 created at /etc/openvpn/easyrsa3/pki/dh.pem

$ cp pki/dh.pem /etc/openvpn/

クライアント証明書(ダミー)の作成

$ ./easyrsa build-client-full dmy nopass
Generating a 2048 bit RSA private key
..................+++
..................+++
writing new private key to '/etc/openvpn/easyrsa3/pki/private/dmy.key.XXXXXXXXXX'
-----
Using configuration from /etc/openvpn/easyrsa3/openssl-1.0.cnf
Enter pass phrase for /etc/openvpn/easyrsa3/pki/private/ca.key:パスワード(A)入力
Check that the request matches the signature
Signature ok
The Subject's Distinguished Name is as follows
commonName            :PRINTABLE:'dmy'
Certificate is to be certified until Feb  3 04:14:46 2026 GMT (3650 days)

Write out database with 1 new entries
Data Base Updated

$ ./easyrsa revoke dmy


Please confirm you wish to revoke the certificate with the following subject:

subject=
    commonName                = dmy


Type the word 'yes' to continue, or any other input to abort.
  Continue with revocation: yes
Using configuration from /etc/openvpn/easyrsa3/openssl-1.0.cnf
Enter pass phrase for /etc/openvpn/easyrsa3/pki/private/ca.key:パスワード(A)入力
Revoking Certificate 02.
Data Base Updated

IMPORTANT!!!

Revocation was successful. You must run gen-crl and upload a CRL to your
infrastructure in order to prevent the revoked cert from being accepted.

$ ./easyrsa gen-crl
Using configuration from /etc/openvpn/easyrsa3/openssl-1.0.cnf
Enter pass phrase for /etc/openvpn/easyrsa3/pki/private/ca.key:パスワード(A)入力

An updated CRL has been created.
CRL file: /etc/openvpn/easyrsa3/pki/crl.pem

$ cp pki/crl.pem /etc/openvpn/
$ chmod o+r /etc/openvpn/crl.pem

OpenVPNの設定

$ vi /etc/openvpn/server.conf
port 1194
proto udp
dev tun

ca ca.crt
cert server.crt
key server.key  # This file should be kept secret
dh dh.pem

server 192.168.100.0 255.255.255.0

ifconfig-pool-persist ipp.txt

push "redirect-gateway def1"
push "dhcp-option DNS 133.242.0.3" # sakura DNS1
push "dhsp-option DNS 133.242.0.4" # sakura DNS2

fragment 1280
mssfix 1280
link-mtu 1400

client-to-client

keepalive 10 120

comp-lzo

user nobody
group nobody

persist-key
persist-tun

status openvpn-status.log

log-append  /var/log/openvpn.log

verb 3

crl-verify crl.pem

サービスの登録・起動

$ systemctl enable openvpn@server.service
Created symlink from /etc/systemd/system/multi-user.target.wants/openvpn@server.service to /usr/lib/systemd/system/openvpn@.service.

$ systemctl start openvpn@server.service

Firewallの設定

$ firewall-cmd --permanent --zone=public --add-masquerade
$ firewall-cmd --permanent --zone=public --add-port=1194/udp
$ firewall-cmd --reload
$ firewall-cmd --list-all
public (default, active)
  interfaces: eth0
  sources:
  services: dhcpv6-client ssh
  ports: 1194/udp
  masquerade: yes
  forward-ports:
  icmp-blocks:
  rich rules:

クライアント証明書・秘密鍵の作成

複数ユーザ分を作る場合は繰り返してください。

$ cd /etc/openvpn/easyrsa3/
$ ./easyrsa build-client-full ユーザ名
Generating a 2048 bit RSA private key
...............................................................+++
...................................+++
writing new private key to '/etc/openvpn/easyrsa3/pki/private/ユーザ名.key.XXXXXXXXXX'
Enter PEM pass phrase:ユーザ個別のパスワード入力
Verifying - Enter PEM pass phrase:ユーザ個別のパスワード再入力
-----
Using configuration from /etc/openvpn/easyrsa3/openssl-1.0.cnf
Enter pass phrase for /etc/openvpn/easyrsa3/pki/private/ca.key:パスワード(A)入力
Check that the request matches the signature
Signature ok
The Subject's Distinguished Name is as follows
commonName            :T61STRING:'ユーザ名'
Certificate is to be certified until Feb  3 04:25:02 2026 GMT (3650 days)

Write out database with 1 new entries
Data Base Updated

$ cp /etc/openvpn/ca.crt /home/sshuser/
$ cp /etc/openvpn/easyrsa3/pki/issued/ユーザ名.crt /home/sshuser/
$ cp /etc/openvpn/easyrsa3/pki/private/ユーザ名.key /home/sshuser/
$ chown sshuser:sshuser /home/sshuser/ca.crt
$ chown sshuser:sshuser /home/sshuser/ユーザ名.crt
$ chown sshuser:sshuser /home/sshuser/ユーザ名.key

各証明書・秘密鍵のユーザへの配布

一つ前の手順で/home/sshuser以下に設置しましたので、 SFTPなどでログインしてダウンロードするとよいでしょう。

ca.crt
ユーザ名.crt
ユーザ名.key

また、下記の情報も伝えます。

ユーザ名
ユーザ個別のパスワード

クライアントの接続設定

ユーザの端末上でOpenVPNへの接続設定をします。 ここではWindows向けOpenVPNクライアントであるvpnux Clientを使った例を記載します。

プロファイルを新規に作成します。 無題.png 無題.png

下記の画面のように入力します。 無題.png

「CA証明書」の「...」を選択し、「ファイルから読み込み」を選択し、ca.crtを選択して「保存」します。 無題.png

「証明書」の「...」を選択し、「ファイルから読み込み」を選択し、ユーザ名.crtを選択して「保存」します。 無題.png

秘密鍵」の「...」を選択し、「ファイルから読み込み」を選択し、ユーザ名.keyを選択して「保存」します。 無題.png

「詳細設定」を選択し、以下のように設定します。 vpnux2.png

設定を保存したら最初の画面に戻り、いよいよ接続です。 e35934e3-57c4-55f6-65cd-d31f9eddc495.png

接続の確認

接続が完了したら、自分のIPアドレス確認用のサイトに行って確認してみましょう。 さくらのVPSで与えられたサーバのIPアドレスになっていたら成功です。 IPアドレス確認|Web便利ツール@ツールタロウ IPアドレス確認|Web便利ツール@ツールタロウ.png

なお、お試し期間中のサーバで回線速度制限がかかった状態でしたが、 Webサイトを閲覧する分には支障はありませんでした。 今もVPN接続している状態で編集作業をしています。

さいごに

必要最小限の作業にとどめたつもりでいるのですが、課題は色々あると思います。

例えば、

  • SSHはどこからでも接続できてしまう(→Firewallで閉じ、普段はさくらのコンソールから作業する等の対応は可能)
  • このサーバの状態を監視する手段がない

などです。

適宜、環境に合わせて追加・変更していただき、快適な運用をしていただければ幸いです。 ここまでお付き合いいただきましてありがとうございました。

Webツールも公開しています。 Web便利ツール@ツールタロウ

Java AUTO_INCREMENTに依存せず、欠番なくIDを作る機能を自前で実装し各種DBに対応する

はじめに

特定のDBの機能に依存した実装をしてしまうと、将来に大きな負担となる場合があります。

AUTO_INCREMENTは便利な機能ですが、機能を有していないDBも存在します。 同等の機能をアプリケーション側で実装し、 各種DBに対応できるようにするとともに、欠番が発生しないようにしてみます。

実装例

ID(シーケンス番号)を管理するためのテーブルを定義します。 カラム:NAMEには、この機能で発行するIDを使うテーブル名、 カラム:SEQUENCEには、テーブルごとのID(数値)が入ります。 桁数などは適当に調整してください。

# MySQL
CREATE TABLE SEQUENCE (
  NAME VARCHAR(255) BINARY NOT NULL,
  SEQUENCE BIGINT DEFAULT 0 NOT NULL,
  PRIMARY KEY PK_SEQUENCE (NAME)
);

# Oracle
CREATE TABLE SEQUENCE (
  NAME VARCHAR2(256) NOT NULL,
  SEQUENCE NUMBER(16) DEFAULT 0 NOT NULL,
  CONSTRAINT PK_SEQUENCE PRIMARY KEY (NAME)
);

# PostgreSQL / SQL Server / DB2
CREATE TABLE SEQUENCE (
  NAME VARCHAR(256) NOT NULL,
  SEQUENCE BIGINT DEFAULT 0 NOT NULL,
  CONSTRAINT PK_SEQUENCE PRIMARY KEY (NAME)
);

次に、IDを発行するクラスを定義します。

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Savepoint;

/**
 *
 * @author tool-taro.com
 */
public class SequenceTest {

    public static long create(Connection connection, String name) throws SQLException {
        long id = 0;

        PreparedStatement preparedStatement;
        ResultSet resultSet;
        int count;
        int update;
        Savepoint savepoint;

        connection.setAutoCommit(false);

        //初回はレコードがないのでINSERTする
        savepoint = connection.setSavepoint();
        count = 0;
        update = 0;
        preparedStatement = connection.prepareStatement("INSERT INTO SEQUENCE (NAME, SEQUENCE) VALUES (?, ?)");
        preparedStatement.setString(++count, name);
        preparedStatement.setLong(++count, 0);
        try {
            update = preparedStatement.executeUpdate();
        }
        catch (SQLException e) {
            connection.rollback(savepoint);
        }
        preparedStatement.close();

        if (update == 0) {
            //INSERTに失敗した場合はレコードがあるのでUPDATEする
            count = 0;
            preparedStatement = connection.prepareStatement("UPDATE SEQUENCE SET SEQUENCE = SEQUENCE + 1 WHERE NAME = ?");
            preparedStatement.setString(++count, name);
            update = preparedStatement.executeUpdate();
            preparedStatement.close();
            //INSERTもUPDATEも失敗したということは最初にINSERTした側のTransactionがrollbackされてレコードが存在しないということなので最初からやり直す
            if (update == 0) {
                return SequenceTest.create(connection, name);
            }

            count = 0;
            preparedStatement = connection.prepareStatement("SELECT SEQUENCE FROM SEQUENCE WHERE NAME = ?");
            preparedStatement.setString(++count, name);
            resultSet = preparedStatement.executeQuery();
            if (resultSet.next()) {
                id = resultSet.getLong("SEQUENCE");
            }
            resultSet.close();
            preparedStatement.close();
        }

        return id;
    }
}

ID発行機構の準備が終わりました。 サンプルでは、動作確認しやすいようにjspで実装しています。

<%-- 
    Author     : tool-taro.com
--%>

<%@page import="SequenceTest"%>
<%@page import="javax.sql.DataSource"%>
<%@page import="java.sql.Connection"%>
<%@page import="javax.naming.InitialContext"%>
<%@page contentType="text/html" pageEncoding="UTF-8" session="false" %>
<%
        //コネクションを取得するjndi
        String jndi = "java:comp/env/jdbc/MySQL";
        //String jndi = "java:comp/env/jdbc/Oracle"; //←Oracleの場合

        InitialContext context = null;
        Connection connection = null;

        //コネクション取得処理
        long id_1 = -1;
        long id_2 = -1;

        try {
                context = new InitialContext();
                DataSource dataSource = (DataSource) context.lookup(jndi);

                connection = dataSource.getConnection();
                //トランザクション分離レベルはDBによって異なるので設定を統一
                connection.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
                connection.setAutoCommit(false);

                //IDを取得してわざとrollbackする
                SequenceTest.create(connection, "TEST_TABLE_NAME");
                connection.rollback();

                //IDを取得してcommitする(初回アクセスなら"0"を得られるはず)
                id_1 = SequenceTest.create(connection, "TEST_TABLE_NAME");
                //このタイミングで対象のテーブルにデータを挿入するなどの処理を行う
                connection.commit();

                //IDを取得してわざとrollbackする
                SequenceTest.create(connection, "TEST_TABLE_NAME");
                connection.rollback();

                //IDを取得してcommitする(初回アクセスなら"1"を得られるはず)
                id_2 = SequenceTest.create(connection, "TEST_TABLE_NAME");
                //このタイミングで対象のテーブルにデータを挿入するなどの処理を行う
                connection.commit();
        }
        finally {
                if (context != null) {
                        try {
                                context.close();
                        }
                        catch (Exception e) {
                        }
                }
                if (connection != null) {
                        try {
                                connection.close();
                        }
                        catch (Exception e) {
                        }
                }
        }
%>
<!DOCTYPE html>
<html>
    <head>
        <title>tool-taro.com</title>
    </head>
    <body>
        取得したID_1="<%= id_1%>"<br>
        取得したID_2="<%= id_2%>"<br>
    </body>
</html>

動作確認

sequence_test.jspの実行結果を見てみましょう。

取得したID_1="0"
取得したID_2="1"

想定通りの結果を得られました。 2回目のアクセスでは次のような結果となります。

取得したID_1="2"
取得したID_2="3"

環境

Webツールも公開しています。 Web便利ツール@ツールタロウ

Java 正規表現を使ってUser-Agentから端末型番を簡単に取得する(ガラケー編)

はじめに

今は昔、の話ですが、応用すれば現代の端末にも使えると思います。

User-Agentの文字列中の端末型番の位置を正規表現でグループ化しておき、 java.util.regex.Matcherクラスを使って該当のグループの文字列部分だけを取得します。

なお、1点ご了承いただきたいのですが、 すでに存在しないキャリア名もありますし、 一部の端末のオプショナルなブラウザまではケアできていません(WILLCOMOperaなど)。

事前に準備する外部ライブラリ等はありません。 java.util.regex.Matcherクラス、およびjava.util.regex.Patternクラスを使用します。

実装例

サンプルでは、動作確認しやすいようにmainメソッドで実行できるようにしてあります。

import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 *
 * @author tool-taro.com
 */
public class UserAgentTest {

    public static void main(String[] args) {

        //型番を取得したいユーザエージェントのリスト
        String[] sources = {
            "DoCoMo/2.0 F900i(c100;TB;W22H12)",
            "UP.Browser/3.04-SN12 UP.Link/3.4.4",
            "KDDI-HI31 UP.Browser/6.2.0.5 (GUI) MMP/2.0",
            "J-PHONE/5.0/V801SA/SN123456789012345 SA/0001JP Profile/MIDP-1.0 Configuration/CLDC-1.0 Ext-Profile/JSCL-1.1.0",
            "MOT-V980/80.2F.2E. MIB/2.2.1 Profile/MIDP-2.0 Configuration/CLDC-1.1",
            "Vodafone/1.0/V705SH/SHJ001[/Serial] Browser/VF-NetFront/3.3 Profile/MIDP-2.0 Configuration/CLDC-1.1",
            "SoftBank/1.0/831SH/SHJ003/SN123456789012345 Browser/NetFront/3.5 Profile/MIDP-2.0 Configuration/CLDC-1.1",
            "Mozilla/3.0(WILLCOM;KYOCERA/WX340K/2;3.0.3.11.000000/1/C256) NetFront/3.4",
            "emobile/1.0.0 (H11T; like Gecko; Wireless) NetFront/3.4"
        };

        /*
            各キャリアの正規表現(型番判定に不要な部分はあいまいさを残し予期せぬアンマッチを避けています)
            docomo   ^DoCoMo/\d+\.\d+[ |/]{1,}([a-zA-Z0-9_\+\-]+).*$
            au       ^UP\.Browser/[\d\.a-z]+-([a-zA-Z0-9]+) .+|KDDI-([a-zA-Z0-9]+) UP\.Browser/[\_\d\.a-zA-Z]+ \(GUI\) .+$
            SoftBank ^J-PHONE/\d+\.\d+/([a-zA-Z0-9_\-]+).*|Vodafone/\d+\.\d+/([a-zA-Z0-9_\-]+).*|SoftBank/\d+\.\d+/([a-zA-Z0-9_\-]+).*|MOT-([a-zA-Z0-9_\-]+)/.*$
            WILLCOM  ^Mozilla/[\d\.]+\(WILLCOM;[a-zA-Z0-9\-_]+/([a-zA-Z0-9\-_]+)/.+\).+$
            emobile  ^emobile/[\d\.]+ \(([a-zA-Z0-9_\-]+); .+\).*$
         */
        //上記をまとめたパターンの集合体
        String patternString = "^DoCoMo/\\d+\\.\\d+[ |/]{1,}([a-zA-Z0-9_\\+\\-]+).*"
                + "|UP\\.Browser/[\\d\\.a-z]+-([a-zA-Z0-9]+) .+"
                + "|KDDI-([a-zA-Z0-9]+) UP\\.Browser/[\\_\\d\\.a-zA-Z]+ \\(GUI\\) .+"
                + "|J-PHONE/\\d+\\.\\d+/([a-zA-Z0-9_\\-]+).*"
                + "|Vodafone/\\d+\\.\\d+/([a-zA-Z0-9_\\-]+).*"
                + "|SoftBank/\\d+\\.\\d+/([a-zA-Z0-9_\\-]+).*"
                + "|MOT-([a-zA-Z0-9_\\-]+)/.*"
                + "|Mozilla/[\\d\\.]+\\(WILLCOM;[a-zA-Z0-9\\-_]+/([a-zA-Z0-9\\-_]+)/.+\\).+"
                + "|emobile/[\\d\\.]+ \\(([a-zA-Z0-9_\\-]+); .+\\).*";

        //判定処理
        Pattern pattern = Pattern.compile(patternString);
        Matcher matcher;
        String result;
        for (String source : sources) {
            result = null;
            matcher = pattern.matcher(source);
            if (source.matches(patternString) && matcher.find()) {
                //matcher.groupCount()はパターン数を返す(この例では9種類あるので"9"が返る)
                for (int i = 0; i < matcher.groupCount(); i++) {
                    /*
                        matcher.group(0)は、マッチした文字列全体を返す
                        例: "DoCoMo/\d+\.\d+[ |/]{1,}([a-zA-Z0-9_\+\-]+).*"にマッチする"DoCoMo/2.0 F900i(c100;TB;W22H12)"
                        
                        matcher.group(1)以降は、パターン内に()で指定されたグループにマッチした文字列を部分的に返す
                        例: "([a-zA-Z0-9_\+\-]+)"にマッチする"F900i"
                     */
                    if (matcher.group(i + 1) != null && !(matcher.group(i + 1)).equals(source)) {
                        result = matcher.group(i + 1);
                        break;
                    }
                }
                //標準出力
                System.out.format("判定 %1$s\n→%2$s\n", source, result);
            }
        }
    }
}

動作確認

$ UserAgentTest.java
$ java UserAgentTest
$ 判定 DoCoMo/2.0 F900i(c100;TB;W22H12)
→F900i
判定 UP.Browser/3.04-SN12 UP.Link/3.4.4
→SN12
判定 KDDI-HI31 UP.Browser/6.2.0.5 (GUI) MMP/2.0
→HI31
判定 J-PHONE/5.0/V801SA/SN123456789012345 SA/0001JP Profile/MIDP-1.0 Configuration/CLDC-1.0 Ext-Profile/JSCL-1.1.0
→V801SA
判定 MOT-V980/80.2F.2E. MIB/2.2.1 Profile/MIDP-2.0 Configuration/CLDC-1.1
→V980
判定 Vodafone/1.0/V705SH/SHJ001[/Serial] Browser/VF-NetFront/3.3 Profile/MIDP-2.0 Configuration/CLDC-1.1
→V705SH
判定 SoftBank/1.0/831SH/SHJ003/SN123456789012345 Browser/NetFront/3.5 Profile/MIDP-2.0 Configuration/CLDC-1.1
→831SH
判定 Mozilla/3.0(WILLCOM;KYOCERA/WX340K/2;3.0.3.11.000000/1/C256) NetFront/3.4
→WX340K
判定 emobile/1.0.0 (H11T; like Gecko; Wireless) NetFront/3.4
→H11T

それぞれの型番を取得できました。 久しぶりに見る型番ばかりで懐かしくなりました。

環境

Webツールも公開しています。 Web便利ツール@ツールタロウ

Java ログファイルを前回読み込んだ最終位置から読み込み始める

はじめに

何GBにもなるようなログファイルを毎回先頭から読み込み直していると大変です。 前回読み込んだ最終位置から読み込み開始できるようにします。

事前に準備する外部ライブラリ等はありません。 JavaSEに含まれるjava.io.RandomAccessFileクラスを使用します。

ログファイルは以下のファイルを使います。

三色団子1本目
三色団子2本目
三色団子3本目
三色団子4本目
三色団子5本目
三色団子6本目
三色団子7本目
三色団子8本目
三色団子9本目
三色団子10本目
三色団子11本目
三色団子12本目
三色団子13本目
三色団子14本目
三色団子15本目
三色団子16本目
三色団子17本目
三色団子18本目
三色団子19本目
三色団子20本目

実装例

サンプルでは、動作確認しやすいようにmainメソッドで実行できるようにしてあります。

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;

/**
 *
 * @author tool-taro.com
 */
public class LogReader {

    public static void main(String[] args) throws FileNotFoundException, IOException {

        //読み込みたいログファイル
        String logFilePath = "input.log";
        //エンコーディング
        String encoding = "UTF-8";
        //読み込んだ前回位置
        long pointer = 0;
        //読み込んだ最終位置
        long nextReturn;

        RandomAccessFile reader = null;
        int lineNumber = 0;
        String line;
        byte[] bytes;

        try {
            //初回は先頭から読み込む
            reader = new RandomAccessFile(new File(logFilePath), "r");
            while (true) {
                line = reader.readLine();
                //読み込んだ位置を記録する
                nextReturn = reader.getFilePointer();
                if (line == null) {
                    break;
                }

                //前回読み込んだ位置に戻り、今回の読み取り範囲をbyte配列で取得する(任意の文字コードで文字列に変換するため)
                {
                    reader.seek(pointer);
                    bytes = new byte[(int) (nextReturn - pointer)];
                    reader.read(bytes);
                    pointer = reader.getFilePointer();
                    line = new String(bytes, encoding);
                    //末尾の改行コードを除去
                    while (line.endsWith("\r") || line.endsWith("\n")) {
                        line = line.substring(0, line.length() - 1);
                    }
                }

                ++lineNumber;
                System.out.format("1回目 %1$d行目=[%2$s]\n", lineNumber, line);
                //10行読み込んだら終了してみる
                if (lineNumber >= 10) {
                    break;
                }
            }
            reader.close();
            System.out.println("--------------");
            System.out.format("ポインタの位置=%1$d\n", pointer);
            System.out.println("--------------");

            //2回目は前回の最終位置から読み込む
            reader = new RandomAccessFile(new File(logFilePath), "r");
            reader.seek(pointer);
            while (true) {
                line = reader.readLine();
                //読み込んだ位置を記録する
                nextReturn = reader.getFilePointer();
                if (line == null) {
                    break;
                }

                //前回読み込んだ位置に戻り、今回の読み取り範囲をbyte配列で取得する(任意の文字コードで文字列に変換するため)
                {
                    reader.seek(pointer);
                    bytes = new byte[(int) (nextReturn - pointer)];
                    reader.read(bytes);
                    pointer = reader.getFilePointer();
                    line = new String(bytes, encoding);
                    //末尾の改行コードを除去
                    while (line.endsWith("\r") || line.endsWith("\n")) {
                        line = line.substring(0, line.length() - 1);
                    }
                }

                ++lineNumber;
                System.out.format("2回目 %1$d行目=[%2$s]\n", lineNumber, line);
            }
        }
        finally {
            if (reader != null) {
                try {
                    reader.close();
                }
                catch (Exception e) {
                }
            }
        }
    }
}

RandomAccessFileクラスでは、readLineする際の文字コードの指定ができません。 その対策として、改行を検知してから一旦戻って読み込み直しているあたり、少し横着な実装にはなってしまいました。

動作確認

$ LogReader.java
$ java LogReader
$ 1回目 1行目=[三色団子1本目]
1回目 2行目=[三色団子2本目]
1回目 3行目=[三色団子3本目]
1回目 4行目=[三色団子4本目]
1回目 5行目=[三色団子5本目]
1回目 6行目=[三色団子6本目]
1回目 7行目=[三色団子7本目]
1回目 8行目=[三色団子8本目]
1回目 9行目=[三色団子9本目]
1回目 10行目=[三色団子10本目]
--------------
ポインタの位置=211
--------------
2回目 11行目=[三色団子11本目]
2回目 12行目=[三色団子12本目]
2回目 13行目=[三色団子13本目]
2回目 14行目=[三色団子14本目]
2回目 15行目=[三色団子15本目]
2回目 16行目=[三色団子16本目]
2回目 17行目=[三色団子17本目]
2回目 18行目=[三色団子18本目]
2回目 19行目=[三色団子19本目]
2回目 20行目=[三色団子20本目]

指定通り、10行目でいったん終了し、再開時には続きから読み込んでいます。 NFS経由でファイルを読み込む場合などにおいては、データ転送にかかるトラフィックも大幅に下げることができます。

環境

Webツールも公開しています。 Web便利ツール@ツールタロウ